はじめに
初歩的なミスだが、かなりハマったので念のため残しておく。
環境
C++14を前提に話を進めるが、恐らくそれ以降のC++でも状況は変わっていないと思われる。
$ g++ --version g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
TL;DR
std::unique_ptr
インスタンスの宣言時、 unique_ptr<T> hoge = nullptr
のように初期化すると代入演算子で reset()
を呼び出すので T
の定義をインクルードしなければならない。
hoge
をメンバに持つクラスの定義ヘッダなど、余計なインクルードをしたくないときには unique_ptr<T> hoge;
だけでも同様に初期化される。
解説
経緯
今回詰まったのは次のような状況だった。
/* Hoge.h */ #ifndef HOGE_H #define HOGE_H #include <iostream> class Hoge { protected: int x = 0; public: Hoge(int x_) { std::cout << "Hoge::Hoge()" << std::endl; x = x_; } void printX() const { std::cout << "Hoge::x = " << x << std::endl; } }; #endif
/* Fuga.h */ #ifndef FUGA_H #define FUGA_H #include <memory> class Hoge; class Fuga { protected: std::unique_ptr<Hoge> hoge = nullptr; public: Fuga(); ~Fuga(); void func(); }; #endif
/* Fuga.cpp */ #include <iostream> #include <memory> #include "Hoge.h" #include "Fuga.h" Fuga::Fuga() { std::cout << "Fuga::Fuga()" << std::endl; } Fuga::~Fuga() {} void Fuga::func() { hoge = std::make_unique<Hoge>(100); hoge->printX(); }
/* main.cpp */ #include "Fuga.h" int main() { Fuga fuga; fuga.func(); return 0; }
Fuga.h
では Hoge
の具体的な実装を知っている必要はないので、 Hoge.h
をインクルードする代わりに Hoge
の前方宣言を行っている。
しかしこれをコンパイルしようとすると、「Hoge
のサイズがわからんが?」とunique_ptr
のデストラクタからお叱りが飛んでくる。
$ g++ -std=c++14 main.cpp Fuga.cpp -o unique In file included from /usr/include/c++/9/memory:80, from Fuga.h:4, from main.cpp:1: /usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Hoge]’: /usr/include/c++/9/bits/unique_ptr.h:292:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Hoge; _Dp = std::default_delete<Hoge>]’ Fuga.h:9:34: required from here /usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Hoge’ 79 | static_assert(sizeof(_Tp)>0, |
もちろん Fuga.h
で #include "Hoge.h"
すれば解決はするが、こんなにバカバカしい話はない。
原因は代入演算子
このエラーは、Fuga.h
における hoge
の宣言を次のように訂正することで解消される。
std::unique_ptr<Hoge> hoge;
コンパイラのエラーを読むと発生箇所がデストラクタになっているので分かりづらいが、実は原因は unique_ptr::~unique_ptr()
でなく unique_ptr::operator=()
側にある。
/* unique_ptr.h */ // 中略 /// Reset the %unique_ptr to empty, invoking the deleter if necessary. unique_ptr& operator=(nullptr_t) noexcept { reset(); return *this; }
ということで、代入時にreset()
が呼ばれるのが原因であった*1。
C++標準化文書にもある通り、普通に constexpr unique_ptr() noexcept
が呼ばれた場合でも中身は nullptr
となる*2。よってわざわざ = nullptr
と書かなくてもnullチェックには問題ないし、むしろしないほうがよい。
$ g++ -std=c++14 main.cpp Fuga.cpp -o unique $ ./unique Fuga::Fuga() Hoge::Hoge() Hoge::x = 100
はい。
デストラクタの定義場所
ところでerror: invalid application of ‘sizeof’ to incomplete type
でググるとデストラクタの宣言をヘッダに、定義をソースに書けという内容がヒットする。
実際これは正しい情報で、上のコードからデストラクタを削除するとやはり同様のコンパイルエラーが発生する。 fuga
が破棄されるとき ~unique_ptr()
が呼ばれるが、~Fuga()
がソース側で定義されていない(デフォルトデストラクタ含む)とヘッダ側で Hoge
の型情報が必要になるということであろう。
おわりに
今回の問題はインターン先の開発中に遭遇した出来事で、実は原因を教えてくれたのも自分ではなく某先輩である。ここでも改めて感謝申し上げる。
業務内容に直接関わりのある話ではないので大丈夫だとは思うが、もし万に一つでもお叱りを受けた場合には本稿は削除する。