【C++】クラスの前方宣言とunique_ptr

2017-06-15

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クラスを前方宣言することでヘッダ"foo.h"のインクルードを避けることができる:

// bar.h
#pragma once
class Foo; // 前方宣言

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

実体を利用する.cppファイル側でインクルードすることで、問題なくコンパイルできる:

// 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内のFooが前方宣言で実体が不明なので?エラーになるらしい。 エラーを回避するにはコピーコンストラクタを宣言すればよいらしい:

// 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);