テンプレート版 noncopyable を使うときは要注意

コピーを禁止するクラスを定義する際に Boost の noncopyable クラスや同様の自作クラスを使うことは多いと思う。
Boost の noncopyable は継承するだけで OK という手軽さがいいんだけど、More C++ Idioms/コピー禁止ミックスイン(Non-copyable Mixin) によると Boost の noncopyable のような単純なクラスでは多重継承した時に空の基底クラスの最適化が行われないらしい。
その代わり、CRTP を使うことでこの最適化を行わせることができるとのこと。


g++ 4.4.3 で確かめてみたところ、確かに非 CRTP 版では最適化が行われておらず、CRTP 版では最適化が行われているようだ。

#include <iostream>

class NonCopyable {
protected:
	NonCopyable() {}
	~NonCopyable() {}
private: 
	NonCopyable(const NonCopyable&);
	NonCopyable& operator=(const NonCopyable&);
};

class A : NonCopyable {};
class B : NonCopyable {};
class C : A, B {};


template <class T>
class CRTPNonCopyable {
protected:
	CRTPNonCopyable() {}
	~CRTPNonCopyable() {}
private: 
	CRTPNonCopyable(const CRTPNonCopyable&);
	T& operator=(const T&);
};

class AA : CRTPNonCopyable<AA> {};
class BB : CRTPNonCopyable<BB> {};
class CC : AA, BB {};

int main()
{
	std::cout << "sizeof(C)==" << sizeof(C) << std::endl;	// sizeof(C)==2
	std::cout << "sizeof(CC)==" << sizeof(CC) << std::endl;	// sizeof(CC)==1

	return 0;
}

と、ここまでは上記サイトに書いてあった通り。


CRTPNonCopyable でテンプレート引数のクラスの代入演算子を定義しているあたり、 Barton-Nackman trick 使ってるのかなーと思いながらふと代入してみると、なんとコンパイルが通る。

class NonCopyable {
protected:
	NonCopyable() {}
	~NonCopyable() {}
private: 
	NonCopyable(const NonCopyable&);
	NonCopyable& operator=(const NonCopyable&);
};

class A : NonCopyable {};


template <class T>
class CRTPNonCopyable {
protected:
	CRTPNonCopyable() {}
	~CRTPNonCopyable() {}
private: 
	CRTPNonCopyable(const CRTPNonCopyable&);
	T& operator=(const T &);
};

class AA : CRTPNonCopyable<AA> {};


int main()
{
	A a1;
	A a2;
//	A a3(a1);	// OK. コンパイルエラー

//	a1 = a2;	// OK. コンパイルエラー

	AA aa1;
	AA aa2;
//	AA aa3(aa1);	// OK. コンパイルエラー

	aa1 = aa2;	// NG. なんとコンパイルが通る

	return 0;
}


gcc の挙動を追ってみたところ、T& CRTPNonCopyable::operator=(const T &) は CRTPNonCopyable の代入演算子とみなされていないようだ。


以下、そこから妄想した処理の流れ。

  1. T& CRTPNonCopyable::operator=(const T &) は定義されているものの、 CRTPNonCopyable 用の代入演算子(CRTPNonCopyable& operator=(const CRTPNonCopyable&)) ではないため、コンパイラがこれを自動生成する。
  2. T& CRTPNonCopyable::operator=(const T &) はテンプレートの実体化によって A& CRTPNonCopyable::operator=(const A &) となるが、これは A の代入演算子 (A& A::operator=(const A&)) ではないため、A の代入演算子も自動生成される。
  3. 自動生成された A& A::operator=(const A &) は自動生成された publicの CRTPNonCopyable& operator=(const CRTPNonCopyable&) を呼び出す。
  4. なんとコンパイルに成功する。


ではどうしたら良いのかというと、きちんと CRTPNonCopyable の代入演算子を private で宣言してやるだけ。

class NonCopyable {
protected:
	NonCopyable() {}
	~NonCopyable() {}
private: 
	NonCopyable(const NonCopyable&);
	NonCopyable& operator=(const NonCopyable&);
};

class A : NonCopyable {};


template <class T>
class CRTPNonCopyable {
protected:
	CRTPNonCopyable() {}
	~CRTPNonCopyable() {}
private: 
	CRTPNonCopyable(const CRTPNonCopyable&);
	CRTPNonCopyable& operator=(const CRTPNonCopyable &);
//	T& operator=(const T &);
};

class AA : CRTPNonCopyable<AA> {};

int main()
{
	A a1;
	A a2;
//	A a3(a1);	// OK. コンパイルエラー

//	a1 = a2;	// OK. コンパイルエラー

	AA aa1;
	AA aa2;
//	AA aa3(aa1);	// OK. コンパイルエラー

//	aa1 = aa2;	// OK. コンパイルエラー

	return 0;
}


もしかすると gcc に特有の問題なのかもしれないけど、そんな変わった解決方法ではないので他のコンパイラでもエラーにはならないはずだと思う。


とりあえず、web の情報は 100% 正確とは限らない*1ので、自分で必ず検証するようにしよう。

*1:特にこのブログは