Let's say we want to overload a function template f, but only if a similar overload is not declared yet:
template<typename T>
void f(T); // main prototype
struct A {};
struct B {};
//we want to declare B f(A), but only if something like A f(A) hasn't been declared
//we can try to check the type of expression f(A) before defining it
//and disable overload via enable_if
template<typename T = void> //it has to be a template to use enable_if
std::enable_if_t<std::is_same_v<void, decltype(T(), (f)(A{}))>, B> f(A);
// decltype expression depends on T, but always evaluates to the type of (f)(A{})
// parenthesis are used to disable ADL, so only preceding definitions are visible
The code is accepted by Clang, kind of works on GCC (See https://godbolt.org/g/ZGfbDW), and causes compiler error 'recursive type or function dependency context too complex' on Visual C++ 15.5.
My question is: is this a legal declaration according to the C++ standard or does it involve undefined behavior?
I believe this is a legal declaration based simply on there not really being any reason for it to not be.
Couple things worth pointing out. First, we have two uses of f
here, and they are different:
template<typename T = void>
std::enable_if_t<std::is_same_v<void, decltype(T(), (f)(A{}))>, B> f(A);
// ^^^ ^^^
// #1 #2
We are declaring the name f
in #2
, but it is not in scope for its usage in #1
† - its point of declaration is after the complete declarator, which includes that enable_if_t
block. So there's no recursion here. If VS has a problem with it, I suspect it might be related to their general issues around name lookup in templates.
Second, these aren't functionally equivalent templates - the main prototype takes an argument whose type is the template parameter, and this one takes an A
. The point of functional equivalency is to match up declarations to definitions in a way that doesn't require token-by-token copies - but our two f
function templates in this example are totally different.
I don't see a reason for this to be ill-formed. Eyebrow-raising, yes. Ill-formed, no.
†This leads to a fairly common error when trying to declare a recursive function template whose return type depends on itself. e.g.:
template <size_t V>
using size_ = std::integral_constant<size_t, V>;
constexpr size_<0> count() { return {}; }
template <typename T, typename... Ts>
constexpr auto count(T, Ts... ts)
-> size_<decltype(count(ts...))::value + 1> { return {}; }
int main() {
static_assert(count() == 0); // ok
static_assert(count(1) == 1); // ok
static_assert(count(1, 2) == 2); // error: no matching function to call
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With