コンパイラの規格違反を発見するのが、私のささやかな楽しみとなっている。もちろん、VC10では、規格通りの解釈されるコードを探すほうが難しいので、まったくもってやりがいがない。gccには、なかなかそういうことがないので、規格違反を発見したときは、実にいい気分になる。たいてい、数ヶ月に一度ぐらいしか、規格違反は見つけられないのが困りものだが。
このたび、面白いバグを発見した。ひょっとしたら、まだどのコンパイラも正しく実装していないだけなのかもしれないが、バグには違いない。
メンバーテンプレート関数は、クラスの「コピー」のために、インスタンス化されることはない。
struct S { S() = default ; template < typename T > S( T && ) { std::cout << "bad copy constructor" << std::endl ; } template < typename T > S & operator = ( T && ){ std::cout << "bad copy assignment operator" << std::endl ; return *this ; } } ; int main() { S a ; S b = a ; // use trivial copy constructor b = a ; // use trivial copy assignment operator }
このコードは、コピーのために、trivialなコピーコンストラクターとコピー代入演算子を使う。メンバーテンプレート関数はインスタンス化されない。したがって、このコードを実行しても、何も出力されない。
ところが、関数の仮引数がrvalueリファレンスの場合、VC10は、テンプレートなコンストラクターを呼び出してしまった。gccに至っては、コンストラクターに加えて、代入演算子も呼び出してしまっている。これは誤りである。
何故、コンパイラは、こんな単純な間違いを犯したのか。それは、インスタンス化されないのは、あくまで「コピー」に対しての話だからである。ムーブに対しては、問題なくインスタンス化される。
// 上記のクラス定義を使う int main() { S a ; S b = std::move(a) ; // use member template function }
このコードは、テンプレート版の代入演算子をインスタンス化して、ムーブのために使う。その結果、bad copy assignment operatorと出力される。この挙動は正しい。
また、テンプレートパラメータへのrvalueリファレンスを、関数の仮引数にすると、argument deductionで、lvalueリファレンスにもなりうるという仕様がある。
template < typename T > void f( T && ) ; int main() { int object ; f( object ) ; // Tはint & f ( std::move( object ) ) ; // Tはint && }
このため、単にメンバーテンプレート関数であればインスタンス化を禁止するというだけではだめで、コピーかムーブかという違いを、コンパイラがしっかり認識していなければならない。コンパイラの実装には詳しくないが、まあ、ちょっと仕様が複雑だというのは理解できる。
ムーブが言語機能に取り入れられたのは、かなり最近の話だが、それだったら軒並みインスタンス化されなくてもいいものを、何でインスタンス化してしまうのか。まあ、軒並み禁止してしまったら、まともにrvalueリファレンスが使えなくなるので、ムーブを言語に取り入れていなかった規格の問題だったというべきなのかもしれない。
No comments:
Post a Comment