LoginSignup
25
21

More than 5 years have passed since last update.

unique_ptr はクラスの正確なサイズを知らないといけない?

Posted at

最近悩んでいること。

ちょっと古いですが、C++11時代におけるクラスの書き方 と言う記事があって、ふむふむなるほど、と思うわけですが、困ってしまうことがあります。

A と言うクラスと、APrivate と言うクラスがあって、Aは外部に公開するクラスで、APrivateは外部に公開したくない、Aのみが使うクラスです。

昔の書き方

a.h
#ifndef __A_H
#define __A_H

class APrivate;

class A {
public:
    A();
    virtual ~A();
    int func();
private:
    APrivate *d;
};

#endif // __A_H
a.cc
#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を使うように書き換えてみる。

a2.h
#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
a2.cc
#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のインスタンスを作ろうとすると、エラーになります。

amain.cc
#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を作ってやればビルドは通りました。

a3.h
#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
a3.cc
#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にすることに意味があるかどうか、と言う所で悩んでいます。

何かもっと簡単にいける方法はないのかなあ・・・。

25
21
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
21