C++0x 標準ライブラリ完全解説 〜 No.03 std::move_if_noexcept,

該当規格: 20.3.3 forward/move helpers [forward], N3225
http://sites.google.com/site/cpprefjp/reference/utility/move_if_noexcept


C++0x 標準ライブラリ完全解説の三回目です。 今回は解説することも少ないので、さっさと書いてしまうことにします。
前回の記事では、主に初心者を対象として 、 std::move という関数がどのような意味を持つかを説明し、
C++0x で新たに追加された概念である rvalue reference について軽く触れました。
今回 触れる std::move_if_noexcept は、名前から分かるように、 std::move の類似品です。
しかし、 C++0x の標準ライブラリ中でも特に使用頻度の高い std::move と違い、 std::move_if_noexcept は、使う人がたまに思い出して使う程度の、使用する場面が非常に限られた関数です。


ですので、今回の解説記事の内容は、初心者向けだった前回とは打って変わって、上級者向けの非常にマニアックなもので、
端的に言うなら「特に知らなくても問題ない」内容となること、
それゆえに解説も丁寧には行わず、分かるひとだけ分かればいいや、というスタンスで行うことを、予めご了承ください。


では、実際の解説に移りたいと思います。

定義

namespace std
{
  template<class T>
  typename conditional<
    !is_nothrow_move_constructible<T>::value && is_copy_constructible<T>::value,
    const T&, T&&
  >::type move_if_noexcept( T& x ) noexcept
  {
    return std::move(x);
  }
  
}

解説

この関数は、与えられた引数の型 T の move constructor が例外を投げない場合、
または T が copy constructor を持たない場合には、与えられたオブジェクトへの rvalue reference を返し、
それ以外の場合には、与えられたオブジェクトへの const lvalue reference*1 を返します。


平たく言い直すと、 std::move_if_noexcept は、
move する際に例外が投げられる可能性がない場合には、 std::move を呼んで move させ、
例外が投げられる可能性がある場合には std::move を呼ばずに copy させる、
という処理を行います。


と、これだけでは、よく意味が分からないでしょうから、具体的に使用する場面を紹介しましょう。
以下の関数 to_vector は、 C++0x で新たに追加された固定長配列 std::array を、動的配列 std::vector へと変換する関数です:

template<class T, std::size_t N>
std::vector<T> to_vector( std::array<T, N> const& a )
{
  std::vector<T> v;
  v.reserve(N);
  
  // C++0x なので遠慮せず範囲 for 構文を使う
  for( auto const& x : a ) {
    v.push_back( x );
  }
  
  return v;
}


さて、上記のコードには、 std::move_if_noexcept どころか、 std::move すら現れませんでした。
引数である std::array を const reference で受け取っていたので、まあ当然といえます。
そして実際問題としては、現行の to_vector だけでも、実用上は特に問題はないのですが、
せっかく C++0x なので、 rvalue reference を使うことで効率を改善したものも、用意したいところでしょう。
実際に実装してみると、こんな感じになります:

template<class T, std::size_t N>
std::vector<T> to_vector( std::array<T, N> && a )
{
  std::vector<T> v;
  v.reserve(N);
  
  // 各要素を move していく
  for( auto& x : a ) {
    v.push_back( std::move(x) );
  }
  
  return v;
}

ここでは、まだ std::move_if_noexcept は使わず、単純に std::move を用いて実装しています。


さて、上記の rvalue reference 版の to_vector には、非常に些細ではあるのですが、問題点が存在します。
for ループの途中で v.push_back( std::move(x) ); が例外を投げた場合を考えてみましょう。
その場合、 a の中身が中途半端に v に move された状態のまま
to_vector は処理を抜けることになり、その際に作りかけの v は破棄されることになります。
そうなると、 a の中身は、前半は v に move され、後半は元のまま、という中途半端な状態になり、
また、既に v に move されていた a の中身は、完全に失われてしまうことになります。


このことを、少し難しい言葉で言うと、 rvalue reference 版の to_vector
例外安全の強い保証を満たさない関数である、と表現することができます。
ここで、この関数内の std::move を std::move_if_noexcept に置き換えると、

template<class T, std::size_t N>
std::vector<T> to_vector( std::array<T, N> && a )
{
  std::vector<T> v;
  v.reserve(N);
  
  // 各要素を move していく
  for( auto& x : a ) {
    v.push_back( std::move_if_noexcept(x) );  // ただし例外を投げうる場合は copy
  }
  
  return v;
}

この関数は、例外安全の強い保証を満たすようになります。*2
もし x が std::move_if_noexcept によって move されるようなら、 T の move constructor ( std::vector の rvalue reference 版の push_back は、内部で move constructor を呼び出します*3)は例外をそもそも投げないので、この関数はそもそも例外を投げないことになります。*4
T の move constructor が例外を投げうる場合、 std::move_if_noexcept は x を move しないので、 a の中身は保存されたまま v へと copy されます。仮にその処理の途中で例外が投げられたとしても、 a の中身はきちんと残っているため、情報が失われることはないのです。


このように、 std::move_if_noexcept は、複数のオブジェクトを連続して move する処理において、
例外安全の強い保証を得るために使用される関数です。
しかし、そもそも例外安全の強い保証を必要とするケースは、実際の C++ プログラミングでは滅多にありません。
実際問題として、例外安全の強い保証を得ようとすると、上記のコードのように、
本来は move によって効率よく処理を行えていた部分が copy になり、その結果 効率が低下する、
なんてことが普通に起きたりするので、尚更 例外安全の強い保証は必要とされません。


ここで誤解しないでいただきたいのは、別に「例外安全はコストがかかるから、しないほうがいい」ということを言っているのではなく、
確かに例外安全の強い保証*5を得るためには、効率が低下することも多いのですが、
例外安全の基本的な保証*6だけを満たす場合においては、効率は低下せず、むしろ綺麗なコードになる場合のほうが多い、という点です。
ここで例外安全について詳しく述べるつもりはありませんが、そのことだけは覚えておいてください。


さて、ネタが尽きてきたのか、少々脇道にズレてしましました。そろそろまとめに入りましょう。


このように、 std::move_if_noexcept は、普通にコードを書いている上では、まずお世話にならないものです。
ですので、今回の記事も、
「こんなものがあるんだよ」
「へー」
程度の軽いノリで片付けていただければいいんじゃないでしょうか、


と、最後gdgdになってしまいましたが、今回の解説記事は、以上で終りにしたいと思います。

*1:以降、単に const reference と表現します。

*2:厳密には、 T に対する copy が定義されず、かつ move が例外を投げうる場合には、例外安全の強い保証は満たせません。が、そのような場合には、どのようにコードを書いても、例外安全の強い保証を満たすようにすることは出来ないので、そのような場合に対処する必要はありません。

*3:厳密に言うなら、アロケータによっては move constructor は呼ばれないかもしれませんが、そんなケースは普通の C++ コードではまず起きえないです。

*4:厳密には、 v.push_back 自体はメモリ確保失敗の例外を投げる可能性があるのですが、 v.reserve(N) で予め領域を確保してあるので、 v.reserve(N); の処理さえ終わっているなら、 to_vector 内で例外が投げられる可能性はありません。

*5:例外が投げられた場合、オブジェクトは例外が投げられる前の状態に巻き戻される、という保証

*6:例外が投げられても、オブジェクトが不完全な状態にはならず、資源をリークすることもない、という保証