🙆‍♀️

std::expected(続々々)

2022/04/14に公開

この記事は以前書いていたstd::expected(続々)の続きです。

前回はexpectedのクラス図を出すところまで進めていました。今回はなんでそのような構造を取っているかを調べていきます。

std::expectedの構造

  • expected_storage_base<T,E>
    実際のT,Eを保存するクラス。デストラクタがtrivialかで振舞いでクラスを特殊化しておりT,Eによってデストラクタの実装を変更している。
  • expected_operations_base<T,E>
    派生クラスで使用できる便利関数を定義する
  • expected_copy_base<T,E>
    T,Eがtrivialなコピーコンストラクタを持つか否かでコピーコンストラクタの実装を変更するクラス。
  • expected_move_base<T,E>
    T,Eがtrivialなムーブを持つか否かでムーブコンストラクタの実装を変更するクラス。
  • expected_copy_assign_base<T,E>
    T,Eがtrivialなコピー代入演算子を持つか否かでコピー代入演算子の実装を変更するクラス。

expected_storage_base<T,E>

expected_storage_baseは"実際のT,Eを保存するクラスです。上の説明でデストラクタがtrivialかで振舞いも変更する"と説明していましたがどういうことを分けて説明します。

template <
    class T,
    class E,
    bool = std::is_trivially_destructible<T>::value,
    bool = std::is_trivially_destructible<E>::value
>
struct expected_storage_base {
// ・・・

// データは以下のように待っている
    union {
        T m_val;
        unexpected<E> m_unexpect;
        char m_no_init;
    };
    bool m_has_val;
}

std::is_trivially_destructibleはtrivialなデストラクタを持っているかの判定を行う関数です。更にexpected_storage_baseは次の特殊化を行っています。

// (1) T,Eはtrivialなデストラクタを持つ
template <T, E, true, true> struct expected_storage_base
// (2) Tはtrivialなデストラクタを持つ、Eはtrivialなデストラクタを持たない
template <T, E, true, false> struct expected_storage_base
// (3) Tはtrivialなデストラクタを持たない、Eはtrivialなデストラクタを持つ
template <T, E, false, true> struct expected_storage_base
// (4) T,Eはtrivialなデストラクタを持たない
template <T, E, false, false> struct expected_storage_base

"(1) T,Eはtrivialなデストラクタを持つ"パターンと" (4) T,Eはtrivialなデストラクタを持たない"パターンのデストラクタを比較します。

// (1) T,Eはtrivialなデストラクタを持つ
template <T, E, true, true> struct expected_storage_base
// ・・・
~expected_storage_base() {
    if (m_has_val) {
        m_val.~T();
    }
    else {
        m_unexpect.~unexpected<E>();
    }
}
// (1) T,Eはtrivialなデストラクタを持つ
template <T, E, true, true> struct expected_storage_base
// ・・・
~expected_storage_base() {}

// (4) T,Eはtrivialなデストラクタを持たない
template <T, E, false, false> struct expected_storage_base
// ・・・
~expected_storage_base() {
    if (m_has_val) {
        m_val.~T();
    }
    else {
        m_unexpect.~unexpected<E>();
    }
}

expected_operations_base<T,E>

このクラスはexpected_storage_base<T,E>を継承しており、派生クラスで使用する便利なメソッドを提供しています。例えば、expected_storage_base<T,E>が持っているTEに対してのコンストラクタやget/setを提供します。

template <class T, class E>
struct expected_operations_base : expected_storage_base<T, E> {
    using expected_storage_base<T, E>::expected_storage_base;

    template <class... Args>
    void construct(Args &&... args) noexcept {
        new (std::addressof(this->m_val)) T(std::forward<Args>(args)...);
        this->m_has_val = true;
    }
    // 引数をT型のコンストラクタに渡す

    template <class Rhs>
    void construct_with(Rhs&& rhs) noexcept {
        new (std::addressof(this->m_val)) T(std::forward<Rhs>(rhs).get());
        this->m_has_val = true;
    }
    // 引数からT型をget()してT型のコンストラクタに渡す

    template <class... Args>
    void construct_error(Args &&... args) noexcept {
        new (std::addressof(this->m_unexpect))
            unexpected<E>(std::forward<Args>(args)...);
        this->m_has_val = false;
    }
    // 引数をE型のコンストラクタに渡す

例えば、以上のようにコンストラクタを定義しています。
また、

bool has_value() const { return this->m_has_val; }

    constexpr T& get()& { return this->m_val; }
    constexpr const T& get() const& { return this->m_val; }
    constexpr T&& get()&& { return std::move(this->m_val); }

    constexpr const T&& get() const&& { return std::move(this->m_val); }

