にゃははー

はへらー

returnとmoveと() ~ GCCを添えて

Boost.Move 1.56 で追加されたBOOST_MOVE_RET*1*2というマクロがあるのですが、GCC 4.9のC++14モードだけなぜかコンパイルできないと言う事象にぶつかってしまい、そういえばここらへんの暗黙のmoveとかって最後にドタバタしてたなぁと思いつつあんまりしっかり見ていなかったのですが、これを気に調べたのでまとめます。

基本的にコードを書いたりする上では気にする必要のない部分です。

BOOST_MOVE_RETマクロ自体はそれほど使い方が難しいわけではないですが、一応例示しておきます。

#include <boost/move/core.hpp>
struct movable
{
    BOOST_COPYABLE_AND_MOVABLE(movable) // copyもmoveもできる
public:

    movable() { }
    movable(BOOST_RV_REF(movable)) { } // move ctor
    movable(const movable &) { }       // copy ctor
};

movable foo()
{
    movable m;
    return BOOST_MOVE_RET(movable, m);
}

上記コードは、C++03ではヘルパーを介しつつBoost.Moveを使ってmove ctorを呼びます。

一方、C++11以降では単に

movable foo()
{
    movable m;
    return (m);
}

へと置き換えられます。ところで、parenthesisで囲まれた式の型はdecltypeで調べれば解りますが、以下のようになります。

int i;
decltype(i)   // int
decltype((i)) // int &

これは(おそらく)割と知られていると思いますが、これをふまえて、parenthesisで囲まれている先のreturn文はmove ctorが呼ばれるでしょうか。それともcopy ctorが呼ばれるでしょうか。

もしあなたが、答えに窮し以下の様な式でmoveとcopyどちらが行われるか疑問に思った場合、いい線をいってます。

movable foo()
{
    movable m;
    return m;
}

return文に渡された`m`という式の型は`movable &`ではなく`movable`ですが、これは確かにlvalueです。lvalueを渡しているのにmove ctorが呼ばれるでしょうか。

結論から言うとparenthesisで囲っていようがいまいがmove ctorが呼ばれます。そして、意外にもmove ctorが呼ばれる理由にはNRVOが関係してきます。


このreturn文での挙動はn3376 12.8 [class.copy] paragraph 31と32に記載があります。
paragraph 31ではcopy/move ctorを"実際"に呼ばなくていい条件(つまり、どういう時にNRVOを行ってよいか)を列挙していて、paragraph 32では、先の条件を満たした場合にはその式をどう評価するかと言うことが書かれています。

paragraph 31によると、return文では

  • non-volatile automatic object
  • オブジェクトの型とreturn typeが同じcv-unqualified type

の場合、copy/move ctorを省略出来ると書いてあります。

そしてparagraph 32では、paragraph 31の条件を満たしたオブジェクトがlvalueで渡されたとき、copyのために呼ばれるctorは、

  • オブジェクトをrvalueとして扱った上でoverload resolutionする
  • 先のoverload resolutionが失敗、若しくはctorの第一引数がrv-refでなかった場合は、オブジェクトをlvalueとして扱い再度overload resolutionする
  • copy/move ctor呼び出しを"実際に"行わなくてもこの二段階のoverload resolutionは必ず実行される

とあります。

ちなみにこの文言だけではparenthesisで囲まれた場合moveできないと解釈できてしまうのですが、CWG DR1579でこの問題が解消されています。


ということで、return文でmove出来る謎が解決したわけですが、冒頭にも述べた

  • GCC 4.9.0, 4.9.1 (今のところGCC 4.9.2/5も)
  • C++14 mode

の場合、parenthesisで囲まれたオブジェクトを正しくrvalueとして扱わずにdelete指定されたcopy ctorを呼ぼうとしてしまうregressionがあります。

Bug 63437 – [4.9/5 regression][C++14] Parenthesized "movable but not copyable" object doesn't compile in return statement

C++11 modeではコンパイル出来るので今のところあまり大きい問題では無いと思いますが、1.56.0のBOOST_MOVE_RETをC++14 modeのGCC 4.9で使用した場合はこの問題にぶつかってしまいます。
とてもひどい方法ですが、この問題にぶつかってしまった場合には

#define BOOST_MOVE_MSVC_AUTO_MOVE_RETURN_BUG
#include <boost/move/core.hpp>

とすると回避できます。

GCCの修正かBoost 1.57のcloseのどちらか早いタイミングに合わせて修正を投げるつもりでいるので、1.57では改善されるはずです。