Как проверить, является ли аргумент шаблона вызываемым с данной сигнатурой
В принципе, то, что я хочу достичь, это проверка времени компиляции (возможно, с хорошим сообщением об ошибке), который зарегистрировал вызываемый (либо функция, либо лямбда, либо структура с оператором вызова) имеет правильную подпись. Пример (содержимое static_assert
должно быть заполнено):
struct A {
using Signature = void(int, double);
template <typename Callable>
void Register(Callable &&callable) {
static_assert(/* ... */);
callback = callable;
}
std::function<Signature> callback;
};
Ответы
Ответ 1
Большинство ответов сосредоточено на основном ответе на вопрос: можете ли вы назвать данный объект функции со значениями этих типов. Это не то же самое, что сопоставление подписи, так как позволяет много неявных преобразований, которые, как вы говорите, вам не нужны. Чтобы получить более строгое соответствие, мы должны сделать кучу 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;
};
Поскольку вы проходите по значению, это в основном все, что вам нужно. Если вы передаете по ссылке, я бы добавил дополнительный статический аргумент, в котором вы используете один из других ответов; вероятно, songyuanyao ответ. Это позаботится о случаях, когда, например, базовый тип был одним и тем же, но квалификация const была в неправильном направлении.
Конечно, вы можете сделать все это общим по типу Signature
вместо того, чтобы делать то, что я делаю (просто повторяя типы в static assert). Это было бы лучше, но это добавило бы еще более сложный 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);
}
Ответ 2
Вы можете использовать std:: is_convertible (начиная с С++ 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!");
LIVE
Ответ 3
В С++ 17 есть черта is_invocable<Callable, Args...>
, которая делает именно то, что вы просите. Его преимущество перед is_convertible<std::function<Signature>,...>
заключается в том, что вам не нужно указывать тип возвращаемого значения.
Это может показаться излишним, но в последнее время у меня была проблема, которая должна была его использовать, именно моя функция-оболочка вывела свой возвращаемый тип из переданного Callable, но я прошел шаблонную лямбду, подобную этой [](auto& x){return 2*x;}
, поэтому возвращаемый тип был выведен в subcall. Я не смог преобразовать его в std::function
, и в итоге я использовал локальную реализацию is_invocable
для С++ 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;
};
Ответ 4
Если вы принимаете преобразование A
в вариационном классе шаблона, вы можете использовать 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 */ };
Ответ 5
Вы можете использовать идиому обнаружения, которая является формой sfinae. Я считаю, что это работает в С++ 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.