「Effective Modern C++」 は、名著 「Effective C++」 のC++11/C++14対応版で、こちらもためになる内容が多く何度も読み返しています。
その中で一点、掲載されているサンプルコード(というか解説?)に誤りがあるのではないかという点があります。
「項目3: decltypeを理解する」で decltype(auto) の解説で以下のようなコードが示されます。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
このコードは、もともと引数cの型がContainer&
で左辺値参照であったコードからContainer&&
のユニヴァーサル参照に変更したもので、戻り値もc[i]
だったものをstd::forward<Container>(c)[i]
に変更しており、「項目25の忠告にもあるように、テンプレートの実装にはユニヴァーサル参照をstd::forwardするように変更する必要があります。」と高らかに宣言しています。
ですが、よく見るとstd::forwardしているのはコンテナのc
であり、return しているのはc[i]
です。いくらcを完全転送たところで、返しているのはそのcの配列に含まれる要素の一つなので、これではstd::forwardしていないのと全く同じ事だと思えます。
実際、c
自体は右辺値として転送されずこの関数終了時に削除されてしまうので、
auto&& hoge = authAndAccess(std::vector<int>{1, 2, 3}, 0);
std::cout << hoge << std::endl;
のようなコードはhogeが破棄された領域を参照しており、未定義動作になります。
もちろん、
auto hoge = authAndAccess(std::vector<int>{1, 2, 3}, 0);
のように書けば値がコピーされるので大丈夫ですが、それだとそもそも std::forward する必要が無いですよね。
C++の超有名な本を書く人でもすみずみまでは理解できないほど、C++は難しいという事の証左でしょうか。
じゃあどう書けばいいのか
結局本来やりたかった事は、c
に左辺値が渡された場合はコンテナの要素の参照を返し、右辺値が渡された場合はコンテナの要素をムーブして返したい、という事なのだと思います。上述した例は整数の配列なのでコピーでも問題ありませんが、巨大なオブジェクトだとコピーのオーバーヘッドを避けたいというのがあります。
じゃあこれを実現するためにはどう書けばいいのか、と頭をひねって絞り出したコードはこんな感じになりました。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
if constexpr (std::is_lvalue_reference_v<Container>) {
return c[i];
} else {
return typename Container::value_type(std::move(c[i]));
}
}
std::is_lvalue_reference_vで左辺値が渡されたかどうかをチェックし、左辺値ならばコンテナ要素の参照を返し、右辺値なら新たにオブジェクトをムーブコンストラクタで生成して返す(RVOにより最適化されるので戻り値に直接ムーブコンストラクトされる)、というものです。(if constexpr を使うのでC++17が必要ですが…)
もっといい書き方がありそうです。