    constexpr unexpected<E>& geterr()& {
        return this->m_unexpect;
    }
    constexpr const unexpected<E>& geterr() const& { return this->m_unexpect; }
    constexpr unexpected<E>&& geterr()&& {
        return std::move(this->m_unexpect);
    }
    constexpr const unexpected<E>&& geterr() const&& {
        return std::move(this->m_unexpect);
    }
    constexpr void destroy_val() {
        get().~T();
    }

のようにget/setも提供されています。すこし難しい書き方になっているのは以下の部分です。
assignというコピー用の関数がTの型によって特殊化されています。

    template <class U = T,
        detail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>
        * = nullptr>
    void assign(const expected_operations_base& rhs) noexcept {
        if (!this->m_has_val && rhs.m_has_val) {
            geterr().~unexpected<E>();
            construct(rhs.get());
        }
        else {
            assign_common(rhs);
        }
    }
    // Tのコピーコンストラクタはnothrow指定されている場合は上の処理

    template <class U = T,
        detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value&&
        std::is_nothrow_move_constructible<U>::value>
        * = nullptr>
    void assign(const expected_operations_base& rhs) noexcept {
        if (!this->m_has_val && rhs.m_has_val) {
            T tmp = rhs.get();
            geterr().~unexpected<E>();
            construct(std::move(tmp));
        }
        else {
            assign_common(rhs);
        }
    }
    // Tのコピーコンストラクタはnothrow指定されていないがmoveコンストラクタは
    // nothrow指定されている
    // moveを使って例外を回避

    template <class U = T,
        detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
        !std::is_nothrow_move_constructible<U>::value>
        * = nullptr>
        void assign(const expected_operations_base& rhs) {
        if (!this->m_has_val && rhs.m_has_val) {
            auto tmp = std::move(geterr());
            geterr().~unexpected<E>();

#ifdef EXPECTED_EXCEPTIONS_ENABLED
            try {
                construct(rhs.get());
            }
            catch (...) {
                geterr() = std::move(tmp);
                throw;
            }
#else
            construct(rhs.get());
#endif
        }
        else {
            assign_common(rhs);
        }
    }    
    // Tのコピーコンストラクタもmoveコンストラクタ双方がnothrow指定されていない
    // 例外処理をちゃんと書く

    // 以下はmoveコンストラクタ用に同じことが続いているため省略する    
};

ここでdetail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>* = nullptr>という部分が意味不明な方をいるため補足します。ここはSFINAEという仕組を使っています。

SFINAE

SFINAEとはテンプレートの置き換えに失敗してもオーバーロード候補から外すだけとする。オーバーロード解決がすべて失敗するまではエラーにしないという仕組みです。

任意の式によるSFINAE - cpprefjp C++日本語リファレンスを参照すると以下のように記載されています。

「SFINAE (Substitution Failure Is Not An Errorの略称、スフィネェと読む)」は、テンプレートの置き換えに失敗した際に、即時にコンパイルエラーとはせず、置き換えに失敗した関数をオーバーロード解決の候補から除外するという言語機能である。
たとえば、関数のシグニチャの一部として「typename T::value_type」が書いてあり、型Tがvalue_typeという型を持っていない場合、その関数がオーバーロード解決から除外される。これによって型が任意の機能を持っているかを、コンパイル時に判定できた。

今回の例で考えていきます。

template <
 class U = T,
 std::enable_if_t<std::is_nothrow_copy_constructible<U>::value, void>::type
        * = nullptr
>
void assign(const expected_operations_base& rhs) noexcept {

std::is_nothrow_copy_constructible<U>::valueUのコピーコンストラクタが例外を投げるか否かでtrue,falseを返す関数ですですので次の2パターンがあり得ます。

template <
 class U = T,
 std::enable_if_t<true, void>::type* = nullptr
>
void assign(const expected_operations_base& rhs) noexcept {

template <
 class U = T,
 std::enable_if_t<false, void>::type* = nullptr
>
void assign(const expected_operations_base& rhs) noexcept {

ここでstd::enable_if_tの実装を見るとtrueの時しか::typeが存在しないためfalseが渡されたときはテンプレート引数の解決ができないことになってしまうため、次のオーバーロードされた関数を次々と見ていき該当する関数を呼ぶようにコンパイルされていきます。

template <bool _Test, class _Ty = void>
struct enable_if {}; // no member "type" when !_Test

template <class _Ty>
struct enable_if<true, _Ty> { // type is _Ty for _Test
    using type = _Ty;
};

ここまで登場する部品について粗方の説明は行いましたので残りは自分で読めるとと思います。ぜひ一度挑戦をお願いします。(私も全部読めてはいないですが。。。)

補足版みたいな記事はその打ち出すかもしれません。

Discussion