如何用constexpr检查类型是否可调用(含泛型Lambda、任意参数场景)
Alright, let's tackle these two callable type checking requirements step by step—they're a bit tricky, especially with generic lambdas in the mix, so let's break them down clearly.
First, let's look at the is_callable template you mentioned:
template<class F, class... Args> struct is_callable { template<class U> static auto test(U* p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type()); template<class> static auto test(...) -> std::false_type; static constexpr bool value = decltype(test<F>(nullptr))::value; };
This works perfectly for non-template callables, but it has a critical limitation: you have to explicitly specify the argument types Args.... That's useless for generic lambdas—their operator() is a template with no fixed parameter types, so we can't predefine args to test with.
To build a constexpr tool that detects any callable type (including generic lambdas, function pointers, member function pointers, and function objects) without needing specific args, we can use SFINAE to check for the presence of a callable operator() (even template ones) or match common callable type signatures directly. Here's a C++17-compatible implementation:
#include <type_traits> // Base case: assume not callable template<typename F, typename = void> struct is_callable : std::false_type {}; // Specializations for function pointers, plain functions, member functions template<typename Ret, typename... Args> struct is_callable<Ret(Args...)> : std::true_type {}; template<typename Ret, typename... Args> struct is_callable<Ret(*)(Args...)> : std::true_type {}; template<typename Ret, typename Class, typename... Args> struct is_callable<Ret(Class::*)(Args...)> : std::true_type {}; template<typename Ret, typename Class, typename... Args> struct is_callable<Ret(Class::*)(Args...) const> : std::true_type {}; // Specialization for types with an operator() (including generic lambdas) template<typename F> struct is_callable<F, std::void_t<decltype(&F::operator())>> : std::true_type {}; // Helper variable for cleaner usage template<typename F> constexpr bool is_callable_v = is_callable<F>::value;
The magic here is:
std::void_tlets us use SFINAE to check if&F::operator()is a valid expression (even for generic lambdas, whose templatedoperator()still produces a valid pointer-to-member-template)- We cover all common callable type signatures to avoid missing edge cases
Test it with a generic lambda to confirm:
int main() { auto generic_lambda = [](auto&&...) {}; static_assert(is_callable_v<decltype(generic_lambda)>); // Compiles successfully static_assert(is_callable_v<int(*)(int)>); // Compiles successfully static_assert(!is_callable_v<int>); // Compiles successfully }
This is a stricter requirement: we need to verify that T can be called with any number and type of arguments. Examples of such types include:
- Generic lambdas with
auto&&...parameters - Custom function objects with a templated
operator()accepting any args std::function<void(...)>(C++11's variadic function wrapper)
C++20 Solution (Clean & Precise)
C++20 Concepts make this trivial to express clearly:
#include <concepts> template<typename T> concept callable_with_any_args = requires(T t, auto&&... args) { t(std::forward<decltype(args)>(args)...); };
Usage example:
auto any_args_lambda = [](auto&&...) {}; static_assert(callable_with_any_args<decltype(any_args_lambda)>); // Passes std::function<void(...)> func; static_assert(callable_with_any_args<decltype(func)>); // Passes auto fixed_args_lambda = [](int) {}; static_assert(!callable_with_any_args<decltype(fixed_args_lambda)>); // Passes
C++17 & Earlier Solution
If you can't use C++20, we can approximate this by checking if T can be called with increasing numbers of distinct argument types. It's not 100% theoretically perfect (there could be types that only accept up to N args), but it works for real-world scenarios:
#include <type_traits> // Base implementation template<typename T, typename... Args> struct is_callable_with_any_args_impl : std::false_type {}; // Recursive case: if T accepts Args..., check with more args template<typename T, typename Arg1, typename... Args> struct is_callable_with_any_args_impl<T, Arg1, Args...> : std::conditional_t< std::is_invocable_v<T, Arg1, Args...>, is_callable_with_any_args_impl<T, Args..., int>, // Add a new unique type std::false_type > {}; // Termination case: check if T accepts 0 args template<typename T> struct is_callable_with_any_args_impl<T> : std::is_invocable_v<T> {}; // Helper variable: check 0, 1, 2, 3, 4 args (adjust as needed) template<typename T> constexpr bool is_callable_with_any_args_v = is_callable_with_any_args_impl<T>::value && is_callable_with_any_args_impl<T, int>::value && is_callable_with_any_args_impl<T, int, double>::value && is_callable_with_any_args_impl<T, int, double, char>::value && is_callable_with_any_args_impl<T, int, double, char, void*>::value;
Test it the same way:
auto any_args_lambda = [](auto&&...) {}; static_assert(is_callable_with_any_args_v<decltype(any_args_lambda)>); // Passes auto fixed_args_lambda = [](int) {}; static_assert(!is_callable_with_any_args_v<decltype(fixed_args_lambda)>); // Passes
内容的提问来源于stack exchange,提问作者Andreas Loanjoe

