ミシシッピ川以東のルイジアナ

わたしのブログへようこそ!出てけ

unique_ptrのメンバをnullptrで初期化する必要はない

はじめに

初歩的なミスだが、かなりハマったので念のため残しておく。

環境

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チェックには問題ないし、むしろしないほうがよい。

timsong-cpp.github.io

$ 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 の型情報が必要になるということであろう。

おわりに

今回の問題はインターン先の開発中に遭遇した出来事で、実は原因を教えてくれたのも自分ではなく某先輩である。ここでも改めて感謝申し上げる。

業務内容に直接関わりのある話ではないので大丈夫だとは思うが、もし万に一つでもお叱りを受けた場合には本稿は削除する。

*1:上の内容は nullptr の場合だが、他の引数でも reset() は呼び出される。

*2:文書には "Constructs a unique_ptr object that owns nothing,..." としか書かれていないが、get() が nullptr.get_deleter() と等価になるのは保証されている。