Boost.Optional Must Go (3)

関連記事:
http://d.hatena.ne.jp/gintenlabo/20100531/1275335373
http://d.hatena.ne.jp/gintenlabo/20100602/1275505739


結論から言います。
以前の記事において、参照に対する Boost.Optional は、生ポインタの代替として使える、と書きましたが、
現状の Boost.Optional の設計では、 boost::optional を生ポインタの代わりに使うのは、様々な点から推奨できません。


というのも、 boost::optional の設計は、基本的に T として参照を取るようには作られていないからです。
例えば等値比較の仕様を見ると、この操作は、 T が参照の場合には、
二つのオブジェクトが同じ参照であるかではなく、参照先のオブジェクトが互いに同じ値を持っているか否かを比較する、というものになっています。
つまり、 boost::optional の比較においては、ポインタの論理は用いられていないということであり、
ポインタの論理で動くことを期待すると、ミスやバグの原因となる、ということです:

#include <boost/optional.hpp>

int main()
{
  int i = 0, j = 0;
  boost::optional<int&> p = i;
  
  assert( p == j ); // p の参照先は i なのに、 true になってしまう
  
}


尤も、これだけならば、まだ「 Boost.Optional の比較は、そういうもんだ」ということで諦めが付きますが、
問題は swap で、これは二つの optional が共に初期化されていた場合、参照するオブジェクトを交換するのではなく、参照先のオブジェクトの中身を交換してしまいます:

#include <boost/optional.hpp>
#include <algorithm>

int main()
{
  using std::swap;  // ADL で呼び出せるよう予め using 宣言
  
  int i = 0, j = 1;
  boost::optional<int&> p = i, q = j;
  
  // ADL 経由で swap を呼び出すと
  swap( p, q );
  assert( p.get_ptr() == &i && q.get_ptr() == &j ); // p, q の保持する参照は変化せず
  assert( i == 1 && j == 0 ); // 参照先のオブジェクトの中身が交換される
  
}


これは、 boost::optional に対して最適化された swap を用いずに、直接 std::swap を呼び出したときの動作とは決定的に異なるものです:

#include <boost/optional.hpp>
#include <algorithm>

int main()
{
  int i = 0, j = 1;
  boost::optional<int&> p = i, q = j;
  
  // 直接 swap を呼び出すと
  std::swap( p, q );
  assert( p.get_ptr() == &j && q.get_ptr() == &i ); // p, q の保持する参照が交換されて
  assert( i == 0 && j == j ); // 参照先のオブジェクトの中身は変わらない
  
}

http://ideone.com/j3KFz
このような動作は明らかに問題でしょう。


何が言いたいかというと、つまり、現状の Boost.Optional の仕様は、
operator= のような特別な場合を除き、 T が参照の場合についてロクに考慮されていない、ということです。
この問題が修正されるまで、 boost::optional を生ポインタの代替として使うのは、避けたほうがいいでしょう。

// なお、現在 Etude C++ Libraries で開発中の C++0x 対応版 optional では、この問題は解決されています。
// C++0x まで待てない人のため、 C++98/03 版の「参照に対する optional 」の実装もあるので、興味が有る方は是非。

追記( 11:30, 3/23 )

boost::optional の比較については、 twitter 上で

という指摘を受けました。


確かにその通りで、

int i = 0, j = 0;
boost::optional<int&> x = i, y = j;
int &ri = i, &rj = j;

assert( ri == rj ); // true
assert(  x == y  ); // true

このような論理で動くのも、それはそれで一貫性のある動作なので、
ポインタの論理とは違うんだよ、ということを認識しておけば、特に問題はありません。


ただ、この場合にも、実は問題があって、

int i = 1; int &ri = i;
boost::optional<int&> x = i;

assert( ri == 1 ); // true
assert(  x == 1 ); // ill-formed

http://ideone.com/N90uR
このように const 参照と比較させようとした場合、コンパイルエラーとなってしまいます。


この点を考えても、 boost::optionalインターフェイス
T が参照の場合について あまり考慮されていない、ということが分かるでしょう。
尤も、この辺りはパッチを書けばすぐにでも修正できる点ですので、
設計自体が狂っていて 破壊的変更なしに問題を修正できない swap の件に比べたら、かなりマシだと思います。