Как проверить, может ли аргумент шаблона вызываться с заданной подписью

По сути, я хочу добиться проверки во время компиляции (возможно, с хорошим сообщением об ошибке), что зарегистрированный вызываемый объект (функция, лямбда, структура с оператором вызова) имеет правильную подпись. Пример (содержание static_assert должны быть заполнены):

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

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

  std::function<Signature> callback;
};

8 ответов

Решение

Большинство ответов сосредоточены на основном ответе на вопрос: можете ли вы вызвать данный объект функции со значениями этих типов. Это не то же самое, что сопоставление подписи, так как позволяет много неявных преобразований, которые, как вы говорите, вам не нужны. Чтобы получить более строгое соответствие, мы должны сделать кучу 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;
};

Поскольку вы передаете по значению, это в основном все, что вам нужно. Если вы передаете по ссылке, я бы добавил дополнительное статическое утверждение, где вы используете один из других ответов; вероятно, ответ Сунюаняо. Это позаботится о случаях, когда, например, базовый тип был одинаковым, но квалификация const пошла в неправильном направлении.

Конечно, вы можете сделать все это общим для типа Signatureвместо того, чтобы делать то, что я делаю (просто повторяя типы в статическом утверждении). Это было бы лучше, но это добавило бы еще более сложный TMP к уже нетривиальному ответу; если вы чувствуете, что будете использовать это со многими различными SignatureЕсли он часто меняется, возможно, стоит добавить и этот код.

Вот живой пример: 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);
}

В C++17 есть черта is_invocable<Callable, Args...>, который делает именно то, что вы просите. Его преимущество перед is_convertible<std::function<Signature>,...> в том, что вам не нужно указывать тип возвращаемого значения. Это может звучать как излишнее, но недавно у меня возникла проблема, которая должна была его использовать, именно моя функция-обертка выводила тип возвращаемого значения из переданного Callable, но я передал шаблонную лямбду, как этот [](auto& x){return 2*x;}, так что возвращаемый тип был выведен в subcall. Я не мог преобразовать это в std::function и я в конечном итоге с использованием локальной реализации is_invocable для C++14. Я не могу найти ссылку, откуда я его взял... В любом случае, код:

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

Вы можете использовать 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!");

ЖИТЬ

Если вы принимаете, чтобы преобразовать A в классе шаблона Variadic, вы можете использовать decltype(), чтобы активировать Register только если 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 */ };

Это как-то еще одна версия ответа @R2RT, когда вы можете использовать C++17. Мы можем использовать черту is_invocable_r для выполнения работы:

      struct Registry {
  std::function<void(int, double)> callback;

  template <typename Callable, 
      std::enable_if_t<
          std::is_invocable_r_v<void, Callable, int, double>>* = nullptr>
  void Register(Callable callable) {
    callback = callable;
  }
};

int main() {
  Registry r;
  r.Register([](int a, double b) { std::cout << a + b << std::endl; });
  r.callback(35, 3.5);
}

который распечатывает 38.5

Хорошая часть std::is_invocable_r заключается в том, что он позволяет вам контролировать тип возвращаемого значения вместе с типами аргументов, в то время как std::is_invocable есть только для вызываемых типов аргументов.

Вы можете использовать идиому обнаружения, которая является формой 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!");

Преимущество этого перед ответами, которые я вижу выше:

  • Это работает для любого вызываемого, а не только лямбда
  • Theres нет времени выполнения или std::function бизнес. std::function например, может вызвать динамическое распределение, которое в противном случае было бы ненужным.
  • вы можете написать static_assert против теста и положил там удобное для чтения сообщение об ошибке

Тартан Ллама написал отличный пост об этой технике в блоге и несколько альтернатив, проверьте его! https://blog.tartanllama.xyz/detection-idiom/

Если вам нужно сделать это много, то вы можете взглянуть на библиотеку callable_traits.

На основе ответа @Nir Friedman.

      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 signature = ::std::tuple<ReturnType, ClassType, Args...>;
};

template<typename ReturnType, typename ... Args>
struct function_traits<ReturnType(&)(Args...)> {
    using signature = ::std::tuple<ReturnType, void, Args...>;
};

template<typename T> using func_sig = typename function_traits<T>::signature;

template<typename Callable1, typename Callable2>
struct has_same_signature {
    static constexpr bool value = ::std::is_same<func_sig<Callable1>, func_sig<Callable2>>::value;
};

Затем вы можете выполнить строгую проверку, например

      int f(int) {
}

int g(double) {
}

template<typename T>
void some_func(T&& a) {
    static_assert(has_same_signature<T, int(&)(int)>::value)
}

int main() {
    some_func(f); // success
    some_func(g); // fail
}

В этом случае вы можете использовать очень простую библиотеку 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

Если вы хотите запустить мой пример, скомпилируйте его с boost 1.66.0 и C++17, используя, например, gcc 7.1.0. Здесь вы можете сделать это онлайн:)

Другие вопросы по тегам