読者です 読者をやめる 読者になる 読者になる

C++17 optionalの実装について

C++17で、optionalが標準ライブラリに入ることが決定した。

そして、C++17 optionalはconstexprに対応している。

以前からその実装について気になっていたので調べたついでに少しまとめる。

optionalとは

無効値を取ることのできる型である。

C++17 optionalはBoost.Optionalの実装を元に設計された。

Boost.Optionalに関しては以下の記事が詳しい。

d.hatena.ne.jp

C++03 Boost.Optionalの実装

C++03時代のBoost.Optionalの実装についても触れておく。

C++03 Boost.Optionalの実装は単純だ。

予めoptionalオブジェクト内にデータを格納するストレージを確保しておき、
適切なタイミングでplacement newをして、初期化する。

また、オブジェクトを破棄する際には、必要があれば明示的デストラクタ呼び出しを行う。

ストレージの確保には、(C++11以降でいうところの)std::aligned_storageのようなものを用いる。

つまり、optionalの内部で領域の動的確保は行われない。

下記のコードはBoost.Optionalの実装のイメージだ(故に正確ではない)。

template <typename T>
class optional {
    //  storage
    typename std::aligned_storage<sizeof(T), alignof(T)>::type storage_;
    bool init_;
    
    //  pointer to storage
    T* ptr() {
        return static_cast<T*>(static_cast<void*>(&storage_));
    }
    
public:
    // default constructor
    optional() : init_(false) {}
    
    // value constructor
    optional(const T& val) : init_(true) {
        //  placement new
        new (&storage_) T(val);
    }
    
    //  destructor
    ~optional() {
        if (init_) {
            // explicit destructor call
            ptr()->~T();
        }
    }
};

見て分かる通り、この実装のoptionalクラスはtrivialデストラクタを持たない。

更には引数付きコンストラクタはconstexprコンストラクタの条件を満たさない。

なので、このままではconstexpr対応の条件であるリテラル型クラスの条件を満たさない。

C++17 optionalはどのような実装により、constexpr対応を果たしたのだろうか。

その前に

上のコードでは、Tの型に関わらずoptional<T>型はデストラクタで明示的デストラクタ呼び出しを行っているが、
実際には、Tがtrivialデストラクタをもつ場合は明示的デストラクタ呼び出しを省略できる。

そのため、Tがtrivialデストラクタを持つときには、optional型もまたtrivially destructibleとなれる。

実装について

さて、ようやくC++17 optionalの実装を確認する。

constexpr対応のoptionalの実装は以下のリポジトリのものを参考にした。

github.com

だが、実際のソースコードは一息に読むには長いので、部分部分に分解して見ていく。

まずは、optional<T>型の定義を確認する。

定義は上記のリポジトリ、optional.hppファイルの350行目付近である。

// 350行目付近
template <class T>
class optional : private OptionalBase<T>
{
   ...
};

optional<T>型はOptionalBase<T>型を継承している。

OptionalBase<T>型はそのすぐ上で定義されている。

// 341行目付近
template <class T>
using OptionalBase = typename std::conditional<
    is_trivially_destructible<T>::value,
    constexpr_optional_base<typename std::remove_const<T>::type>,
    optional_base<typename std::remove_const<T>::type>
>::type;

std::conditional<B, X, Y>::typeは、B == trueの時 X型になり、B == falseの時 Y型となる。

ようは、条件分岐のようなものだ。

この場合の分岐条件は、

is_trivially_destructible<T>::value

つまり、Tがtrivialデストラクタを持つかどうかだ。

Tがtrivialデストラクタを持つ時、OptionalBase<T>は、constexpr_optional_base<T>となり、
Tが非trivialデストラクタを持つ時、OptionalBase<T>は、optional_base<T>となる。

双方の定義を確認する。

// 296行目付近
template <class T>
struct optional_base
{
    bool init_;
    storage_t<T> storage_;

    ...

    ~optional_base() { if (init_) storage_.value_.T::~T(); }
};

// 319行目付近
template <class T>
struct constexpr_optional_base
{
    bool init_;
    constexpr_storage_t<T> storage_;

    ...

    ~constexpr_optional_base() = default;
};

「...」で省略した部分はどちらのクラスもほぼ同じであった。

違いは2点。

1つ目は、storage_ メンバ変数の型、

2つ目は、デストラクタでのメンバの明示的デストラクタ呼び出しの有無だ。

constexpr_optional_baseは、Tがtrivialデストラクタを持つため、明示的デストラクタ呼び出しを省略しているのだろうと予想できる。

では、storage_t と constexpr_storage_t の定義を確認する。

// 266行目付近
template <class T>
union storage_t
{
  unsigned char dummy_;
  T value_;

  ...

  ~storage_t(){}
};

// 281行目付近
template <class T>
union constexpr_storage_t
{
    unsigned char dummy_;
    T value_;

    ...

    ~constexpr_storage_t() = default;
};

このoptionalは、ストレージにstd::aligned_storageのような型を使わず、共用体を使って実現しているようだ。

storage_t と constexpr_storage_t の定義の違いは、デストラクタの定義のみである。

ではこの違いは何故必要なのか。

これには、C++11の「共用体の制限解除」が関係している。

C++11以降では非trivialデストラクタを持つオブジェクトも共用体のメンバにすることが可能になった。

しかし、その時その共用体のデストラクタは暗黙的にdeleteされる。

そのため、非trivialデストラクタを持つT型のメンバ value_ を持つstorage_tには空のデストラクタ定義が必要になる。

さらに、共用体のコンストラクタの初期化子を使って value_ を初期化すれば、T型のコンストラクタが呼び出されるため、
optional<T>のコンストラクタ内での placement new の必要がなくなる。

以上をもって、Tがリテラル型である場合に、constexpr_storage_t<T>、constexpr_optional_base<T>、optional<T>の全てがリテラル型の条件を満たし、コンパイル時のオブジェクト構築が可能となる。

#include <type_traits>
#include <string>
#include <cassert>

template <typename T>
union storage_t {
	char dummy_;
	T value_;

	template <typename... Args>
	constexpr storage_t(Args&&... args) : value_(args...) {}

	~storage_t() {}
};

template <typename T>
union constexpr_storage_t {
	char dummy_;
	T value_;

	template <typename... Args>
	constexpr constexpr_storage_t(Args&&... args) : value_(args...) {}

	~constexpr_storage_t() =default;
};

template <typename T>
struct optional_base {
	bool init_;
	storage_t<T> storage_;
    
	template <typename... Args>
	constexpr optional_base(Args&&... args)
		: init_(true), storage_(args...) {}

	~optional_base() { if (init_) storage_.value_.~T(); }
};

template <typename T>
struct constexpr_optional_base {
	bool init_;
	constexpr_storage_t<T> storage_;

	template <typename... Args>
	constexpr constexpr_optional_base(Args&&... args)
		: init_(true), storage_(args...) {}

	~constexpr_optional_base() =default;
};

template <typename T>
using OptionalBase = std::conditional_t<
	std::is_trivially_destructible<T>::value,
	constexpr_optional_base<T>,
	optional_base<T>
>;

template <typename T>
class optional : OptionalBase<T> {
public:
	constexpr optional(const T& value) : OptionalBase<T>(value) {}

	constexpr const T& operator*() const {
		return OptionalBase<T>::storage_.value_;
	}
};

int main() {
    // constexpr
    constexpr optional<int> a(42);
    static_assert(*a == 42);
    
    // 非constexpr
    /*constexpr*/ optional<std::string> b("nyan");
    assert(*b == "nyan");
}