yohhoyの日記

技術的メモをしていきたい日記

ref-qualifierの使い道

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

*1:メンバ関数に対して参照修飾を指定する場合、同メンバ関数への全オーバーロードで参照修飾を明示する必要がある。

*2:左辺値参照修飾オーバーロードのみを定義し、右辺値参照修飾オーバーロードは宣言無しとしても、同様にコンパイルエラーとして検知は可能。本文中では「選択禁止」意図を明確にするため、オーバーロード関数のdeleted指定を行った。