highlight

2020年8月6日木曜日

std::is_invocable でオーバーロード関数の呼び出し可否を判定

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 も同じ様なテンプレート引数を取るようだという事を知ってしまった。 このコンセプトを使う上で、使い方をキチンと分かっておく必要がある気がしたので、ここで漸く真面目に考えたところ、こういう時は関数オブジェクトクラスを使ってたのでは?という単純な事に気がついた。 ラムダが使えるようになってからは出番が激減していたので、その存在が記憶の隅っこに追いやられて居たのか、なかなかコレを思いつかなかった。 最終的に結局はジェネリックラムダを使う方法が良いのかな、という目下の結論に至ったのだが、その過程を記事にする。