C++11で、前方参照しているクラスをunique_ptrで保持するクラスの宣言方法がわかっていなかったのでメモ。

生ポインタの場合

適当なFooクラスがあったとして、

// foo.h
#pragma once
class Foo {
public:
  ~Foo();
};
// foo.cpp
#include "foo.h"
#include <iostream>
Foo::~Foo() {
  std::cout << "Foo:dtor" << std::endl;
}

Fooクラスを生ポインタで持つクラスBarがあったとする。 Fooクラスを前方参照して、ヘッダのインクルードを避けることができる:

// bar.h
#pragma once
class Foo;

class Bar {
public:
  Bar();
  ~Bar();
private:
  Foo* foo;
};

.cppファイルでは"foo.h"をインクルードし、問題なくコンパイルできる:

// bar.cpp
#include "bar.h"
#include <iostream>
#include "foo.h"

Bar::Bar() : foo(new Foo()) {
  std::cout << "Bar:ctor" << std::endl;
}

Bar::~Bar() {
  std::cout << "Bar:dtor" << std::endl;
  delete foo;
}

めでたしめでたし。

unique_ptrを使う場合

生ポインタの代わりにstd::unique_ptrを使って、所有権をわかりやすくしようとしてみる:

// bar.h
#pragma once
#include <memory>
class Foo;

class Bar {
public:
  Bar();
  ~Bar();
private:
  std::unique_ptr<Foo> foo;  // <- 生ポインタからunique_ptrに変更
};
// bar.cpp
#include "bar.h"
#include <iostream>
#include "foo.h"

Bar::Bar() : foo(new Foo()) {
  std::cout << "Bar:ctor" << std::endl;
}

Bar::~Bar() {
  std::cout << "Bar:dtor" << std::endl;
  // fooは自動的に解放される
}

自動的に解放されるので、安全だし楽だ!

さて、Barクラスを使用しようと:

// main.cpp
#include "bar.h"
int main() {
  Bar bar = Bar();
  return 0;
}

するとコンパイルエラーが出る:

$ g++ -c main.cpp
main.cpp:4:7: error: call to implicitly-deleted copy constructor of 'Bar'
  Bar bar = Bar();
      ^     ~~~~~
./bar.h:13:24: note: copy constructor of 'Bar' is implicitly deleted because field 'foo' has a deleted copy constructor
  std::unique_ptr<Foo> foo;
                       ^
include/c++/v1/memory:2621:31: note: copy constructor
      is implicitly deleted because 'unique_ptr<Foo, std::__1::default_delete<Foo> >' has a user-declared move constructor
    _LIBCPP_INLINE_VISIBILITY unique_ptr(unique_ptr&& __u) _NOEXCEPT
                              ^
1 error generated.

デストラクタは宣言しているのになぜだ!?と原因がよくわからなかったんだけど、コピーコンストラクタが使われる?がその際にデフォルトが使われる?がBarが前方参照で実体が不明なので?エラーになるらしい。 エラーを回避するにはコピーコンストラクタを宣言すればよいらしい:

// bar.h
  Bar(const Bar&);  // <- コピーコンストラクタを宣言

上記のmain.cppだと、実際にはコピーコンストラクタはオプティマイズで削除されて?起動されないので、実体がなくても動く。

また、最初から

// main.cpp
  Bar bar;

と書いた場合にはコピーコンストラクタを明示しなくても動く。

代入

同様に、インスタンスの代入が呼び出される場合には代入演算子を明示的に宣言、定義してやる必要がある:

// bar.h
  Bar& operator=(const Bar& bar);  // <- またcpp内で適切に実装してやる必要がある

ムーブなら、unique_ptrを使っていればデフォルト実装も使える:

// bar.h
  Bar& operator=(Bar&& rhs);
// bar.cpp
Bar& Bar::operator=(Bar&&) = default;
// main.cpp
  Bar bar2;
  bar2 = std::move(bar);