std::expected(続)

2022/04/11に公開

この記事は以前書いていたstd::expectedの続きで、その実装について調べていこうという内容です。

std::expectedはまだ標準ライブラリに登録されていないためSy Brandの実装[1]を参照させていただき実装調べていきたいと思います。

std::expectedについて

std::expectedは正常値または不正値のいずれかを格納する型でありRustをかじったこと[2]がある方であればResultと同じものと考えて頂けれイメージが湧きやすいかと思います。

Sy Brandの実装を使うと成功時はそのままreturn、失敗時はmake_unexpectedを使うことで値を返すことができ、valueerrorで値を取得することができます。値の有無はhas_valueを使います。

サンプルコード

#include <iostream>
#include "include/tl/expected.hpp"

expected <int, std::string> func(int num) {
    if (num < 0) {
        return make_unexpected("失敗した");
    }

    return num;
}

int main() {    
    std::cout << func(0).value()  << std::endl;
    std::cout << func(-1).error() << std::endl;

    return 0;
}

実行結果

0
失敗した

どういう実装になっているか?

いきなり標準に使われるかもしれないSy Brandの実装の本格的な実装を見ても訳が分からなくなってしまうため少しどういう実装になるか考えてみましょう。

まず、成功時の型Tと失敗時の型Eを汎用的に格納でき、そのいずれかの値を取ると考えてみます。

template <class T, class E>
class expected {
    expected(const T& ok) : has(true) {
        ok_ = ok;
    }
    
    expected(const E& err) : has(false) {
        err_ = err;
    }

    bool has;
    union {
        T ok_;
        E err_;
    };
};

こんな感じでしょうか。TEをテンプレート引数にとり、そのいずれかを取りうると考えるとunionboolで値を管理することで対応しました。

実は問題がいくつか。。。。

コンストラクタとデストラクタ

実はコンストラクタとデストラクタは次のように配置構文new(placement new)、明示的なデストラクタ呼び出しが必要でした。

expected(const T& ok) : has(true) {
    new (&ok_) T(ok);// 配置構文new(placement new)
    }

expected(const E& err) : has(false) {
    new (&err_) E(err);// 配置構文new(placement new)
    }

~expected() {
    if (has) {
        ok_.~T(); //明示的なデストラクタ呼び出し
    }
    else {
        err_.~E(); //明示的なデストラクタ呼び出し
    }
}

なぜ、以下のようなことが必要なのでしょうか。実はunionには以下の制限がありました。
共用体の制限解除[3]
共用体のメンバ変数として、クラスオブジェクトを保持できるようになった:

そして新たに以下の仕様になりました。
共用体の新たな仕様[3:1]

  1. 共用体の非静的メンバ変数が非トリビアルな特殊メンバ関数を持っている場合、その共用体の対応する特殊メンバ関数はデフォルトでdelete宣言される
  2. 共用体の非静的メンバ変数として定義されている非トリビアルなコンストラクタおよびデストラクタを持つ型のオブジェクトに対しては、配置new構文でオブジェクトを構築し、明示的にデストラクタを呼び出す必要がある
  3. 共用体には、参照のメンバ変数は保持できない
  4. 共用体は、継承に関連する機能を使用できない

ここで2について掘り下げます[4]。単純にいうとunionは型情報が不明確な状態を取るため、どの型のコンストラクタとデストラクタを呼ぶといいかわからなくなるためです。
※特殊メンバ関数とトリビアルな型は後の補足で説明します。

union UNION {
    string s;
    vector v;
};

UNION u: //stringとvectorのいずれのコンストラクタを動かす?

TとEが同じ型だと

もう一つわかりやすい問題はT,Eが同じ型の場合に同じ引数型が2つあるとなってしまいコンパイルエラーになってしまいます。

template <int, int>
class expected {
 public:
    expected(const int& ok) : has(true) {
        new (&ok_) T(ok);
    }
    
    expected(const int& err) : has(false) {// 2つめのintを引数にとるコンストラクタ×
        new (&err_) E(err);
    }

これは型Eをラップするunexpected<E>を使うことで防ぎます。

template<class T, class E>
class expected {
    expected(T v);
    expected(unexpected<E> e); // 引数がunexpectedになるためTとEが同じでも〇
}

補足

特殊メンバ関数

特殊メンバ関数 定義
デフォルト・コンストラクタ X()
デストラクタ ~X()
コピー・コンストラクタ X(const X&)
コピー代入演算子 X& operator=(const X&)
ムーブ・コンストラクタ X(X&&)
ムーブ代入演算子 X& operator=(X&&)

上記のように暗黙でクラスに定義されるたりdeletedefault指定ができるメソッドを特殊メンバ関数といいます。

トリビアル(trivial)

これを知っているとつよつよエンジニアらしいです[5]。詳しくは注釈のリンク先のほうがわかりやすいので、そちらを参考にしてください。

  • trivialな特殊メンバ関数を持つ
    簡単にいうとデフォルトの特殊メンバ関数かどうかということです。trivialに破棄可能だったらデストラクタを呼ばなくても大丈夫で、trivialにコピー可能であればあればmemcpyなどで単純にコピーできるということです。たぶん、コンパイラの最適化やC言語との互換性の兼ね合いで重要になってくるんだと思います。(自信はないので[5:1]からC++の仕様などを確認してください)

いったんまとめ

Sy Brandの実装をいきなり見てもわけがわからないと思うため、いったん長い前置きを置きました。
次回で実装を見ていきます。(あれば。。。)

std::expected(続々)

脚注
  1. 最も人気のある実装はSy Brandのもので、GitHub上で500以上のスターを持ち、広く利用されています。 ↩︎

  2. Rustを熟知している人はC++使う必要はないのでは? ↩︎

  3. cpprefjp - C++日本語リファレンス 共用体の制限解除(C++11) ↩︎ ↩︎

  4. 1で特殊メンバ関数がdeleteされることにも対策がホントは必要です。 ↩︎

  5. C++初心者に贈る強そうな人からC++のclassに関連する謎な用語を使われたときにみるもの: trivialとか こんなこと知っている前提で話されるときっと困りますよね。 ↩︎ ↩︎

Discussion