空いているので、短い軽い話をします。
前日の記事は @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
私は使ったことがないですが、volatile
もconst
と同様にしてメンバ関数を分けることが可能です。使ったことがないので憶測ですが、何らかのスマートなリソース管理機構か何かを作るときに、デバイスを触っている(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++標準ライブラリにあるoptional
のoperator*
やvalue()
などはこういうことをしています。関数の戻り値になっているoptional
から値を取り出そうとする、というようなケースを想定しているのではないでしょうか。
代入制御
他にも、左辺値でしかできない操作、例えば代入などを禁止することができます。
struct X
{
X& operator=(int i) & {v = i; return *this;}
X& operator=(int i) && = delete; // 自分が右辺値だった場合、代入不可
private:
int v;
};
あるいは、ムーブして返すこともできない所有リソースへの参照を取得する場合なども、似たようにして上手く制御できそうです。テンポラリなオブジェクトに対して呼び出してはいけない関数に全部同じことをしておけば、間違えて呼んでしまったミスを全てコンパイル時に落とすことができます。
うっかりミスをコンパイル時にエラーとして検出できるようになるのは、常に良いことです。
コンビネータをサポートする
C++17ではoptional
やvariant
が入って、エラー処理がいい感じになっていきそうな感じがしています。ところで、こういったものが入ると、以下のようなコードを書きたくならないでしょうか。
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_)));
}
};
本当に実装するなら変換とかもっとちゃんとしないと駄目ですが。
まとめ
メンバ関数をオブジェクト自身の状態で分けて最強のクラスを作ろう!
関連
-
https://cpprefjp.github.io/lang/cpp11/ref_qualifier_for_this.html
- C++11で追加された右辺値参照版オーバーロードに関するcpprefjpの言語機能紹介。
-
http://d.hatena.ne.jp/yohhoy/20160417/p1
- 右辺値参照版オーバーロードの使い道、メンバ変数のムーブと代入制御の紹介
-
https://qiita.com/kktk-KO/items/7f03dfdf7cc020aa2dc5
- メンバ関数qualifierの紹介と、規格文書中の対応箇所、微妙なケースの事情など
-
https://cpplover.blogspot.com/2009/12/rvalue-reference.html
- メンバ関数へのref-qualifierの紹介。