c++17 で追加された std::is_invocable
について考えた。
1つ目のテンプレートパラメタが判定対象の型になっている。ある関数について、ある型の引数を渡したときの呼び出しの可否を調べたい場合、確かに関数(と渡す引数)の型さえ分かれば呼び出しの可否は判定できるので、この形には納得がいく。関数の型は decltype
で取ればいい。
#include <iostream>
#include <type_traits>
#include <boost/type_index.hpp>
struct A {};
struct B {};
// no overload
int foo([[maybe_unused]] A const& a, [[maybe_unused]] B const& b) {
return 42;
}
// overload
int ovlFoo(int, [[maybe_unused]] A const& a) {
return 9;
}
float ovlFoo(int, [[maybe_unused]] B const& b) {
return 2.7182;
}
template <class T>
auto ti() {
return boost::typeindex::type_id_with_cvr<T>();
}
template <class T, class U>
void showTypes() {
std::cout << "(" << ti<T>().pretty_name() << ", " << ti<U>().pretty_name() << ")" << std::endl;
}
template <class T, class U>
void test0() {
if constexpr (std::is_invocable_v<decltype(foo), T, U>) {
std::cout << "Can call foo: ";
} else {
std::cout << "Cannot call foo: ";
}
showTypes<T, U>();
}
と思いきや、オーバーロードされている関数について、任意の型で呼び出せるか判定したい場合に、はたと手が止まってしまった。
is_invocable
の1つ目のパラメタに、decltype
で型を取ろうとしても、オーバーロードされた関数の様な複数の型が返ってくるようなものに decltype
を適用するのは不適格とされているので使えない。
私はこの時点で一旦 is_invocable
の使用は諦めて SFINAE で判定すればいいやと思っていた。
しかしある時 c++20 で導入されたコンセプト std::invocable
も同じ様なテンプレート引数を取るようだという事を知ってしまった。
このコンセプトを使う上で、使い方をキチンと分かっておく必要がある気がしたので、ここで漸く真面目に考えたところ、こういう時は関数オブジェクトクラスを使ってたのでは?という単純な事に気がついた。
ラムダが使えるようになってからは出番が激減していたので、その存在が記憶の隅っこに追いやられて居たのか、なかなかコレを思いつかなかった。
最終的に結局はジェネリックラムダを使う方法が良いのかな、という目下の結論に至ったのだが、その過程を記事にする。
基本的な SFINAE
まずは SFINAE の基本形。SFINAE の説明はググれば色々出てくると思う。やり方も様々。 SFINAE の利用法としては至って普通。
// basic sfinae
template <class T, class U>
class CanCallOvlFooSFINAE {
template <class A0, class A1>
static auto check(A0* t, A1* u) -> decltype(ovlFoo(*t, *u), std::true_type{});
template <class, class>
static std::false_type check(...);
public:
static constexpr bool value = decltype(check<T, U>(nullptr, nullptr))::value;
};
template <class T, class U>
void testRawSFINAE() {
if constexpr (CanCallOvlFooSFINAE<T, U>::value) {
std::cout << "Can call ovlFoo[SFINAE]: ";
} else {
std::cout << "Cannot call ovlFoo[SFINAE]: ";
}
showTypes<T, U>();
}
void_t を使った SFINAE
void_t
は展開中にエラーが無ければ void
型になるという性質を利用すれば SFINAE を簡素に記述できる。
// void_t sfinae
template <class, class, class = void>
struct CanCallOvlFooVoidSfinae : std::false_type {
};
template <class T, class U>
struct CanCallOvlFooVoidSfinae<T, U, std::void_t<decltype(ovlFoo(std::declval<T>(), std::declval<U>()))>>
: std::true_type {
};
template <class T, class U>
void testVoidSFINAE() {
if constexpr (CanCallOvlFooVoidSfinae<T, U>::value) {
std::cout << "Can call ovlFoo[void_t]: ";
} else {
std::cout << "Cannot call ovlFoo[void_t]: ";
}
showTypes<T, U>();
}
is_detected を使った SFINAE
C++20 でも experimental/type_traits
ヘッダにしか入ってないので、まだまだ使うのは先になりそうだけど、自前の名前空間に定義しておけば、時代を先取りできる。
namespace detail {
template <class A, template <class...> class Op, class... Args>
struct is_detect_impl : std::false_type {};
template <template <class...> class Op, class... Args>
struct is_detect_impl<std::void_t<Op<Args...>>, Op, Args...> : std::true_type {};
}
template <template <class...> class Op, class... Args>
using my_detected = detail::is_detect_impl<void, Op, Args...>;
// ここまでがライブラリに入る感じになる見込み。my_detected じゃなくて is_detected として
template <class T, class U>
using CanCallOvlFooDetectOp = decltype(ovlFoo(std::declval<T>(), std::declval<U>()));
template <class T, class U>
using CanCallOvlFooDetect = my_detected<CanCallOvlFooDetectOp, T, U>;
template <class T, class U>
void testDetectedSFINAE() {
if constexpr (CanCallOvlFooDetect<T, U>::value) {
std::cout << "Can call ovlFoo[detected]: ";
} else {
std::cout << "Cannot call ovlFoo[detected]: ";
}
showTypes<T, U>();
}
関数オブジェクト型を使う
ここからが本題に入ってくる。std::is_invocable
を使うために、オーバーロード関数の型と同じになるような関数オブジェクト型を定義する。こうすれば、わざわざ SFINAE を使う必要が無かった。
operator()
関数は宣言だけでいい。
余談だが Haskell をかじってから、これをファンクタというのは違和感を感じるようになった。
// function object
struct CanCallOvlFooFO {
template <class T, class U>
auto operator()(T&& tv, U&& uv) -> decltype(ovlFoo(std::forward<T>(tv), std::forward<U>(uv)));
};
template <class T, class U>
void testInvocableFO() {
if constexpr (std::is_invocable_v<CanCallOvlFooFO, T, U>) {
std::cout << "Can call foo[FunctionObject]: ";
} else {
std::cout << "Cannot call foo[FunctionObject]: ";
}
showTypes<T, U>();
}
既存のオーバーロード関数を相手するわけではなく、これから自前で定義するという状況なら、関数の実体込みで operator()
を定義しておけば、std::is_invocable に型をそのまま渡せるので使い勝手は高まるし、次節の generic lambda 云々の話は不要になる。
なお、このままだと関数呼び出しが煩わしいという事であれば、下の様に inline インスタンスを作っておけばいい。
CanCallOvlFooFO()(args...); // 余分なカッコのせいで、見た目がいまいち気に入らない
inline constexpr CanCallOvlFooFO canCallOvlFoo = CanCallOvlFooFO{};
canCallOvlFoo(args...); // 普通の関数と使い勝手が同じ
Generic lambda
関数オブジェクトを使えばオーバーロード関数でも is_invocable
を利用できることがわかった。
ということは、ジェネリックラムダ作って decltype
する、でも良いんじゃないのか?
// generic lambda
template <class T, class U>
void testInvocableLmbd() {
auto constexpr lmbd = [](auto&& t, auto&& u)
-> decltype(ovlFoo(std::forward<decltype(t)>(t), std::forward<decltype(u)>(u))) {};
if constexpr (std::is_invocable_v<decltype(lmbd), T, U>) {
std::cout << "Can call foo[invocable(lambda)]: ";
} else {
std::cout << "Cannot call foo[invocable(lambda)]: ";
}
showTypes<T, U>();
}
ok. これでも is_invocable
はちゃんと判定できた。
ラムダの文法は関数本体の {} が必須なので return
で何を返すかちょっと考え込んだが、decltype
で使うだけなせいか空にしておけば問題なかった。
- 引数が 2 つになっただけでも返り値の型の
decltype()
が面倒くさいのなんのって - variadic templates にしたほうが寧ろ簡潔じゃないか?オーバーロードしてる関数は引数の数が同じとは限らないし
- ここまでやるなら関数名を引数に取るマクロにしておくだけで再利用できるな
- 例外仕様を書かないのが許されるのは小学生までだよねー、きゃはははは
- 例外仕様の
noexcept()
の中身はdecltype()
と同じになるからマクロにしておこう - C++20 だとジェネリックラムダにテンプレートパラメタを書けるようになったから使ってみたい
#if 0
に変えてある。// general generic lambda macro
#define ReturnTypeSnippet(...) noexcept(noexcept(__VA_ARGS__)) -> decltype(__VA_ARGS__)
#if defined(__cpp_generic_lambdas) && __cpp_generic_lambdas >= 201707L
#define MakeEmptyLambdaFromFunction(FUNC) \
[]<class... _a>(_a&&... arg) ReturnTypeSnippet(FUNC(std::forward<_a>(arg)...)) {}
#else
#define MakeEmptyLambdaFromFunction(FUNC) \
[](auto&&... arg) ReturnTypeSnippet(FUNC(std::forward<decltype(arg)>(arg)...)) {}
#endif
template <class T, class U>
void testInvocableLambdaMacro() {
auto constexpr lmbd = MakeEmptyLambdaFromFunction(ovlFoo);
if constexpr (std::is_invocable_v<decltype(lmbd), T, U>) {
std::cout << "Can call foo[invocable(make_lambda)]: ";
} else {
std::cout << "Cannot call foo[invocable(make_lambda)]: ";
}
showTypes<T, U>();
}
これで std::invocable
コンセプトが使われてるテンプレート引数に、オーバーロード関数を渡す時代になっても安心だ(多分)。
実のところまだ std::invocable コンセプトについては、リファレンスすらちゃんと読んでないので、本当に大丈夫かは知らない。
0 件のコメント:
コメントを投稿
スパムフィルタが機能しないようなので、コメント不可にしました。
注: コメントを投稿できるのは、このブログのメンバーだけです。