C++11 範囲に基づく for 文

多くのスクリプト言語では for in の形式でコンテナーの要素にアクセスできます。 Boost や Qt などでは C++ でもなんとかそれを実現させようとマクロを駆使して for_each として定義されていました。
しかし、とうとう C++11 で範囲 for は言語機能として正式に追加されました。
今回はそんな 範囲 for について紹介します。

範囲 for とは

範囲 for というのは C++11 で The range-based for statement(範囲に基づく for 文) として規定されたもので、 配列、リストといったコンテナーの各要素に順にアクセスする for です。


プログラミング中では、配列などのコンテナーに対してループを回して要素にアクセスする というのはよく行う処理です。
その際、 今まではループカウンターを回したり、イテレーターを使っていたと思います。 範囲 for を使えば次のように書けます。
  vector<int> ary = { 3,2,9,6 };

  for (auto elem : ary) {
    cout << elem << endl;
  }  
ループカウンターやイテレーターを書く必要がなく、簡単に書けるようになります。 すっきりしたので、 "コンテナーの要素にアクセスしている" というコードの意図が分かり易くなるという利点もあります。
また、添え字アクセスと違い、イテレーターのようにランダムアクセス以外にも使える汎用性があります。

なお、この考えを一歩進めると関数型のデータ処理となり、 for の逐次処理以外のいろんな処理ができるようになります。

範囲 for の対象

範囲 for に使えるコンテナーは次の 3 つです。
  • STL のコンテナーなど begin(), end() メソッドを持つクラス
  • 配列
  • 初期化リスト
前章のコンテナー以外もサンプルをあげておきます。

配列:
  int cary[] = { 3,2,9,6 };
  
  for (auto elem : cary) {
    cout << elem << endl;
  }
初期化リスト:
  for (auto elem : { 3,2,9,6 }) {
    cout << elem << endl;
  }

要素の型

範囲 for の要素(elem)の定義にはコンテナーの要素の型を記述します。 これには C++11 に型変換として追加された auto が使えます。

その際、関数の引数のようにコンテナーに変更を加えるかどうかによって、 参照(auto&)か const 参照(const auto &)になります。

変更を加える:
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (auto &elem : strs) {
    elem += "!";
  }
  // { "foo!", "bar!", "baz!" }   
変更を加えない:
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (const auto &elem : strs) {
    cout << elem << endl;
  }  
ただし、最初のサンプルのように要素が int などサイズが小さい場合は、 コピーしても問題がないため、ただの auto にします。
また、参照ではなく、ポインターを指定することも出来ます。

std::map の場合

Boost の for_each の時は std::map だとちょっと面倒でした。 範囲 for の場合は問題なく使えます。
  map<string, int> strmap = {
      { "foo", 3 },
      { "bar", 2 },
      { "baz", 9 }
  };
  for (const auto &elem : strmap) {
    cout << elem.first << " - " << elem.second << endl;
  }
std::map の場合の要素は std::pair です。
範囲 for の対象は begin(), end() を持つ必要がありました。 すなわち、各要素にはイテレーターを使ってアクセスしています。
前章では要素の型をいくつか紹介しましたが、 「イテレーターから導けるものが型として使える」と言えます。

範囲 for は次の記述を簡略化したものと考えると分かりやすいと思います。
  for (auto itr = ary.begin() ; itr != ary.end() ; itr++) {
    auto elem = *itr;
    // 要素に対する処理
  }  

サンプルコード

記事中で紹介したサンプルコードのファイルです。


 

C++11 型推論(auto, decltype)の徹底活用

C++11, 14 により C++ でも 型推論(auto) が使えるようになりました。 C++11 の機能の中でも型変換は手っ取り早くコードを書くのが楽になるので、 使っている人は多いと思います。
今回はその型推論をもっと徹底的に使っていこうというお話です。

型推論とは

C++ のような静的型付けの言語では変数の型はきっちり指定しておく必要があります。 いちいち書くのはめんどくさいですが、 コードの安全性や実行プログラムの速度に効果があります。
一方、Ruby や Python のような動的型付け言語では、 変数自体が型情報を持っていて、型を指定する必要がなく楽ちんですし、 配列に任意の型を入れられるといった柔軟性があります。
これはどちらが優れているというものではなく、言語には適材適所があるということです。

とはいえ、めんどくさいものはめんどくさいので、「少しでも楽をしよう」というのが型推論です。
型推論では変数の型を与えられた初期値から推論します。 それを使って、型を書くのを省略できるところは省略してしまおうという方法です。 C++ の型推論では型宣言に auto と書きます。 それをコンパイラーが初期値から推論して型を決めてくれます。

型推論はどこまで使えるのか

型推論はローカル変数以外にも使えます。 対象をまとめると次のようになります。

