По сути, я хочу добиться проверки во время компиляции (возможно, с хорошим сообщением об ошибке), что зарегистрированный вызываемый объект (функция, лямбда, структура с оператором вызова) имеет правильную подпись. Пример (содержание static_assert
должны быть заполнены):
struct A {
using Signature = void(int, double);
template <typename Callable>
void Register(Callable &&callable) {
static_assert(/* ... */);
callback = callable;
}
std::function<Signature> callback;
};
Большинство ответов сосредоточены на основном ответе на вопрос: можете ли вы вызов данный объект функции со значениями этих типов. Это не то же самое, что сопоставление подписи, так как позволяет много неявных преобразований, которые, как вы говорите, вам не нужны. Чтобы получить более строгое соответствие, мы должны сделать кучу 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);
}
Ты можешь использовать станд :: шаблона 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!");
В 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;
};
Если вы принимаете, чтобы преобразовать 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 */ };
Вы можете использовать идиому обнаружения, которая является формой 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
против теста и положил там удобное для чтения сообщение об ошибкеТартан Ллама написал отличный пост об этой технике в блоге и несколько альтернатив, проверьте его! https://blog.tartanllama.xyz/detection-idiom/
Если вам нужно сделать это много, то вы можете взглянуть на библиотеку callable_traits.
В этом случае вы можете использовать очень простую библиотеку Boost.Callable Черты.
Пример использования:
#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. Вот Вы можете сделать это онлайн 🙂