Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When defining a prototype of an overloaded C++ function template, is it legal to use its name to refer to previous definitions?

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?

like image 591
Григорий Шуренков Avatar asked Oct 28 '22 22:10

Григорий Шуренков


1 Answers

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
}
like image 150
Barry Avatar answered Nov 01 '22 14:11

Barry