LoginSignup
6

More than 3 years have passed since last update.

posted at

updated at

メンバ関数をもっと分ける

空いているので、短い軽い話をします。

前日の記事は @Nabetani さんによる「C++ ライフをよりハッピーにするための 14の小ネタ」
翌日の記事は @nomunomu0504 さんによる「CとC++が混在したプログラムでの注意点」です。

const, nonconst

こちらはよく知られていると思いますが、オブジェクトがconstかそうでないかによって呼ばれる時のメンバ関数を分けることができます。その場合、以下のようにメンバ関数の後ろにconstを付けます。

struct X
{
    int& value()       noexcept {return v;}
    int  value() const noexcept {return v;}
  private:
    int v;
};

X x = make_x(42);
x.value(); // int& X::value() noexcept が呼ばれる

const X x = make_x(42);
x.value(); // int X::value() const noexcept が呼ばれる

constなオブジェクトには変更操作が出来ませんが、変更操作を伴わないメンバ関数にconstをつけておけば、constなオブジェクトのそのメンバ関数を呼ぶことが可能です。

constメンバ関数で直接、mutableでない値を変更しようとすると、エラーが出ます(当然)。また、constメンバ関数でmutableな参照を返そうとすると、discard qualifierみたいなエラーが出ます。constメンバ関数からメンバの値を取ろうとするときは、中身を変更できないような返し方(コピーとかconst&とか)しかできません。

値が変更されるかどうかを明示することは、可読性やバグ回避の点から常によいことです。

volatile, nonvolatile

私は使ったことがないですが、volatileconstと同様にしてメンバ関数を分けることが可能です。使ったことがないので憶測ですが、何らかのスマートなリソース管理機構か何かを作るときに、デバイスを触っている(volatileになっている)ときはちゃんと毎回見に行くけれど、そうでないときはデータをキャッシュしておく、というような抽象化ができたりするかもしれません。知らんけど。

lvalue, rvalue reference

さて、本題ですが、左辺値と右辺値でもメンバ関数を分けることが可能です。

右辺値のメンバアクセスでメンバをムーブして返す

struct X
{
    std::vector<int>&       values() &      noexcept {return v;}
    std::vector<int> const& values() const& noexcept {return v;}
    std::vector<int>&&      values() &&     noexcept {return std::move(v);}
  private:
    std::vector<int> v;
};

std::vector<int> v = make_x(/*...*/).values(); // move 版が呼ばれる

上記の例では、オブジェクトが右辺値で呼ばれた時は中身をムーブして返すようになっています。

これは、例えば以下のように明示的にstd::moveを呼べば

struct X
{
    std::vector<int>&       values()       noexcept {return v;}
    std::vector<int> const& values() const noexcept {return v;}
  private:
    std::vector<int> v;
};

auto v = std::move(make_x().values()); // 左辺値参照を取得して、ムーブする

ちゃんとムーブできるわけですが、あまりにも面倒です。右辺値のメンバアクセスをしているのにまたstd::moveを書くのは面倒ですよね? それを自動で解決するためにこの機能が使えます。

例えば、GNUのC++標準ライブラリにあるoptionaloperator*value()などはこういうことをしています。関数の戻り値になっているoptionalから値を取り出そうとする、というようなケースを想定しているのではないでしょうか。

代入制御

他にも、左辺値でしかできない操作、例えば代入などを禁止することができます。

struct X
{
    X& operator=(int i) & {v = i; return *this;}
    X& operator=(int i) && = delete; // 自分が右辺値だった場合、代入不可
  private:
    int v;
};

あるいは、ムーブして返すこともできない所有リソースへの参照を取得する場合なども、似たようにして上手く制御できそうです。テンポラリなオブジェクトに対して呼び出してはいけない関数に全部同じことをしておけば、間違えて呼んでしまったミスを全てコンパイル時に落とすことができます。

うっかりミスをコンパイル時にエラーとして検出できるようになるのは、常に良いことです。

コンビネータをサポートする

C++17ではoptionalvariantが入って、エラー処理がいい感じになっていきそうな感じがしています。ところで、こういったものが入ると、以下のようなコードを書きたくならないでしょうか。

foo::result<std::int32_t, std::exception_ptr> read_id();
std::map<std::int32_t, std::string> username;

auto value = read_id().and_then([&](std::int32_t id)
     -> foo::result<std::string, std::exception_ptr> {
        if(username.count(id) == 0) {
            return foo::err(std::make_exception_ptr(std::out_of_range("user ID not found")));
        }
        return foo::ok(username[id]);
    });

これは、失敗するかもしれない関数(read_id)を使ってIDを読み込み、それが成功したら(and_then)テーブル(username)を見て、IDがなければエラーメッセージ(out_of_range)を、あればユーザー名(username[id])を、そもそも読み込みに失敗していたらその時点でのエラーメッセージ(read_idの返り値のエラーの方)を返す、というものです。

こういうことをするために、std::variantを使って自前のresult型を作ったとします。これは、成功時の型か失敗時の型かのどちらかを持つクラスなので、std::variantをラップして便利関数をいくつか生やすと作れます。

この関数でオブジェクトが右辺値の時にはand_thenで受け取った関数オブジェクトに中身をムーブして渡す、というような制御も、ref-qualifierを使えば実装可能です。

template<typename T, typename E>
struct result
{
    template<typename F>
    std::invoke_result_t<F, T&&> and_then(F&& f) &&
    {
        if(this->is_err())
        {
            return std::move(std::get<E>(this->value_));
        }
        return f(std::move(std::get<T>(this->value_)));
    }
};

本当に実装するなら変換とかもっとちゃんとしないと駄目ですが。

まとめ

メンバ関数をオブジェクト自身の状態で分けて最強のクラスを作ろう!

関連

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6