I'm currently designing some functions that make use of SFINAE to control overload resolution. This is easy to get wrong, so I'd like to be able to write both positive and negative tests for what the API can be called with. For example:
// A templated function that should only be used via type deduction. (We
// don't want to let users to set template arguments explicitly in order
// to reserve the right to change them. They are not a public API.)
template <
int&... ExplicitArgumentBarrier,
typename T,
typename = std::enable_if<!std::is_same_v<T, int>>>
void AcceptVector(std::vector<T>);
// We should be able to feed the function most vectors.
static_assert(kCanCallAcceptVector<std::vector<double>>);
static_assert(kCanCallAcceptVector<std::vector<std::string>>);
// But not vector<int>.
static_assert(!kCanCallAcceptVector<std::vector<int>>);
It feels like I should be able to do this with std::is_invocable_v
, but that requires us to explicitly name the object being called, which requires providing template arguments and which assumes something about the overload set. Instead I literally want to test overload resolution: regardless of how many overloads for AcceptVector
there are and what their template arguments look like, is a call site spelled AcceptVector
that provides a particular type as argument valid?
What's the best way to do this? Here is what I've come up with:
#define DEFINE_CAN_CALL_VARIABLE(namespace_name, function_name) \
template <typename... Args> \
struct internal_CanCall##function_name final { \
private: \
struct Functor { \
template <typename... U, \
typename = decltype(::namespace_name ::function_name( \
std::declval<U>()...))> \
void operator()(U&&... args); \
}; \
\
public: \
static constexpr bool kValue = std::is_invocable_v<Functor, Args...>; \
}; \
\
template <typename... Args> \
inline constexpr bool kCanCall##function_name = \
internal_CanCall##function_name<Args...>::kValue;
DEFINE_CAN_CALL_VARIABLE(my::project, AcceptVector);
The idea is to define a functor that we can name for std::is_invocable_v
, and then lean on overload resolution in that functor. But there are some downsides:
I have to package it into a macro in order to make it reusable without having the same problem as std::is_invocable_v
over again.
I need to have the user provide the namespace as a macro argument in order to avoid problems due to argument-dependent lookup. (Although I said I'm interested in any call spelled AcceptVector
, I guess I really mean any call spelled AcceptVector
that resolves to an AcceptVector
function in my project's namespace.)
I'm not 100% sure I haven't missed other edge cases.
Does it seem like this is correct? Is there a less awful way to do this?
Since you tagged this C++20, the better approach is to write a concept:
template <typename... Args>
concept canAcceptVector = requires (Args(*args)()...) {
AcceptVector(args()...);
};
Which you can then test:
// We should be able to feed the function most vectors.
static_assert(canAcceptVector<std::vector<double>>);
static_assert(canAcceptVector<std::vector<std::string>>);
// But not vector<int>.
static_assert(!canAcceptVector<std::vector<int>>);
The odd formulation with the function pointer is to ensure that canAcceptVector<int>
, canAcceptVector<int&>
, and canAcceptVector<int&&>
try to call AcceptVector
with a prvalue, lvalue, and xvalue respectively. Probably not super important in this case, but I find it slightly less unwieldy than dealing with std::forward
and plus it correctly deals with prvalues.
The best C++17 approach for this problem is the detection idiom, which requires an extra step over concepts:
template <typename... Args>
using AcceptVector_t = decltype(AcceptVector(std::declval<Args>()...));
template <typename... Args>
inline constexpr bool canAcceptVector = is_detected_v<AcceptVector_t, Args...>;
Which you can static_assert
in the same way as above.