対象 対応
ローカル変数
グローバル変数 ×
クラスのメンバー変数 ×
クラス変数(static) const のみ可
関数の戻り値 仮想(virtual)関数以外 (C++14)
関数の引数 ラムダ式のみ可 (C++14)


C++14 は C++11 の補足的な変更なのですが、C++14 で結構広がりました。 特に「関数の戻り値」の型推論は意外な実力を秘めています。
それを含め次章から個々の型推論について説明してきます。

ローカル変数

通常ローカル変数

関数や制御文のブロック内での変数で、型推論として一番よく使う場面だと思います。
int foofunc()
{
  auto localver = 1;  
const やポインター(*)、参照(&)等に変えたい場合、修飾子は別途付ける必要があります。 (もともとポインターであれば auto でポインター)

リテラルの工夫

文字列リテラルを初期値として渡す場合は型推論では const char * になります。(環境によって char * かも)
しかし std::string にしたいという時もあると思います。 そういった場合には C++14 で追加された basic_string のリテラルを使うと出来ます。
  auto cstr = "Hellow";  // const char *
  auto str  = "Hellow"s; // std::string  
そのほかのクラスでよく使うものがあれば、C++11 では自作のリテラルを定義することもできるようになっています。

範囲 for

明示的に初期値を与えているわけではありませんが、C++11 の 範囲 for 文 でも型推論は使えます。
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (const auto &elem : strs) {
    cout << elem << endl;
  }  

グローバル変数

グローバル変数の場合には extern 宣言を必要とするため、使うことはできません。
extern int64_t ErrNo;  
ファイルローカル、逆に言うとファイル内でグローバルな static 変数に関しても、auto が使えます。
static auto stvar = 1.0;
static を外したものは分類としては名前空間スコープ変数に入り、auto は使えます。 そのままだと普通は使いませんが、const を付けて定数として公開することはあります。
const auto VERION = "1.0.0";  

クラス関連の変数

クラス変数 (静的メンバー変数)

クラス定義で static を付けると静的メンバーとなり、変数はクラス変数となります。 クラス変数は const の場合、初期値を指定することができます。
class Foo
{
  static const int Dim = 2;
  // static int Dim = 2; NG const のみ OK
  static constexpr double Mag = 0.5;
const が付けられるのは int のみです。C++11 では constexpr を付けると他の型でも初期値が指定できるようになっています。 const ではないクラス変数では初期値を指定できないので auto は使えませんが、 上記のようなクラス定数であれば auto が使えます。
class Foo
{
  static const auto Dim = 2;
  static constexpr auto Mag = 0.5;

インスタンス変数 (メンバー変数)

メンバー変数でいえば、C++11 からクラス変数ではない通常のメンバー変数(インスタンス変数)でも初期値を指定できるようになりました。 初期値を指定できるので auto も使えるのではないかなと思ったのですが、auto の対象外っぽいです。
class Foo
{
  /// メンバー変数
  int m_bar = 0;
  // auto m_baz = 0; NG

関数の戻り値

戻り値の型推論

C++14 から戻り値の型推論もできるようになりました。
例えば次の関数では戻り値は a または b でともに T なので、 コンパイラーが T と確定します。
template <typename T>
auto StdMax(const T &a, const T &b)
{
  return (a < b) ? b : a;
}  
  cout << StdMax(4, 12) << endl;  // 12
戻り値の型推論は通常関数、クラスのメンバー関数にかかわらず使えます。 ただし、 virtual なメンバー関数の場合は auto は使えません。 これは、オーバーライドのチェックと仮想関数テーブルのレイアウトが複雑になるためらしいです。

型が確定しない戻り値 : 三項演算子

前節の StdMax() は std::max() とほぼ同じなのですが、 整数のリテラルと int64_t や double を比較するとコンパイルエラーになるので、使いづらいです。 そこで、引数の型を別々にできるようにしてみます。
 template <typename T, typename U>
 auto MyMax(const T &a, const U &b)
 {
   return (a < b) ? b : a;
 }
こうすると、引数によって戻り値の型がかわる関数を作成することができます。
  cout << MyMax(4.2, 12) << endl;  // 12
  cout << MyMax(4.2,  2) << endl;  // 4.2  

型が確定しない戻り値 : 複数の return 文

複数のリターン文で戻り値の型が確定しない場合には型推論はできません。 前節の関数を if 文を使うように直してみます。
template <typename T, typename U>
auto MyMax2Return(const T &a, const U &b)
{
  if (a < b) {
    return b;
  }
  else {
    return a;
  }
}  
これだと MyMax2Return(4.2, 12) のように T, U が別々の型になる場合にはコンパイルエラーとなってしまいます。
  cout << MyMax2Return(4, 12) << endl;  // 12
  // cout << MyMax2Return(4.2, 12) << endl;  // コンパイルエラー
「さっきはできたなのになんで」って思いますが、 どちらかというと 出来る方が C++11 の仕様的にいいのかな という感じです。
これに限って言えば三項演算子に戻せばいいのですが、 「どうしても複数の return 文する必要がある」として解決策を考えてみます。

どうすれば解決できるかというと、C++11 で追加された式の型を取得する decltype後置の戻り値型宣言を使います。
template <typename T, typename U>
auto FixedMyMax2Return(const T &a, const U &b) -> decltype(a+b)
{
  if (a < b) {
    return b;
  }
  else {
    return a;
  }
}  
"a+b" の型というのは、例えば double と int との変数を足すと double になります。 基本型の四則演算の結果のように新しい型を決めてその値を返します。
  cout << FixedMyMax2Return(4.2, 12) << endl;  // 12.0
  cout << FixedMyMax2Return(4.2,  2) << endl;  //  4.2  

decltype の活用

decltype を使えば、今まで出来なかったようなことができるようになります。

例えば、以下のような点クラス(struct)があったとします。
template <typename T>
struct Point
{
  T x;
  T y;
};  
このクラスの掛け算(*)の演算子をオーバーロードしたとして、次の計算をします。
  Point<int>(3,2) * 0.5 
戻り値をしては Point<double>(1.5,1.0) のような double の点を返してくれるのが一番自然だと思います。
しかし、今まではこれを実現するのは不可能でした。 正確には基本型の全種別を個別に定義していけば可能ですが、現実的ではありません。

これが decltype と戻り値の型推論を使えば、基本型のように型を変えるクラスを簡単に定義できます。
template <typename T, typename U>
inline auto operator*(const Point<T> &a, U b)
{
  return Point<decltype(a.x*b)>(a.x*b, a.y*b);
}
  cout << Point<int>(3, 2) * 0.5 << endl;  // Point<double>(1.5, 1.0)
ちなみに decltype と後置の宣言でも定義可能です。
template <typename T, typename U>
inline auto operator*(const Point<T> &a, U b) -> Point<decltype(a.x*b)>
{
  return {a.x*b, a.y*b};
}

関数の引数

引数宣言の auto

関数の引数の宣言に auto を使うことを考えてみます。
先ほどの点クラスに引数、戻り値ともに auto で指定したとします。
template <typename T>
struct Point
{
    :
  auto Plus(auto val) const
  {
    return Point(static_cast<T>(x+val), static_cast<T>(y+val));
  }
これは初期値から推測する型推論ではなくなっているのに気付かれるでしょうか。 こうなってくるとテンプレートを簡略化した 自動ジェネリック というものになってきます。

ただ、残念ながらこれはコンパイルできるようになっていません。 しかし、仕様上は OK なのかはわかりませんが、 戻り値の型推論をしないと gcc ではコンパイルが通ります。
  // 引数だけ auto
  Point PlusAutoArg(auto val) const
  {
    return Point(static_cast<T>(x+val), static_cast<T>(y+val));
  }
前章と同様に戻り値だけ auto も出来ます。
  // 戻り値だけ auto
  template <typename U>
  auto PlusAutoReturn(U val) const
  {
    return Point<decltype(x+val)>(x+val, y+val);
  }

ジェネリックラムダ (C++14)

C++14 からラムダ式に限り、 引数、戻り値ともに auto を指定できるようになっています。
正確にいうともともとラムダ式は戻り値は記述せずに型推論し、 C++14 から引数宣言で auto が使えるようになりました。
  auto plus = [](auto a, auto b) { return a + b; };
  cout << plus(2,   3) << endl; // result = 5
  cout << plus(2.5, 3) << endl; // result = 5.5  
これまた、「これが大丈夫なのに、さっきのはなんでダメなの」という気はします。 ただ、ラムダ式のように関数内に書くのに template 宣言は書けないですし、 頑張って仕様として入れたのではないかと思います。

サンプルコード

今まで出てきたコードのファイルもあげておきます。


 

C++ のスタイルを変えるかもしれない右辺値参照とムーブセマンティクス

C++11 で右辺値参照とムーブセマンティクスが追加されました。 これらを活用するとコピーのコストを削減できるようになり、 C++ では今まで出来なかったスタイルを広める可能性を秘めています。
そこで今回はその右辺値参照とムーブセマンティクスについて説明します。 これらは難しいと思っている人もいるかもしれませんが、ポイントを押さえればそんなに難しい話ではありません。 理解さえすれば、C++ コーディングにおける強力な武器になるはずです。

変数の不変性を重視したスタイル

C++ でいままでできなかったスタイルというのは変数の不変性を重視したスタイルです。
ここでいう変数の不変性一度作ったオブジェクトを変更しないことを言います。

例えば、文字列を大文字に変換する関数があったとします。

prologue.cpp:
 string ToUpperString(const string &src)
 {
   string dest;
 
   for (auto ch : src) {
     dest.push_back(::toupper(ch));
   }
   return dest;
 }
   string src = "Hello";
   auto newstr = ToUpperString(src);
受け取った引数(src)を変更するのではなく、src はそのままで変換した新しい文字列を返します。
不変性を重視するスタイルでは、変数を変更したい場合にオブジェクトの中身を変更するのではなく、 こういった関数を使って新しい変数を作成していきます。

変数の不変性(参照透過性)

なぜ、変数の不変性を重視しないといけないのでしょうか?
変数をなるべく変えないようにした方がコードの質、安全性が上がります。 オブジェクトの変更点をきっちり追わないといけないのは手間ですし、見過ごしによる危険を伴います。 これは const の重要性が分かっていれば、理解しやすいのではないでしょうか。 ただ、それらは感覚的な面も多いのですが、スレッドなどの並列処理では参照しているオブジェクトが変わらない(参照透過性)というのは、 安全面でかなり重要です。
cpp_move_reftrans.png

実際、C#, Java, JavaScript などの言語では文字列オブジェクトは変更不可(immutable)となっています。 これをさらに、すべての変数に適用していたのが関数型プログラミングです。

C++ ではなぜできなかったのか

それほど有用なスタイルですが、C++ ではあまり採用されていません。(Qt などでは少し採用されています)
というのもこのスタイルでは無駄が多いためです。先ほどの関数では戻り値を返すところでコピーが発生します。

cpp_move_cpfunc.png

他の言語などではポインターのように扱うものが多く、作成したオブジェクトを返してもポインター分のコピーにしかなりません。 一方、C++ ではオブジェクトのコピーではコストがかかりますし、ポインターを返すとガベッジコレクション(GC)がないため管理に問題が出てきます。

もともと変数の不変性を重視したスタイルは効率を犠牲にすることが多いのですが、 戻り値のコピーは完全に無駄で許容するには大きすぎます。

ムーブセマンティクスによる解決

実をいうとかしこいコンパイラーでは関数の戻り値でコピーが発生しない場合があります。 これは 戻り値の最適化(RVO) や 名前付き戻り値の最適化(NRVO) といった技術なのですが、 これは必ずできるわけではありません。

ムーブセマンティクスが C++11 で導入された要因の一つはこういったコピーのコストを明示的に減らすことです。
 string ToUpperStringEffective(const string &src)
 {
   string dest;
 
   for (auto ch : src) {
     dest.push_back(::toupper(ch));
   }
   return std::move(dest);
 }
std::move を使うとコピーではなく、移動になります。 次章でこの「移動」について説明します。

ムーブセマンティクスと右辺値

ムーブセマンティクスの「セマンティクス」は直訳すると「意味論」ですが、式や構文などよりもちょっと広い感じで、 「移動に関連すること」ぐらいに思ってください。

ムーブセマンティクス

関数の戻り値だとちょっと説明しづらいので、次のような変数の swap(入れ替え)を行う場合を考えてみます。
 void Swap(string &a, string &b)
 {
   string tmp = b;
   b = a;
   a = tmp;
 }
std::string の中身は実装依存ですが、 文字数やヒープ領域に確保した配列はメンバーに持っているはずです。 そういったアロケートしたメモリーを持つオブジェクトが対象だとします。

アロケートしたメモリーへのポインターがある場合には浅いコピーの問題が発生します。
単純な浅いコピーだと同じメモリーを二つのオブジェクトで持ってしまいます。
cpp_move_cpraw.png


これを回避するためにはコピー先でもアロケートしたメモリーにコピーする必要があります。 こういった処理を深いコピーやクローンと言います。
このため、アロケートしたメンバーを持つ場合はコピーコンストラクターや代入演算子をオーバーロードします。
cpp_move_cpdeep.png


C++11 で新しくできるようになったのが移動です。 浅いコピーでは二つのオブジェクトが共有するのがダメなので、元の方から取ってしまいます。
cpp_move_cpmv.png

ここで Swap の処理を見てみると tmp は関数をでると解放される変数ですが、代入する時に深いコピーが発生します。 この文字列がとても長い場合などかなりの無駄が生じます。
移動をすると当然、元の方は使えなくなってしまいます。 しかし、「使わなくなると分かっているのだったら移しちゃえ」ということで移動させるのがムーブセマンティクスの考え方です。 移動させることによりコピー時のコストが削減できます。


コピーではなく、移動させるためには std::move を使います。
 void SwapEffective(string &a, string &b)
 {
   string tmp = b;
   b = a;
   a = std::move(tmp);
 }
ちなみに tmp ← b, b ← a の時にも std::move を使ってもいいのですが、 分かりやすいように明らかにいらない tmp だけにしています。

なぜ、std::move を使うと移動になるのかというと、tmp が右辺値として扱われるためです。 次は「右辺値」について説明します。

右辺値

右辺値参照を理解する上で重要なポイントがあります。
それは「代入式の右辺にある値が右辺値ではない」ということです。 右辺値参照を難しいと思っている人の多くはここを勘違いしているのではないでしょうか。 かく言う私も最初は間違っていました。
では、右辺値とは何かというと「使用する式を超えて保持されない一時的な値」のことです。
逆に左辺値というものもあり、変数であれば const であってもすべて左辺値です。
リテラルや計算結果で作成される値などは右辺値です。
   int num = 5;         // リテラル 5
   int num2 = num * 2;  // 計算結果 10 (num*2)
代入式の右辺にあるからといって右辺値ではありませんが、 右辺値は左辺になれません。 ただ、const 定義された変数なども左辺になれないので、なれないからといって右辺値ではありません。
   // 3 = num;
   // num * 2 = 7;
右辺値というのは右辺にある必要もなく、次の式でも右辺値です。
   cout << 3 << endl;
   cout << num * 2 << endl;
   cout << string("Hello") << endl;  // string("Hello") はすぐに解放
結局のところ、右辺値は「一時的な値」ということです。
右辺値はコンパイラーが必ず使わなくなると分かっています。 右辺値参照「もう使わなくなるオブジェクトの参照」です。

前節の swap 時の tmp は使わなくなる変数でした。 これはコードの流れ上そうなっているだけで、 tmp は右辺値ではありません。 そのため、もう使わないということをコンパイラーに伝えるために std::move を使っています。

移動コンストラクター、移動代入演算子

std::move で std::string の移動を行いましたが、実際には std::move が移動させているわけではありません。 std::move は変数を右辺値にしているだけであり、右辺値に変換するためのキャストのようなものです。
移動を行うのはオブジェクトの 移動コンストラクター移動代入演算子 です。これは右辺値を渡すことによって呼び出されます。
std::string などの標準ライブラリーにはそれらが予め実装されています。

ここからは移動コンストラクター、移動代入演算子を定義する方法について説明していきます。
ただし、それらを理解するためにはコピーコンストラクターと代入演算子についてしっかりと理解しておく必要があります。 その辺がまだあいまいな場合は先に以下の記事を読んでみてください。

自前で定義

サンプルとして次のようなクラスを考えてみます。
これは文字列をアロケートを何度も書かなくていいように SetName() というメソッドを持っています。

movableclass.cpp:
class Person
{
  char *m_name;         ///< 名前
  unsigned int m_age;   ///< 年齢

 public:
  /// コンストラクター
  Person(const char *name = nullptr, unsigned int age = 0)
      :m_name(0), m_age(age)
  {
    SetName(name);
  }

  /// デストラクター.
  virtual ~Person()
  {
    SetName(nullptr);
  }

      :  

   Person &SetName(const char *name)
   {
     // 前の分は解放
     if (m_name != nullptr)
     {
       delete m_name;
       m_name = nullptr;
     }
 
     // NULL でなければメモリーを確保
     if (name != nullptr) {
       int leng = strlen(name);
       m_name = new char [leng + 1];
       strcpy(m_name, name);
     }
     return *this;
   }

    :
};
アロケートしたメンバーを持っているので、コピーコンストラクターと代入演算子を用意します。
   /// コピーコンストラクター
   Person(const Person &other)
       :m_age(other.m_age)
   {
     SetName(other.m_name);
   }
 
   /// 代入演算子
   Person &operator=(const Person &other)
   {
     // 自身の代入チェック
     if (this != &other) {      
       SetName(other.m_name);
       m_age = other.m_age;
     }
     return *this;
   }

まず、移動コンストラクターの定義から説明します。
&& が右辺値参照です。 右辺値がコンストラクターの引数として渡された場合に移動コンストラクターが呼ばれることになります。
右辺値ですので参照先のオブジェクトはもう使われません。そのため、渡されたオブジェクトは移動させてもよいということになります。
   /// 移動コンストラクター
   Person(Person && other)
       :m_age(other.m_age)
   {
     std::cout << "Move Constructor from " << other << std::endl;
     m_name = other.m_name;
     other.m_name = nullptr;  // 元のは NULL を指すように変更
   }
定義時のポイントは移動元のオブジェクトが指すポインターに nullptr を設定している点です。
移動元はもう使わないとは言っても好きに壊していいわけではありません。 移動元のオブジェクトでもデストラクターは呼ばれます。このクラスはデストラクターで nullptr かチェックして delete します。 nullptr に設定していないと浅いコピーのように多重解放となってしまいます。

なお、 ここでは nullptr かチェックしてますが、nullptr を delete をしてもよい というのは仕様で保証されています。


次に、移動代入演算子の定義です。
自身の代入のチェックをしている以外はコンストラクターと同じです。
   /// 移動代入演算子
   Person &operator=(Person && other)
   {
     std::cout << "Move = from " << other << std::endl;
     // 自身の代入チェック
     if (this != &other) {      
       m_name = other.m_name;
       other.m_name = nullptr;  // 元のは NULL を指すように変更
       m_age = other.m_age;
     }
     return *this;
   }
これらが定義されていることによって、右辺値を渡せば移動が行われるようになります。
   Person peter(std::move(Person("Peter", 21)));
   Person michael;
   michael = Person("Michael", 16);
実行結果:
$ ./a.exe 
 Move Constructor from {"Peter"(21)}
 Move = from {"Michael"(16)}
なお、コンストラクターの std::move はいらないのではないか と思った人用にちょっと補足です。
コンストラクターは結構柔軟に動作します。 どのコンパイラーでも同じかはわかりませんが、 Person peter(Person("Peter", 21)) だと Person peter("Peter", 21) のように自動的に変換され、デフォルトコンストラクターが呼ばれてしまいます。


実行例では右辺値を渡しましたが、通常の変数など右辺値参照ではない値が渡された場合にはどうなるでしょうか?
その場合はオーバーロードで合う方、すなわち通常の参照(左辺値参照)を引数とする方が呼ばれます。 で、これがコピーコンストラクターや代入演算子です。

自動で定義される条件

コピーコンストラクターなどと同じように移動コンストラクター、移動代入演算子は自動で定義されることがあります。
自動で定義される場合の条件は次の 3 つが定義されていない場合です。
  1. 移動コンストラクター、移動代入演算子
  2. コピーコンストラクター、代入演算子
  3. デストラクター
1 番目が定義されているのは、明示的に定義したということですから、自動定義されないのは当然です。
2 番目を定義するのは、わざわざコピーコンストラクターなどを定義しないといけないメンバーがあることを意味します。 そのため、移動コンストラクターも自動では定義されません。

気を付けなければいけないのは 3 番目の「デストラクターが定義されていない」という条件です。 移動した後のオブジェクトもデストラクターは呼ばれるわけで、 そこで独自の定義がされていると矛盾が生じる可能性があります。そのため、自動定義はされないようになっています。
今後は 空のデストラクターをとりあえず書いておく といったような不要なデストラクターは定義しないようにする必要があります。

ちなみに移動コンストラクター、移動代入演算子を定義せず、自動定義もされないとどうなるでしょうか?
これは簡単です。std::move などで右辺値を渡したとしても今まで通りコピーコンストラクター、代入演算子が呼ばれ、コピーされます。

自動定義の内容

自動で定義された移動コンストラクター、移動代入演算子で行う処理ですが、 これはコピーコンストラクターなどと一緒で、個々のメンバーに対して移動コンストラクター(移動代入演算子)を実行します。

先ほどの Person クラスの名前のメンバー(m_name)が std::string であったとします。
class Person
{
   std::string m_name;
   unsigned int m_age;

       :
};
int のような基本型は移動とコピーは同じです。
std::string にはすでに移動コンストラクター、移動代入演算子が実装されています。


よってこのクラスように すべてのメンバー変数が移動コンストラクター、移動代入演算子に対応している場合は自動定義を使う (明示的な定義をしない)方がいいでしょう。

移動を禁止する場合

アロケートしたメンバーを持つ場合、コピーコンストラクター等を定義するのではなく、コピーを禁止する場合があります。
その場合、当然移動も禁止される必要があります。
で、どうするかというと、何もする必要はありません。 コピーを禁止すると移動も禁止されます。

protect.cpp:
 /// コピー禁止クラス
 class NonCopy
 {    
   int m_val;
 
  public:
   NonCopy(const NonCopy &) = delete;
   void operator=(const NonCopy &) = delete;
 
   NonCopy(int val=0) :m_val(val) {}    
 
   int GetVal() const { return m_val; }  
 };
   NonCopy ncpy(1);
 
   // コンパイルエラー
   // NonCopy ncpy2(ncpy);
   // NonCopy ncpy2(NonCopy(2));
   // NonCopy ncpy2(std::move(ncpy));
 
   NonCopy ncpy3(3);
   // コンパイルエラー
   // ncpy3 = ncpy;
   // ncpy3 = NonCopy(4);
   // ncpy3 = std::move(ncpy);
後述する所有権の移動などではコピーは禁止だけど移動は可能という場合があります。
この場合はコピーを禁止しておいて、移動コンストラクター、移動代入演算子を定義します。
 /// 移動だけ OK クラス
 class MoveOnly
 {    
   int m_val;
 
  public:
   MoveOnly(const MoveOnly &) = delete;
   void operator=(const MoveOnly &) = delete;
 
   MoveOnly(int val=0) :m_val(val)
   {
     std::cout << "Default Constructor val = " << val << std::endl;
   }
 
   MoveOnly(MoveOnly && other)
       :m_val(other.m_val)
   {
     std::cout << "Move Constructor from " << other << std::endl;
   }
 
   MoveOnly &operator=(MoveOnly && other)
   {
     std::cout << "Move = from " << other << std::endl;
     if (this != &other) {      
       m_val = other.m_val;
     }
     return *this;
   }
 
   int GetVal() const { return m_val; }  
 };
   MoveOnly moly(1);
     
   // MoveOnly moly2(moly);                  // コンパイルエラー
   MoveOnly moly21(MoveOnly(21));            // デフォルトコンストラクター
   MoveOnly moly22(std::move(MoveOnly(22))); // 移動コンストラクター
 
   MoveOnly moly3(3);
   moly3 = MoveOnly(4);
   moly3 = std::move(moly);
ちなみにコピーは可能だけど移動は禁止というのは今まで通りということです。 移動コンストラクター、移動代入演算子を定義せず、自動定義もなければ、右辺値を渡したとしてもコピーコンストラクターや代入演算子が呼ばれます。

デストラクターが定義されている場合の自動定義の利用

デストラクターは定義しているけど、移動は自動定義されたものでよいこともあると思います。
そういった場合には関数の default の宣言によって自動定義と同じものを使うことができます。

defaultmove.cpp:
 class Baz
 {
   Foo m_foo;    ///< 移動対応済みメンバー
 
  public:
   Baz(int val=0) :m_foo(val) {}
   ~Baz() {}  // ← これがあるので移動は自動定義されない
 
   // コピー定義
   Baz(const Baz &src) = default;
   Baz &operator=(const Baz &src) = default;
   // 移動定義
   Baz(Baz &&src) = default;
   Baz &operator=(Baz &&src) = default;  
 };
サンプルではコピーコンストラクターや代入演算子も default 宣言しています。 これは移動の方だけ default 宣言すると、明示的に禁止なくてもコピーは禁止、移動は OK となってしまうためです。

右辺値を受け取る関数

移動コンストラクターのように右辺値参照を使えば、 右辺値かどうかで処理を分ける関数をつくることができます。

確認に使うクラスですが、先ほどの Person クラスはちょっと大きいので、 簡単なクラスを用意します。 移動を必要とするメンバーは持ってませんが、移動コンストラクターなどが呼ばれた時にメッセージを出すようにしています。

movetest.cpp:
 class Foo
 {
   int m_val;
 
  public:
   Foo(int val=0) :m_val(val) {}
 
   Foo(const Foo &other)
       :m_val(other.m_val)
   {
     std::cout << "Copy Constructor from " << other << std::endl;
   }
 
   Foo(Foo && other)
       :m_val(other.m_val)
   {
     std::cout << "Move Constructor from " << other << std::endl;
   }
 
 
   Foo &operator=(const Foo &other)
   {
     std::cout << "Copy = from " << other << std::endl;
     if (this != &other) {      
       m_val = other.m_val;
     }
     return *this;
   }

    :
};

標準ライブラリーでの活用例

まず、右辺値で処理をわけるメリットを標準ライブラリーで見てみます。

例えば、そこそこ大きなクラスを std::vector に格納する場合を考えてください。
vector への格納はコピーが発生し、それなりのコストになります。 すごく大きなクラスはポインターを格納するべきですが、管理が面倒になるので、そこそこぐらいではやりたくないです。

C++11 では vector などのコンテナークラスに格納する場合、右辺値であれば移動で格納されるようになっています。
   vector<Foo> foos;
   foos.reserve(4);
     
   Foo left(1);
   foos.push_back(left);
   foos.push_back(Foo(2));
コード中で vecotr::reserve() 予め配列を確保しているところがあります。
格納する時は移動なのですが、vector が配列を大きくした際にはコピーになってしまうようで、見やすいように予め確保しています。

実行結果:
 Copy Constructor from Foo{1}
 Move Constructor from Foo{2}
ちなみに std::vector を移動した場合はどうなるでしょうか?
この場合は移動元の vector 自体を使わないため、内部の配列を移動するだけで各メンバーは移動もコピーもしません。 vector のコピーやデフォルトの移動と混乱しないように注意してください。
vector<Foo> foos2 = std::move(foos);

文字列(string)の連結などにもムーブセマンティクスが活用されています。
   string str = string("Hello") + " " + "world" + "!";
上記の場合、 以前は string("Hello") + " " で "Hello " を新しくつくり、 + " world" で "Hello world" を作り、 という感じで、一文字追加するにも全体を作成するので、記述は楽になったがコストはかかる という処理でした。
こちらも C++11 以降では右辺値の場合は全体を作成するのではなく、必要な分を追加するということができるようになりました。

定義

右辺値かどうかで処理を分ける関数を実際に書いていきます。

やり方はコンストラクターと同じです。 同名(オーバーロード)で次の 2 つを引数とする関数を定義します。
  • 左辺値参照(従来の参照)
  • 右辺値参照
 class Bar
 {
   Foo m_foo;    
 
  public:
   void Set(const Foo &obj)
   {
     std::cout << "Bar::Set(left )" << std::endl;
     m_foo = obj;
   }
 
   void Set(Foo &&obj)
   {
     std::cout << "Bar::Set(right)" << std::endl;
     m_foo = std::move(obj);
   }
 
 };
これにより右辺値かどうかで処理を変えることができるようになります。
   Foo obj(1);    
   bar.Set(obj);
   bar.Set(Foo(5));
   bar.Set(9);  // 暗黙的な型変換が起こり Foo(9) が作成される
実行結果:
 Bar::Set(left )
 Copy = from Foo{1}
 Bar::Set(right)
 Move = from Foo{5}
 Bar::Set(right)
 Move = from Foo{9}
ちなみに、右辺値参照の引数によるオーバーロードだけでなく、 呼び出し時のオブジェクトが右辺値かどうかでメンバー関数をオーバーロードすることもできます。
こちらはオブジェクトが const かどうかでメンバー関数を二つ用意したときの拡張という感じです。

落ち穂拾い

右辺値参照とムーブセマンティクスで重要な点は説明し終わりましたが、 あまり使わないけど一応押さえておいた方がいいかなというのも説明しておきます。

ユニバーサル参照と完全転送

前章の右辺値と左辺値用の関数ですが、 右辺値参照の型がテンプレートや auto(自動ジェネリック)である場合はユニーバーサル参照と呼ばれ、 どちらにもなれるようになっています。
 template <typename T>
 void SetBar(Bar &bar, T && val)
 {
   bar.Set(val);
 }
このユニバーサル参照の関数を使ってみます。
   SetBar(bar, obj);
   SetBar(bar, Foo(5));
   SetBar(bar, 9);
SetBar() に渡す値が右辺値だったとしても Bar::Set() に渡す時には関数引数の変数となるため、 左辺値になってしまいます。
(9 を渡した場合は SetBar 内で変換が起きるので、右辺値)

実行結果:
 Bar::Set(left )
 Copy = from Foo{1}
 Bar::Set(left )
 Copy = from Foo{5}
 Bar::Set(right)
 Move = from Foo{9}
これを右辺値のまま渡したいとなると、右辺値で処理を分けられるようにして std::move を使う必要があります。これは、ちょっとめんどくさいです。
しかし、完全転送(std::forward)を使うと簡単に書けるようになります。
template <typename T>
void TransSetBar(Bar &bar, T && val)
{
  bar.Set(std::forward<T>(val));
}
完全転送では左辺値は左辺値、右辺値は右辺値のままになります。
   TransSetBar(bar, obj);
   TransSetBar(bar, Foo(5));
   TransSetBar(bar, 9);
実行結果:
 Bar::Set(left )
 Copy = from Foo{1}
 Bar::Set(right)
 Move = from Foo{5}
 Bar::Set(right)
 Move = from Foo{9}

所有権の移動

C++11 ではスマート(かしこい)ポインターも追加されました。 これはポインターの寿命が無くなった時にポインターが指す先も解放されるというものです。 そのスマートポインターの一つとして unique_ptr というのがあり、これは auto_ptr の後継として用意されています。

unique と付いているように対象をポインターとして指せるのは一つだけで、これをコピーすることはできません。
しかし、移動であれば複数から指されることにはならないため、新しい変数が作成可能となります。
   std::unique_ptr<string> ptr(new string("Hello"));
   cout << *ptr << endl;
     
   auto newptr = std::move(ptr);
   cout << ((ptr == nullptr) ? "(null)" : *ptr) << endl;
   cout << *newptr << endl;
実行結果:
 Hello
 (null)
 Hello
unique_ptr だけでなく、スレッドなど所有権があって複製するとまずいものはいくつかあります。 そういったコピーできないものを移動することを所有権の移動と呼びます。

なお、特に所有権の移動と呼ぶことはありませんが、通常の「移動」も 「アロケートしたオブジェクトの所有権を移動した」とも言えます。

まとめ

最後におさらいを兼ねて重要なポイントをまとめておきます。
  • 右辺値とは右辺の値ではなく、もう使わない値
  • 左辺値参照(今までの参照)の他に右辺値参照を引数とする関数を定義することによって、右辺値を区別して処理できる
  • 右辺値はもう使わない値なので、ヒープ領域のデータをコピーではなく移動ができる(効率的になる)
  • 右辺値でなくても移動させたいものには std::move を使う
  • 移動のためには移動コンストラクター移動代入演算子を定義する
自作クラス作成時には移動の定義(移動コンストラクターや移動代入演算子)の検討する必要があります。 移動の定義の方針をまとめると次のようになります。
  1. すべてのメンバーが基本型か移動対応済みクラスであれば定義しない(default を使用)
    • デストラクターは定義してはいけない
  2. アロケートしたオブジェクトをメンバーとして持つ場合は移動を定義するか、禁止する
    • 移動を禁止する場合はコピー禁止だけで OK




 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
33位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
3位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

04月 | 2017年05月 | 06月
- 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31 - - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。