はじめに
以前自分が1度だけカンマ演算子のオーバーロードを使ったことがあったのですが
その時の事例を紹介しようと思います。
導入
むかしむかし、まだC++20で人類がconceptの力を手に入れるよりも前のことです。
あるところに、どうしてもテンプレート型制約がしたい、おじいさんとおばあさんがいました。
おじいさんは山括弧でテンプレートをしに
おばあさんはSFINAEで選択をしに行きました。
すると、どんぶらこ~ どんぶらこ~ と カンマ演算子が流れてきたのです。
型制約・メタ関数の実装
Detection Idiomなどで任意のメソッドをもつ型を判定したい際に、
decltypeの中に必要なメソッドをカンマ演算子で並べて記述するといった書き方があります。
template<class T,class = void>
struct is_hogefoo : std::false_type
{};
template<class T>
struct is_hogefoo <T, std::void_t<decltype(
std::declval<T>().hoge(),
std::declval<T>().foo()
)> > :std::true_type
{};
しかし、これだけだと、戻り値の型が何かまでは制約ができていないので
その辺りの条件もつけていきます。
書き方はいろいろありそうですが、例えばこう
template<class T>
struct is_hogefoo <T, std::enable_if_t<std::conjunction_v<
std::is_same<int, decltype(std::declval<T>().hoge())>,
std::is_same<void, decltype(std::declval<T>().foo())>
>>> :std::true_type
{};
片方は戻り値の型は任意にしようと思ったら
以下みたいになってきます。
template<class T>
struct is_hogefoo <T, std::enable_if_t<std::conjunction_v<
std::is_void<std::void_t<decltype(std::declval<T>().hoge())>>,
std::is_same<void, decltype(std::declval<T>().foo())>
>>> :std::true_type
{};
これくらいなら、まだいいのかもですが…
複雑で、よみづらーーーい!!!
書くこと多くて、めんどうくさーーーい!!!!!!
と私は感じました。
もう少し簡潔に書けないか考える
ここで以下みたいな関数を宣言します(評価しないので定義は不要だよ)
template<class T, class U>
auto valid_expr(U&&)->std::enable_if_t<std::is_same_v<T, U>>;
そうすると最初の書き方と近い書き方で簡潔にかける
template<class T>
struct is_hogefoo <T, std::void_t<decltype(
valid_expr<int>(std::declval<T>().hoge()),
std::declval<T>().foo()
)> > : std::true_type
{};
しかし、ひとつ大きな問題があります。
void型の判定には使えない。
valid_expr<void>(std::declval<T>().foo())
上記はうまくいきません。なぜならfoo()
がvoid
の場合は関数の引数に渡せないからです。
かといってvoid
の時だけ素直な書き方にするのも汚くなる
template<class T>
struct is_hogefoo <T, std::void_t<decltype(
valid_expr<int>(std::declval<T>().hoge()),
std::enable_if_t<std::is_void_v<decltype(std::declval<T>().foo())>, void*>{}
)> > : std::true_type
{};
この問題を解決してくれたカンマ演算子のオーバーロード
ここで以下のように void_tester_t
という構造体を導入し、カンマ演算子をオーバーロードしました。(ここも実装は不要)
struct void_tester_t
{
template<class T>
friend void operator, (T&&, void_tester_t);
};
inline constexpr void_tester_t void_tester{};
さらに以下のようなvalid_expr
もオーバーロードします
template<class T>
auto valid_expr(void_tester_t)->std::enable_if_t<std::is_same_v<T, void>>;
何が起きるのか解説
template<class T>
friend void operator, (T&&, void_tester_t);
上記のようなオーバーロードをしたことで
(a(), void_tester)
を呼んだ際に
-
a()
がvoid
以外の場合- カンマ演算子のオーバーロードが呼ばれて、void型が返る
-
a()
がvoid
の場合- void型は引数に渡せないので カンマ演算子のオーバーロードが呼べずに、通常のカンマ演算子として処理され、後方のvoid_tester_t型が返る
このテクニックによりvoid
型かそうでない時かで処理を分岐することができます。
また、このような処理は他の演算子のオーバーロードでは実現できません
(オーバーロードできる演算子で通常void
に対しても動作する演算子は他にないはず?)
valid_expr<void>((std::declval<T>().foo(), void_tester))
今回では上記のような使い方をすることで、foo()
に戻り値があった場合は、void型が返るので関数にvoidは渡せずに失敗。
voidだった場合は、void_tester_tが返るので以下のオーバーロードに合致で成功といった使い方になります。
template<class T>
auto valid_expr(void_tester_t)->std::enable_if_t<std::is_same_v<T, void>>;
これにより最終的に以下のような記述が可能になりました。
voidだけ特殊な書き方になるので、ちょっと気持ち悪さも残るけど、記述の複雑さはだいぶスッキリしました。
template<class T>
struct is_hogefoo <T, std::void_t<decltype(
valid_expr<int>(std::declval<T>().hoge()),
valid_expr<void>((std::declval<T>().foo(), void_tester))
)> > : std::true_type
{};
余談
そもそもdeclvalがぱっとみの複雑さを残している気もしており
関数の文脈とかでの制約で考えると、簡潔さがわかりやすい
template<class T>
auto func(T a) -> decltype(
valid_expr<int>(a.hoge()),
valid_expr<void>((a.foo(), void_tester))
);
まとめ
カンマ演算子のオーバーロード
- カンマ演算子は通常void型も受け取れる数少ない演算子
これを利用してカンマ演算子のオーバーロードをすることで、void型かどうかで評価を分岐できた- このテクニックの面白い使い道があるかは不明
- 基本的にvoidの処理分岐には
std::is_void
で事足りるはず- 今回のような特殊な理由がなければ…
- あんまりカンマ演算子のオーバーロードはよく思われないぞ
型制約
- C++20以上を書こう
- conceptを書こう
template<class T>
concept hogefoo_callable = requires(T a)
{
{a.hoge()}->std::same_as<int>;
{a.foo()}->std::same_as<void>;
};
おじいさん と おばあさん とは何だったのか?
- 自分にもわからない