C++11 で拡張された機能について その2

C++11 で追加されたモダン言語寄りの機能紹介2回目です。

右辺値参照

C++11では、明示的に右辺値(Rvalues)を参照する記述が追加されました。 右辺値とは、以下の式の右辺に記述されているように名前をもたない一時的な インスタンスを差します。

int       x=0;                // リテラルな値
string    s=string("text");   // コンストラクタで生成されたインスタンス
auto      a=func(b);          // 関数の戻り値

右辺値は、一時的なインスタンスであるため、&宣言子による参照が行えません。 しかし、C++03以前でも const int &x = 0 などとすることで、右辺値の参照を 行うことができます。 const ... &による宣言は左辺値にも使えるため、コードの可読性という意味では、 が落るため、以下のように明示的に右辺値を参照するための宣言を行えるようにし ました。

int&&     x=0;               // リテラルな値
string&&  s=string("text");  // コンストラクタで生成されたインスタンス

なお、 Template や、 auto での宣言の場合、 &&宣言子は、 右辺値参照では なく、ユニバーサル参照といわれる参照定義となります。

ユニバーサル参照は、代入対象が右辺値ならば、右辺値参照、 左辺値(通常の 名前付きインスタンス)であれば、通常の参照(&宣言子と同じ作用)となります。

右辺値参照をC++11で明確に宣言出きるようにした理由は、以下に説明する ムーブセマンティックスを実現するために必要となったためです。

ムーブセマンティックス

以下のような副作用のない大文字化プログラムを作成したとする。

例:1

upper_string(std::string &src) {
  std::string& dest;
  for (auto ch: src) {
      dest.push_back(std::toupper(ch));
  }
  return dest;
}

main() {
  string src="Hello";
  auto new_str = upper_string(src);

  std::cout << *new_str << std::endl;
}

C++ は、関数の戻り値は値わたしとなるので、上記の new_strupper_stringdest 変数の内容をすべてコピーすることになり、コストが 発生する。

このコストを回避するために、dest に動的メモリのポインタを格納するように して、new を使うように対応した場合、呼び出し元での delete を必要と するようになるため、関数設計としてはよくないです。 (戻り値を明示的に把握する必要があるため auto が使えない)

例:2

std::string *upper_string(std::string &src) {
  std::string *dest = new std::string();
  for (auto ch: src) {
      dest->push_back(std::toupper(ch));
  }
  return dest;
}

int main() {
  std::string src="Hello";
  std::string *new_str = upper_string(src);

  std::cout << *new_str << std::endl;
  delete(new_str);
}

しかし、C++11では、以下のように new_str を右辺値参照することに よって、new_strupper_stringdest に格納されたインスタンスの 参照値を持つようにすることが出きます。

例:3

int main() {
  std::string src="Hello";
  auto&& new_str = upper_string(src);

  std::cout << new_str << std::endl;
}

これによって、変数代入によるコピーコストを減らすことができ、また、 new による動的メモリ管理を意識しなくても良いコーディングが行えるように なります。 この右辺値参照によって、一時インスタンスの参照を行うことを、 変数代入で複製をつくるのに対して、参照の対象が移動したととらえて、ムーブと 称します。

ただし、例:3 の記述では、関数の戻り値をすべて auto&& で受けるようになり 只の数値のようなものを返却する関数でも右辺値参照されてしまうことになり、 不便です。 このため、C++11では、明示的にムーブを指定する std::move()が 追加されています。

これを利用すると以下のように返却値に対して、コピーではなく、ムーブを 行うように明示的に定義できるため、呼び元側では、コピーかムーブかを考慮する 必要がなくなります。

例:4

std::string upper_string(std::string &src) {
  std::string dest;
  for (auto ch: src) {
      dest.push_back(std::toupper(ch));
  }
  return std::move(dest);
}

int main() {
  std::string src="Hello";
  auto new_str = upper_string(src);
  std::cout << src << std::endl;
  std::cout << new_str << std::endl;
}

(C++のコンパイラでは、戻り値の最適化(RVO) によって関数の戻り値のコピー 処理を省略することがある。 std::move()はこの最適化と競合することがある ため、RVOを行う場合には戻り値に std::move()を指定した場合には、Warning となることがあります。)

また、ムーブは一時的に使う変数に対して使うことで、コピーを減らすことに 役立ちます。

void SwapEffective(string &a, string &b)
 {
   string tmp = b;
   b = a;
   a = std::move(tmp);
 }

上記の例では、 a = std::move(tmp); でムーブを行うことで コピーコストを減らしています。