web-dev-qa-db-ja.com

テンプレート引数が特定の署名で呼び出し可能かどうかを確認する方法

基本的に、私が達成したいのは、呼び出し可能な関数(関数、ラムダ、呼び出し演算子を持つ構造体)が正しい署名を持っていることを示すコンパイル時の検証(おそらくNiceエラーメッセージ)です。例(static_assertの内容が入力されます):

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    static_assert(/* ... */);
    callback = callable;
  }

  std::function<Signature> callback;
};
24
pzelasko

ほとんどの回答は、基本的に質問への回答に重点が置かれています。これらのタイプの値を持つ特定の関数オブジェクトをcallできますか。これは、署名を照合することと同じではありません。これにより、不要な多くの暗黙的な変換が可能になります。より厳密な一致を得るためには、一連のTMPを実行する必要があります。まず、この答え: 可変引数の一部を使用して関数を呼び出す は、引数の正確な型と呼び出し可能オブジェクトの戻り型を取得する方法を示しています。ここに再現されたコード:

template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())>
{};

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
{
    using result_type = ReturnType;
    using arg_Tuple = std::Tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

template <typename R, typename ... Args>
struct function_traits<R(&)(Args...)>
{
    using result_type = R;
    using arg_Tuple = std::Tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

これで、一連の静的アサートをコードに追加できます。

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    using ft = function_traits<Callable>;
    static_assert(std::is_same<int,
        std::decay_t<std::Tuple_element_t<0, typename ft::arg_Tuple>>>::value, "");
    static_assert(std::is_same<double,
        std::decay_t<std::Tuple_element_t<1, typename ft::arg_Tuple>>>::value, "");
    static_assert(std::is_same<void,
        std::decay_t<typename ft::result_type>>::value, "");

    callback = callable;
  }

  std::function<Signature> callback;
};

値渡しなので、基本的にこれで十分です。参照渡しする場合は、他の回答の1つを使用する静的アサートを追加します。おそらくsongyuanyaoの答え。これにより、たとえば、基本型は同じであるが、const修飾が誤った方向に進んだ場合に対処できます。

もちろん、これを、Signatureを介してすべてジェネリックにすることもできます。代わりに、静的なアサートで型を繰り返すだけです。これはより良い方法ですが、すでに自明ではない答えにさらに複雑なTMPを追加することになります。これをさまざまなSignaturesで使用する場合、または頻繁に変更される場合は、おそらくそのコードを追加する価値もあります。

これがライブの例です: http://coliru.stacked-crooked.com/a/cee084dce9e8dc09 。特に、私の例:

void foo(int, double) {}
void foo2(double, double) {}

int main()
{
    A a;
    // compiles
    a.Register([] (int, double) {});
    // doesn't
    //a.Register([] (int, double) { return true; });
    // works
    a.Register(foo);
    // doesn't
    //a.Register(foo2);
}
10
Nir Friedman

std :: is_convertible (C++ 11以降)を使用できます。

static_assert(std::is_convertible_v<Callable&&, std::function<Signature>>, "Wrong Signature!");

または

static_assert(std::is_convertible_v<decltype(callable), decltype(callback)>, "Wrong Signature!");

