最近悩んでいること。
ちょっと古いですが、C++11時代におけるクラスの書き方 と言う記事があって、ふむふむなるほど、と思うわけですが、困ってしまうことがあります。
A と言うクラスと、APrivate と言うクラスがあって、Aは外部に公開するクラスで、APrivateは外部に公開したくない、Aのみが使うクラスです。
昔の書き方
#ifndef __A_H
#define __A_H
class APrivate;
class A {
public:
A();
virtual ~A();
int func();
private:
APrivate *d;
};
#endif // __A_H
#include "a.h"
class APrivate {
public:
int someData;
};
A::A() : d(new APrivate) {
d->someData = 5;
}
A::~A() {
delete d;
}
int A::func() {
return d->someData;
}
外に見せない内部実装やデータはAPrivateが持ち、Aはそのポインタだけ持ちます。
このやり方の利点は、たとえばAが外部のライブラリに依存していたとして、そのライブラリで定義される型を持つ必要があるときに、AではなくAPrivateのメンバにすることによって、a.hに外部ライブラリのヘッダを書かなくて済むようになります。
(例えば、sys/select.h の fd_set とか)
推奨される書き方
- デストラクタは virtual = default とする
- リソースの解放が必要な場合は、ラップした型を作りメンバにする
ですが、ラップした型を外に見せたくないのです。
そのような場合、昔の書き方でポインタで持っているところを、unique_ptr にすれば良いのではないかと考えました。
unique_ptr にすれば、デストラクタを書かなくてもAが解放されるときに一緒にAPrivateも解放されるはずです。
と、言うわけでunique_ptrを使うように書き換えてみる。
#ifndef __A_H
#define __A_H
#include <memory>
class APrivate;
class A {
public:
A();
virtual ~A() = default;
int func();
private:
std::unique_ptr<APrivate> d;
};
#endif // __A_H
#include "a2.h"
class APrivate {
public:
int someData;
};
A::A() : d(std::unique_ptr<APrivate>(new APrivate)) {
d->someData = 5;
}
int A::func() {
return d->someData;
}
このようにすると、a2.cc自体のコンパイルは成功するけど、いざ別のファイルからa2.hをincludeして、Aのインスタンスを作ろうとすると、エラーになります。
#include "a2.h"
int main(int argc, char *argv[]) {
A a;
return 0;
}
In file included from amain.cc:1:
In file included from ./a2.h:4:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:2424:27: error:
invalid application of 'sizeof' to an incomplete type 'APrivate'
static_assert(sizeof(_Tp) > 0, "default_delete can not delet...
^~~~~~~~~~~
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:2625:13: note:
in instantiation of member function
'std::__1::default_delete<APrivate>::operator()' requested here
__ptr_.second()(__tmp);
^
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/memory:2593:46: note:
in instantiation of member function 'std::__1::unique_ptr<APrivate,
std::__1::default_delete<APrivate> >::reset' requested here
_LIBCPP_INLINE_VISIBILITY ~unique_ptr() {reset();}
^
./a2.h:11:13: note: in instantiation of member function
'std::__1::unique_ptr<APrivate, std::__1::default_delete<APrivate>
>::~unique_ptr' requested here
virtual ~A() = default;
^
./a2.h:6:7: note: forward declaration of 'APrivate'
class APrivate;
^
1 error generated.
どうも、デフォルトのdeleterの中のassertで、sizeof()しようとするけどAPrivateの中身がわからないのでエラーになるようです。
classだって言ってるんだからサイズチェックなんかしないでただdeleteしてくれれば良いのになあ。
とりあえず動くようにする
unique_ptrにはcustom deleterが指定できるそうなので、チェックなしでdeleteするdeleterを作ってやればビルドは通りました。
#ifndef __A_H
#define __A_H
#include <memory>
class APrivate;
class A {
public:
A();
virtual ~A() = default;
int func();
private:
typedef void (*APrivateDeleter)(APrivate *p);
std::unique_ptr<APrivate, APrivateDeleter> d;
};
#endif // __A_H
#include "a3.h"
class APrivate {
public:
int someData;
};
static void deleteAPrivate(APrivate *p) {
delete p;
}
A::A() : d(std::unique_ptr<APrivate, APrivateDeleter>(new APrivate, deleteAPrivate)) {
d->someData = 5;
}
int A::func() {
return d->someData;
}
ビルドは通って、なんとなく動いていそうなのですが、ここまでしてAのデストラクタをdefaultにすることに意味があるかどうか、と言う所で悩んでいます。
何かもっと簡単にいける方法はないのかなあ・・・。