web-dev-qa-db-fra.com

Comment vérifier si l'argument de modèle est un appelable avec une signature donnée

En gros, ce que je veux obtenir, c’est une vérification à la compilation (avec éventuellement un message d’erreur de Nice) indiquant que le callable enregistré (une fonction, un lambda, un struct avec un opérateur d’appel) possède une signature correcte. Exemple (le contenu du static_assert doit être renseigné):

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

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

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

La plupart des réponses se concentrent essentiellement sur la réponse à la question: pouvez-vous appeler l'objet fonction donné avec les valeurs de ces types. Ce n'est pas la même chose que faire correspondre la signature, car cela permet de nombreuses conversions implicites que vous dites ne pas vouloir. Afin d'obtenir un match plus strict, nous devons faire un tas de TMP. Tout d’abord, cette réponse: Fonction d’appel avec une partie des arguments variadiques montre comment obtenir les types exacts des arguments et le type de retour d’un appelable. Code reproduit ici:

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);
};

Ceci fait, vous pouvez maintenant mettre une série d’assertions statiques dans votre code:

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;
};

Puisque vous passez par valeur, c'est tout ce dont vous avez besoin. Si vous passez par référence, j'ajouterais une assertion statique supplémentaire dans laquelle vous utiliserez l'une des autres réponses; probablement la réponse de songyuanyao. Cela prendrait en charge les cas où, par exemple, le type de base était le même, mais la qualification const allait dans la mauvaise direction.

Vous pouvez bien sûr rendre tout cela générique sur le type Signature, au lieu de faire ce que je fais (en répétant simplement les types dans l'assertion statique). Ce serait plus agréable, mais cela aurait ajouté un TMP encore plus complexe à une réponse déjà non triviale. si vous avez l’impression que vous utiliserez cela avec de nombreux Signatures différents ou que cela change souvent, il vaut probablement la peine d’ajouter ce code également.

Voici un exemple en direct: http://coliru.stacked-crooked.com/a/cee084dce9e8dc09 . En particulier, mon exemple:

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);
}
8
Nir Friedman

Vous pouvez utiliser std :: is_convertible (depuis C++ 11), par exemple.

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

ou

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

VIVRE

9
songyuanyao

En C++ 17, il existe le trait is_invocable<Callable, Args...>, qui fait exactement ce que vous demandez. Son avantage par rapport à is_convertible<std::function<Signature>,...> est qu'il n'est pas nécessaire de spécifier le type de retour. Cela peut sembler excessif, mais récemment, un problème me survenant à l’utiliser, ma fonction d’enveloppement a déduit son type de retour de Callable, mais j’ai passé le modèle lambda comme celui-ci [](auto& x){return 2*x;}, le type de retour a été déduit. en sous-appel. Je ne pouvais pas le convertir en std::function et j'ai finalement utilisé l'implémentation locale de is_invocable pour C++ 14. Je n'arrive pas à trouver le lien d'où je viens, cependant ... le code:

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;
};

et pour votre exemple:

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;
};
4
R2RT

Si vous acceptez de transformer A dans une classe de modèle variadique, vous pouvez utiliser decltype() pour activer Register uniquement si callable est compatible, comme suit

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;
 };

De cette façon, si vous préférez appeler Register() avec une fonction incompatible, vous pouvez obtenir une erreur logicielle et activer une autre fonction Register().

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

Vous pouvez utiliser l'idiome de détection, qui est une forme de sfinae. Je crois que cela fonctionne en 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 {};

Ensuite, vous pouvez écrire une assertion statique dans votre code comme ceci:

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

L'avantage de ceci par rapport aux réponses que je vois ci-dessus est:

  • Cela fonctionne pour tout appelable, pas seulement un lambda
  • theres aucune surcharge d'exécution ou std::function entreprise. std::function peut provoquer une allocation dynamique, par exemple, qui serait sinon inutile.
  • vous pouvez réellement écrire un static_assert contre le test et y placer un joli message d'erreur lisible par l'homme

Tartan Llama a écrit un excellent article sur cette technique, ainsi que sur plusieurs alternatives, à découvrir! https://blog.tartanllama.xyz/detection-idiom/

Si vous devez le faire beaucoup, vous voudrez peut-être consulter la bibliothèque callable_traits.

1
Chris Beck

Dans ce cas, vous pouvez utiliser une bibliothèque très simple Boost.Callable Traits .

Exemple d'utilisation:

#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;
    }
}

Pour obtenir le type de fonction, vous pouvez utiliser boost::callable_traits::function_type_t<decltype(func)>.

Comme vous pouvez le voir dans les fonctions main et register_handler, il est possible de comparer le type expected_function_type avec le type de fonction (boost::callable_traits::function_type_t<FUNCTION>) en utilisant std::is_same_v "fonction" -> https://en.cppreference.com/w/cpp/types/is_same

Si vous voulez utiliser mon exemple, veuillez le compiler avec boost 1.66.0 et c ++ 17 en utilisant, par exemple, gcc 7.1.0. Ici tu peux le faire en ligne :)

0
MateuszGierczak