[〜#〜]ライブ[〜#〜]

12
songyuanyao

C++ 17には、トレイト_is_invocable<Callable, Args...>_があります。 _is_convertible<std::function<Signature>,...>_に対する利点は、戻り値の型を指定する必要がないことです。それはやり過ぎのように聞こえるかもしれませんが、最近私はそれを使用しなければならないという問題を抱えていました、私のラッパー関数は渡されたCallableから戻り値の型を推定しましたが、私はこのようなテンプレート化されたラムダを渡しました[](auto& x){return 2*x;}、したがって戻り値の型それはサブコールで推定されました。私はそれを_std::function_に変換できず、C++ 14の_is_invocable_のローカル実装を使用してしまいました。私はそれを得たリンクを見つけることができません...とにかく、コード:

_template <class F, class... Args>
struct is_invocable
{
    template <class U>
    static auto test(U* p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
    template <class U>
    static auto test(...) -> decltype(std::false_type());

    static constexpr bool value = decltype(test<F>(0))::value;
};
_

そしてあなたの例のために:

_struct A {
using Signature = void(int, double);

template <typename Callable>
void Register(Callable &&callable) {
    static_assert(is_invocable<Callable,int,double>::value, "not foo(int,double)");
    callback = callable;
}

std::function<Signature> callback;
};
_
6
R2RT

可変個のテンプレートクラスでAを変換することを受け入れる場合、次のようにRegisterが互換性がある場合にのみ、decltype()を使用してcallableをアクティブ化できます。

_template <typename R, typename ... Args>
struct A
 {
   using Signature = R(Args...);

   template <typename Callable>
   auto Register (Callable && callable)
      -> decltype( callable(std::declval<Args>()...), void() )
    { callback = callable; }

   std::function<Signature> callback;
 };
_

このように、互換性のない関数を使用してRegister()を呼び出すと、ソフトエラーを取得して別のRegister()関数をアクティブ化できます

_void Register (...)
 { /* do something else */ };
_
2
max66

Sfinaeの形式である検出イディオムを使用できます。これはc ++ 11で機能すると思います。

template <typename...>
using void_t = void;

template <typename Callable, typename enable=void>
struct callable_the_way_i_want : std::false_type {};

template <typename Callable>
struct callable_the_way_i_want <Callable, void_t <decltype (std::declval <Callable>()(int {},double {}))>> : std::true_type {};

次に、静的アサートをコードに次のように記述できます。

static_assert (is_callable_the_way_i_want <Callable>::value, "Not callable with required signature!");

上記の回答に対するこの利点は、次のとおりです。

  • ラムダだけでなく、あらゆる呼び出し可能オブジェクトで機能します
  • 実行時のオーバーヘッドやstd::functionビジネスはありません。 std::functionは、動的割り当てを引き起こす可能性があります。たとえば、そうでなければ不要です。
  • あなたは実際にテストに対してstatic_assertを書いて、そこに人間が読める素晴らしいエラーメッセージを置くことができます

Tartan Llamaがこのテクニックといくつかの代替案について素晴らしいブログ投稿を書いています。ぜひチェックしてください! https://blog.tartanllama.xyz/detection-idiom/

これをたくさん行う必要がある場合は、callable_traitsライブラリを確認することをお勧めします。

1
Chris Beck

その場合、非常に単純なライブラリ Boost.Callable Traits を使用できます。

使用例:

_#include <boost/callable_traits.hpp>
#include <iostream>
#include <Tuple>

template<typename F>
void register_handler(F&)
{
    if constexpr (std::is_same_v<boost::callable_traits::function_type_t<F>, void(int&, double)>)
    {
        std::cout << "Register handler with signature void(int&, double)" << std::endl;
    }
    else if constexpr (std::is_same_v<boost::callable_traits::function_type_t<F>, void(int)>)
    {
        std::cout << "Register handler with signature void(int)" << std::endl;
    }
}

void func(int&, double)
{}

auto lambda = [](int) {};

int main()
{
    {
        register_handler(func);
        register_handler(lambda);
    }

    {
        using function_type = boost::callable_traits::function_type_t<decltype(func)>;
        using expected_function_type = void(int&, double);

        std::cout << std::boolalpha << std::is_same_v<expected_function_type, function_type> << std::endl;
    }
}
_

関数のタイプを取得するには、boost::callable_traits::function_type_t<decltype(func)>を使用できます。

mainおよび_register_handler_関数で確認できるように、_expected_function_type_ "関数"->を使用して_boost::callable_traits::function_type_t<FUNCTION>_タイプを関数タイプ(_std::is_same_v_)と比較することができます。 https://en.cppreference.com/w/cpp/types/is_same

私の例を実行したい場合は、gcc 7.1.0などを使用して、ブースト1.66.0およびc ++ 17でコンパイルしてください。 ここ オンラインで実行できます:)

0
MateuszGierczak