C++11で追加されたメンバ関数への参照修飾(ref-qualifier)の使い道についてメモ。
メンバ変数からのムーブ
メンバ変数へのアクセサ関数を定義する場合、(1)非const参照を返す非constオーバーロード関数と、(2)const参照を返すconstオーバーロード関数を提供するのが一般的である。下記コードのように、関数f()
が返す一時オブジェクト(=rvalue; 右辺値)のアクセサ関数を呼び出す場合も、非constオーバーロード関数(1)が選択されstd::string
はコピーされる。
なお、std::move
関数などで明示的に右辺値へ変換すれば、std::string
をムーブすること自体は実現可能。
struct X { // (1)非const版はメンバ変数への非const参照を返す std::string& get() { return m_; } // (2)const版はメンバ変数へのconst参照を返す const std::string& get() const { return m_; } //.. std::string m_; }; X x; // X型の変数 X f(); // X型オブジェクトを返す関数 std::string s1 = x.get(); // (1)を選択: コピーコンストラクタ std::string s2 = f().get(); // (1)を選択: コピーコンストラクタ // (1)選択後に右辺値参照へキャスト: ムーブコンストラクタ std::string s3 = std::move(f().get());
非constアクセサ関数を左辺値参照修飾(&
)と右辺値参照修飾(&&
)とに分割し、(1a)左辺値版は従来通り、(1b)右辺値版はメンバ変数のムーブ結果を返すようオーバーロードする。また(2')const版は左辺値参照修飾(const &
)を明示する*1。下記コードでは、式f().get()
は(1b)により “関数f()
が返す一時オブジェクト内のメンバ変数” からムーブされた一時オブジェクトとなり、さらに代入先の変数s2
へとムーブされる。
struct X { // (1a)非const/左辺値版はメンバ変数への(非const)左辺値参照を返す std::string& get() & { return m_; } // (1b)非const/右辺値版はメンバ変数からのムーブ結果を返す std::string get() && { return std::move(m_); } // (2')const版はメンバ変数へのconst左辺値参照を返す const std::string& get() const & { return m_; } //.. std::string m_; }; X x; // X型の変数 X f(); // X型オブジェクトを返す関数 std::string s1 = x.get(); // (1a)を選択: コピーコンストラクタ std::string s2 = f().get(); // (1b)を選択: ムーブコンストラクタ
ノート:もう一つ残されたアクセサ関数のオーバーロード、const右辺値参照版(const &&
)は、ムーブセマンティクス目的では使い道がないため提供する意義がない。const rvalue referenceは何に使えばいいのか も参照のこと。
一時オブジェクトへの代入禁止
下記コードにおいて条件式中で比較演算==
を代入演算=
に誤って記述した場合、プログラマの意図に反した処理が行われる。ここでは関数h()
が返す一時オブジェクトに対して、まず(1)代入演算子が呼び出され、続いて(2)ユーザ定義変換が行われるため、コンパイラにとってはwell-definedなコードとなっている。
struct Y { // (1)整数型を取る代入演算子 Y& operator=(int v) { val_ = v; return *this; } // (2)整数型へのユーザ定義変換 operator int() const { return val_; } //... int val_ = 0; }; // Y型オブジェクトを返す関数 Y h() { return {0}; } if (h() = 42) { // BUG: 比較演算==ではなく代入演算として解釈される assert("do not fire!?"); }
ユーザ定義演算子Y::operator=(int)
に対して明示的な左辺値参照修飾(&
)を行い、右辺値参照修飾(&&
)オーバーロードをdeleted指定することで、この種の誤りをコンパイル時に検出可能となる。*2
struct Y { // (1a)整数型を取る代入演算子: 左辺値への代入は許可 Y& operator=(int v) & { val_ = v; return *this; } // (1b)整数型を取る代入演算子: 右辺値への代入は禁止 Y& operator=(int v) && = delete; //... }; if (h() = 42) // NG: 右辺値への代入演算子はdeleted指定のためコンパイルエラー
メモ:このような条件式中での==
/=
誤記は昔から良くあるバグのため、まともなC++コンパイラは警告として報告するだろうが、本テクニックにより厳格にコンパイルエラーとして検出できる。また応用として、一時オブジェクトへの複合代入(operator+=()
など)禁止という使い方もある。
関連URL
- C++ Standard Library Closed Issues List, 941. Ref-qualifiers for assignment operators
- c++ - What's a use case for overloading member functions on reference qualifiers? - Stack Overflow
- c++ - Is there any real use case for function's reference qualifiers? - Stack Overflow
- メンバ関数の左辺値/右辺値修飾
- 参照渡し or 値渡し? - yohhoyの日記