すべてのC ++開発者が使用すべき10のC ++ 11機能

この記事では、すべての開発者が知って使用する必要のある多くのC ++ 11機能について説明します。 言語と標準ライブラリには多くの新しい追加がありますが、この記事ではそれらの一部のみを表面的に説明しています。 ただし、これらの新機能の一部は、すべてのC ++開発者にとってありふれたものになるはずです。 おそらく多くの同様の記事がありますが、この記事では、日常的に使用する機能のリストを作成してみます。



今日のプログラム:



#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-スマートポインター



ハブとこのトピックで書かれた他のリソースの両方に多くの記事がありますので、参照カウントと自動メモリ割り当て解除を備えたスマートポインターに言及したいだけです。

  1. unique_ptr :メモリリソースを共有すべきでない場合(コピーコンストラクターがない場合)に使用する必要がありますが、別のunique_ptr



    渡すことができます
  2. shared_ptr :メモリリソースを共有する必要がある場合に使用します
  3. weak_ptrshared_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 != &copy) { _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 ++開発者が知っておくべき一連の言語機能と標準ライブラリを紹介しました。 ただし、これまでに述べられたことすべてをより深く理解するには、この記事だけでは十分ではないため、追加の文献がなければこれを行う方法はありません。



All Articles