今日のプログラム:
auto
-
nullptr
- 範囲ベースのループ
-
override
およびfinal
- 強く型付けされた
enum
型 - スマートポインタ
- ラムダ
- 非メンバー
begin()
およびend()
-
static_assert
およびプロパティクラス - 変位のセマンティクス
#1-自動
C ++ 11より前は、変数(
register, static, extern
)を格納するための指定子として
auto
キーワードが使用されていました。 C ++ 11では、
auto
使用すると、変数の型を明示的に指定せずに、初期化される値の型に基づいて変数自体の実際の型を決定するようコンパイラーに指示できます。 これは、名前空間、ブロック、ループでの初期化など、さまざまなスコープで変数を宣言するときに使用できます。
auto i = 42; // i - int auto l = 42LL; // l - long long auto p = new foo(); // p - foo*
auto
使用すると、コードを短縮できます(もちろん、型が
int
ではなく、1文字少ない場合を除きます)。 コンテナを通過するために常に記述する必要があるSTLイテレータについて考えてください。 したがって、単純にするために
typedef
の定義は廃止されます。
std::map<std::string, std::vector<int>> map; for(auto it = begin(map); it != end(map); ++it) { // do smth } // , ++03 ++11 // C++03 for (std::vector<std::map<int, std::string>>::const_iterator it = container.begin(); it != container.end(); ++it) { // do smth } // C++11 for (auto it = container.begin(); it != container.end(); ++it) { // do smth }
戻り値は
auto
できないことに注意してください。 ただし、関数の戻り値型の代わりに
auto
を使用できます。 この場合、
auto
は型を決定する必要があることをコンパイラに通知せず、関数の最後で戻り値の型を検索するコマンドを提供するだけです。 以下の例では、
compose
関数の戻り値の型は+演算子の戻り値の型であり、
T
型と
E
型の値を合計します
E
template <typename T, typename E> auto compose(T a, E b) -> decltype(a+b) // decltype - { return a+b; } auto c = compose(2, 3.14); // c - double
#2-nullptr
以前は、ポインターをNULLにするために、ゼロ(整数型)であるNULLマクロが使用されていました。これは自然に問題を引き起こしました(たとえば、関数がオーバーロードされたとき)。
nullptr
は独自の型
std::nullptr_t
があり、以前の問題から私たちを救います。
nullptr
から任意の型のnullポインターおよび
bool
(
false
)への暗黙的な変換がありますが、整数型への変換はありません。
void foo(int* p) {} void bar(std::shared_ptr<int> p) {} int* p1 = NULL; int* p2 = nullptr; if(p1 == p2) {} foo(nullptr); bar(nullptr); bool f = nullptr; int i = nullptr; // : int reinterpret_cast
#3-範囲ベースのループ
C ++ 11では、セットを反復処理するための
foreach
パラダイムのサポートが追加されました。 新しいフォームでは、繰り返しオブジェクトの
begin()
および
end()
メソッドがオーバーロードされている場合、繰り返しを実行できます。
これは、インデックス、イテレータ、または要素の数を気にせずに、配列/コンテナの要素を取得したり、それらで何かをしたい場合に便利です。
std::map<std::string, std::vector<int>> map; std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); map["one"] = v; for(const auto &kvp: map) { std::cout << kvp.first << std::endl; for(auto v: kvp.second) std::cout << v << std::endl; } int arr[] = {1,2,3,4,5}; for(int &e: arr) e *= e;
#4-オーバーライドおよびファイナル
私はいつもC ++の仮想関数が好きではありませんでした。
virtual
オプションであるため、コードの読み取りが少し難しくなり、常に継承階層の最上部に戻り、特定のメソッドが仮想と宣言されているかどうかを確認します。 コードを理解しやすくするために、派生クラスでもこのキーワードを常に使用しています(そして、これを行った人に奨励しています)。 ただし、まだ発生する可能性のあるエラーがあります。 次の例をご覧ください。
class B { public: virtual void f(short) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) {std::cout << "D::f" << std::endl;} };
D::f
B::f
D::f
オーバーライドします。 ただし、シグネチャは異なり、1つのメソッドは
short
受け入れ、もう1つのメソッドは
int
受け入れます。したがって、
B::f
は同じ名前の別のメソッドであり、オーバーロードされ、オーバーライドされません。 したがって、基本クラスへのポインターを使用して、
f()
を呼び出し、「オーバーライド」メソッドの出力「D :: f」を待つことができますが、出力は「B :: f」になります。
別のエラーの可能性があります。パラメーターは同じですが、基本クラスではメソッドは定数ですが、派生クラスではそうではありません。
class B { public: virtual void f(int) const {std::cout << "B::f " << std::endl;} }; class D : public B { public: virtual void f(int) {std::cout << "D::f" << std::endl;} };
繰り返しますが、これらはオーバーライドされた関数ではなく、2つのオーバーロードされた関数です。
幸いなことに、現在これらのエラーを取り除く方法があります。 キーワードではなく2つの新しい識別子が追加されました:メソッドが基本クラスの仮想メソッドのオーバーライドであることを示すoverrideと、派生クラスが仮想メソッドをオーバーライドしてはならないことを示す
final
です。 最初の例は次のようになります。
class B { public: virtual void f(short) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) override {std::cout << "D::f" << std::endl;} };
これにより、コンパイルエラーがスローされます(2番目の例で
override
を使用する場合とまったく同じです)。
'D::f': method with override specifier 'override' did not override any base class methods
一方、(階層の下で)オーバーライドすることを意図していないメソッドを作成する場合は、
final
としてマークする必要があります。 派生クラスでは、両方の識別子を一度に使用できます。
class B { public: virtual void f(int) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) override final {std::cout << "D::f" << std::endl;} }; class F : public D { public: virtual void f(int) override {std::cout << "F::f" << std::endl;} };
final
として宣言された関数は、関数
F::f()
でオーバーライドできません。この場合、クラス
D
基本クラス(
)のメソッドをオーバーライドします
D
#5-強く型付けされた列挙型
C ++の「従来の」列挙にはいくつかの欠点があります。値を周囲のスコープにエクスポートし(名前の競合を引き起こす可能性があります)、暗黙的に型全体に変換され、ユーザー定義型を持つことはできません。
これらの問題はC ++ 11で修正され、 強く型付けされた列挙と呼ばれる列挙の新しいカテゴリが導入されました 。 それらは、
enum class
キーワードによって定義されます。 列挙値を周囲のスコープにエクスポートしなくなり、暗黙的に整数型に変換されなくなり、ユーザー定義型を持つことができます(このオプションは「従来の」列挙にも追加されます)。
enum class Options {None, One, All}; Options o = Options::All;
#6-スマートポインター
ハブとこのトピックで書かれた他のリソースの両方に多くの記事がありますので、参照カウントと自動メモリ割り当て解除を備えたスマートポインターに言及したいだけです。
- unique_ptr :メモリリソースを共有すべきでない場合(コピーコンストラクターがない場合)に使用する必要がありますが、別の
unique_ptr
渡すことができます - shared_ptr :メモリリソースを共有する必要がある場合に使用します
- weak_ptr :
shared_ptr
によって制御されるオブジェクトへの参照を含みますが、リンクはカウントしません。 周期的な依存関係を取り除くことができます
次の例は
unique_ptr
示しています。 オブジェクトの所有権を別の
unique_ptr
に転送するには、std :: moveを使用します(この機能については、最後の段落で説明します)。 所有権を転送した後、所有権を転送したスマートポインターはnullになり、
get()
は
nullptr
を返します。
void foo(int* p) { std::cout << *p << std::endl; } std::unique_ptr<int> p1(new int(42)); std::unique_ptr<int> p2 = std::move(p1); // transfer ownership if(p1) foo(p1.get()); (*p2)++; if(p2) foo(p2.get());
2番目の例は
shared_ptr
示しています。 所有権が共有されるようになったため、セマンティクスは異なりますが、使用方法は似ています。
void foo(int* p) { } void bar(std::shared_ptr<int> p) { ++(*p); } std::shared_ptr<int> p1(new int(42)); std::shared_ptr<int> p2 = p1; bar(p1); foo(p2.get());
最初の宣言は次と同等です。
auto p3 = std::make_shared<int>(42);
make_sharedは、少なくとも2つの割り当てが必要なコンストラクターを通じて
shared_ptr
を明示的に取得するのとは対照的に、共有オブジェクトとスマートポインターにメモリを1つの割り当てで割り当てるという利点がある関数です。 これにより、メモリリークが発生する可能性があります。 次の例では、これだけを示しています
seed()
が例外をスローすると、リークが発生する可能性があります。
void foo(std::shared_ptr<int> p, int init) { *p = init; } foo(std::shared_ptr<int>(new int(42)), seed());
この問題は、
make_shared
を使用して解決されます。
最後に、
weak_ptr
た例。
lock()
を呼び出してオブジェクトにアクセスすることにより、オブジェクトの
shared_ptr
を取得する必要があることに注意してください。
auto p = std::make_shared<int>(42); std::weak_ptr<int> wp = p; { auto sp = wp.lock(); std::cout << *sp << std::endl; } p.reset(); if(wp.expired()) std::cout << "expired" << std::endl;
#7-ラムダ
新しい標準では、ラムダ式のサポートがついに追加されました。 ファンクタまたは
std::function
が予想される
std::function
はいつでもラムダを使用できます。 ラムダは、一般的に言えば、ファンクターの短い表記であり、匿名ファンクターのようなものです。 詳細については、 MSDNなどをご覧ください。
std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(v), std::end(v), is_odd); if(pos != std::end(v)) std::cout << *pos << std::endl;
少し複雑になりました-再帰ラムダ。 フィボナッチ関数を表すラムダを想像してください。
auto
を使用して記述しようとすると、コンパイルエラーが発生します。
auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);};
error C3533: 'auto &': a parameter cannot have a type that contains 'auto' error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer error C3536: 'fib': cannot be used before it is initialized error C2064: term does not evaluate to a function taking 1 arguments
循環依存があります。 それを取り除くには、
std::function
を使用して関数のタイプを明示的に決定する必要があり
std::function
。
std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);};
#8-非メンバーbegin()およびend()
前の例では、
begin()
および
end()
関数を使用していることに気づいたでしょう。 これは、標準ライブラリへの新しい追加です。 これらはすべてのSTLコンテナで機能し、任意のタイプで機能するように拡張できます。
たとえば、ベクトルを出力して最初の奇数要素を探す前の例を見てみましょう。
std::vector
をCのような配列に置き換えると、コードは次のようになります。
int arr[] = {1,2,3}; std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto begin = &arr[0]; auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]); auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl;
begin()
および
end()
使用すると、次のように書き換えることができます。
int arr[] = {1,2,3}; std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd); if(pos != std::end(arr)) std::cout << *pos << std::endl;
これは、
std::vector
を使用したコードとほぼ完全に同じです。 したがって、
begin()
および
end()
サポートされるすべての型に対して1つの汎用メソッドを作成できます。
template <typename Iterator> void bar(Iterator begin, Iterator end) { std::for_each(begin, end, [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl; } template <typename C> void foo(C c) { bar(std::begin(c), std::end(c)); } template <typename T, size_t N> void foo(T(&arr)[N]) { bar(std::begin(arr), std::end(arr)); } int arr[] = {1,2,3}; foo(arr); std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v);
#9-static_assertおよびプロパティクラス
static_assert
は、コンパイル時にステートメントをチェックします。 ステートメントが真の場合、何も起こりません。 falseの場合、コンパイラは指定されたエラーメッセージを表示します。
template <typename T, size_t Size> class Vector { static_assert(Size > 3, "Size is too small"); T _points[Size]; }; int main() { Vector<int, 16> a1; Vector<double, 2> a2; return 0; }
error C2338: Size is too small see reference to class template instantiation 'Vector<T,Size>' being compiled with [ T=double, Size=2 ]
static_assert
は、プロパティクラスで使用するとさらに便利になります。 これは、コンパイル時に型情報を提供するクラスのコレクションです。 それらは
<type_traits>
ヘッダーで利用可能です。 このヘッダーには、ヘルパークラス、変換クラス、プロパティクラス自体のクラスがいくつかあります。
次の例では、
add
関数は整数型でのみ機能することになっています。
template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1 + t2; }
ただし、次のように記述してもコンパイルエラーは発生しません。
std::cout << add(1, 3.14) << std::endl; std::cout << add("one", 2) << std::endl;
プログラムは単に「4.14」と「e」を表示します。
static_assert
を使用すると、これらの2行はコンパイル時にエラーを
static_assert
ます。
template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); return t1 + t2; }
error C2338: Type T2 must be integral see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled with [ T2=double, T1=int ] error C2338: Type T1 must be integral see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled with [ T1=const char *, T2=int ]
#10-運動のセマンティクス
これは、C ++ 11で取り上げられた別の重要なトピックです。 このテーマについては、段落ではなく複数の記事を書くことができるので、これ以上深くは述べません。
C ++ 11では、右辺値参照(&&で指定)の概念を導入して、左辺値(名前を持つオブジェクト)と右辺値(名前がないオブジェクト)への参照を区別しました。 再配置のセマンティクスにより、右辺値を変更できます(以前は変更されていないと見なされ、const Tと型とは異なりませんでした)。
暗黙的なメンバー関数を使用するために使用されるクラス/構造体:デフォルトのコンストラクター(他のコンストラクターが定義されていない場合)、コピーコンストラクター、およびデストラクター。 コピーコンストラクターは、変数のビットごとのコピーを実行します。 これは、オブジェクトへのポインターを持つクラスがある場合、コピーコンストラクターはポインターがコピーするのであって、それらが指すオブジェクトではないことを意味します。 オブジェクトへのポインタだけでなく、コピー内のオブジェクトを正確に取得する場合は、コピーコンストラクターで明示的にこれを記述する必要があります。
移動コンストラクターと移動代入演算子-これら2つの特別な関数は、パラメーターT &&を受け取ります。これは右辺値です。 実際、彼らはオブジェクトを変更できます。
次の例は、ダミーバッファの実装を示しています。 バッファは名前で識別され、タイプTの要素の配列へのポインタ(
std::unique_ptr
ラップ)、および配列のサイズを含む変数を持っています。
template <typename T> class Buffer { std::string _name; size_t _size; std::unique_ptr<T[]> _buffer; public: // default constructor Buffer(): _size(16), _buffer(new T[16]) {} // constructor Buffer(const std::string& name, size_t size): _name(name), _size(size), _buffer(new T[size]) {} // copy constructor Buffer(const Buffer& copy): _name(copy._name), _size(copy._size), _buffer(new T[copy._size]) { T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } // copy assignment operator Buffer& operator=(const Buffer& copy) { if(this != ©) { _name = copy._name; if(_size != copy._size) { _buffer = nullptr; _size = copy._size; _buffer = (_size > 0)? new T[_size] : nullptr; } T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } return *this; } // move constructor Buffer(Buffer&& temp): _name(std::move(temp._name)), _size(temp._size), _buffer(std::move(temp._buffer)) { temp._buffer = nullptr; temp._size = 0; } // move assignment operator Buffer& operator=(Buffer&& temp) { assert(this != &temp); // assert if this is not a temporary _buffer = nullptr; _size = temp._size; _buffer = std::move(temp._buffer); _name = std::move(temp._name); temp._buffer = nullptr; temp._size = 0; return *this; } }; template <typename T> Buffer<T> getBuffer(const std::string& name) { Buffer<T> b(name, 128); return b; } int main() { Buffer<int> b1; Buffer<int> b2("buf2", 64); Buffer<int> b3 = b2; Buffer<int> b4 = getBuffer<int>("buf4"); b1 = getBuffer<int>("buf5"); return 0; }
デフォルトのコピーコンストラクタとコピー割り当て演算子はおなじみのはずです。 C ++ 11の新機能は、移動コンストラクターと移動代入演算子ですこのコードを実行すると、
b4
作成時に移動コンストラクターが呼び出されることがわかります。 さらに、
b1
値
b1
割り当てられると、移動割り当て演算子が呼び出されます。 その理由は、
getBuffer()
関数によって返される値が右辺値であるためです。
おそらく、変数名とバッファーへのポインターを初期化するときに、 移動コンストラクターでstd :: moveを使用していることに気づいたでしょう。 名前は文字列
std::string
あり、
std::string
は移動のセマンティクスも実装します。
unique_ptr
についても同じことが
unique_ptr
ます。 ただし、単に
_name(temp._name)
場合、コピーコンストラクターが呼び出されます。 しかし、なぜこの場合、
std::string
moveコンストラクタが呼び出されなかったのですか? 実際、
Buffer
の移動コンストラクターが右辺値で呼び出された場合でも、コンストラクター内では左辺値として表示されます。 再び右辺値にするには、
std::move
を使用する必要があります。 この関数は、単純に左辺値参照を右辺値に変換します。
結論の代わりに
C ++ 11には、話すことができるし、話すべきことがたくさんあります。 この記事は、考えられる多くの始まりの1つにすぎません。 この記事では、すべてのC ++開発者が知っておくべき一連の言語機能と標準ライブラリを紹介しました。 ただし、これまでに述べられたことすべてをより深く理解するには、この記事だけでは十分ではないため、追加の文献がなければこれを行う方法はありません。