yohhoyの日記

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

const値型へのユーザ定義変換演算子と引数lvalue/rvalueオーバーロードの落とし穴

C++11以降では、ユーザ定義変換演算子(user-defined conversion operator)の戻り値型をconst Tよりも非constなTとしたほうが良い。関数オーバーロードで引数型const T&およびT&&を受ける関数*1へ渡す際に、同関数呼び出し時のオーバーロード解決失敗によるコンパイルエラーを引き起こす。

本記事の内容はStack Overflowで見つけた質問と回答に基づく。Howard Hinnant氏の回答によれば、これはC++11/14言語仕様の問題でありCWG defect候補とのこと。

下記コードではstd::vector<B>::push_back呼び出しにて型Aから型Bへの暗黙型変換を意図しているが、C++標準コンテナのpush_backメンバ関数ではconst B&B&&オーバーロードを提供するため、C++11/14標準規格に厳密準拠したコンパイラとライブラリでのみ問題が生じる。なお、rvalue参照型が存在しなかったC++03標準規格では問題とならない。

#include <vector>

struct B {
  int m_;
  B(int v) : m_(v) {}
};

struct A {
  int m_;
  A(int v) : m_(v) {}
  // A から const B への変換演算子
  operator const B() const { return B(m_); }
};

int main()
{
  std::vector<B> v;
  v.push_back( A(42) );  // ??
}

gcc 4.9.1/libstdc++, Clang 3.5.0/libc++(-std=c++98), MSVC11(Visual Studio 2012)では正常にコンパイルできる。Clang 3.5.0/libc++(-std=c++11)では下記の通りコンパイルエラーとなる。これは期待される結果ではないが、C++11言語仕様準拠の振る舞いとなっている。(…らしい。詳細仕様は理解できていない。)

error: no viable conversion from 'A' to 'value_type' (aka 'B')
note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'A' to 'const B &' for 1st argument
note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'A' to 'B &&' for 1st argument

ユーザ定義変換演算子の戻り値型をconst BからBへ変更すると、Clang(libc++) C++11/14モードでも正常にコンパイルできるようになる。

struct A {
  int m_;
  A(int v) : m_(v) {}
  // `A`から`B`への変換演算子
  operator B() const { return B(m_); }
};

関連URL

*1:ムーブセマンティクスが導入されたC++11以降では、関数の引数型 const T& と T&& によるオーバーロード提供は、オブジェクトのコピー/ムーブ回数を最小化する実装パターンとなっている。