decltype, recursive type deduction for overloaded operator

418 views Asked by At

For a class with expression templates, I stumbled over the following error during return type deduction of overloaded operators. The example below illustrates the error:

template < typename T >
struct A_Number {
     T x;
};


// TAG1
template < typename T >
A_Number< T >
operator+(const A_Number< T > &a, const A_Number< T > &b)
{
    return {a.x + b.x};
}

// TAG2
template < typename T, typename S >
A_Number< T >
operator+(const A_Number< T > &a, const S &b)
{
    return {a.x + b};
}

// TAG3
template < typename T, typename S >
auto
operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)
//                                                        ^^^^^
{
    return a + b;
}

int
main(void)
{
    auto x1 = A_Number< int >{1};
    auto x2 = A_Number< int >{1};

    auto res1 = x1 + 1;  // instantiates TAG2

    auto res2 = 1 + x1;  // instantiates TAG3, TAG2

    auto res3 = x1 + x2; // error, tries to match TAG3
    return EXIT_SUCCESS;
}

When trying to compile this with g++-5 or clang++, I get this error

fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
 operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)

Apparently, the compilers try to match version TAG3, although a better match (TAG1) is available. When trying to match, they try to deduce the return type which seems to cause recursive instantiations of TAG3. Why does the return type deduction not see the other (better matching) overloads? And is it correct behavior to deduce the return type even though another overloaded template function has a better matching signature?

Interestingly, this error evaporates in thin air, when omitting the return type altogether and compiling with c++14, like so:

// TAG3
template < typename T, typename S >
auto
operator+(const S &b, const A_Number< T > &a) // C++14
{
    return a + b;
}

Admittedly, it's an academic question because workarounds are possible. But can anybody elucidate whether this behavior is standard conforming or a compiler bug?

1

There are 1 answers

0
Barry On

This is actually correct behavior on the compilers' part. Overload resolution has two steps, from [over.match]:

— First, a subset of the candidate functions (those that have the proper number of arguments and meet certain other conditions) is selected to form a set of viable functions (13.3.2).
— Then the best viable function is selected based on the implicit conversion sequences (13.3.3.1) needed to match each argument to the corresponding parameter of each viable function.

The candidate functions for x1 + x2 are:

// TAG1
template < typename T >
A_Number< T >
operator+(const A_Number< T > &a, const A_Number< T > &b);

// TAG2
template < typename T, typename S >
A_Number< T >
operator+(const A_Number< T > &a, const S &b);

// TAG3
template < typename T, typename S >
auto operator+(const S &b, const A_Number< T > &a) -> decltype(a + b)

So ok, we need to determine what the return type is for TAG3 first, before we pick the best viable candidate. Now, operator+ is a dependent name here, as it depends on two template arguments. So according to [temp.dep.res]:

In resolving dependent names, names from the following sources are considered:
— Declarations that are visible at the point of definition of the template.
— Declarations from namespaces associated with the types of the function arguments both from the instantiation context (14.6.4.1) and from the definition context.

So to resolve the return type of TAG3, we need to do lookup and overload resolution on a+b. That gives us our same three candidates again (the first two found the usual way, TAG3 not-so-helpfully being found again via ADL), so around and around we go.

Your solutions are:

  • In C++14, drop the trailing return type for TAG3. That stops the merry-go-round, and we will start with our three viable candiates, of which TAG1 is most specialized so it will be selected as the best viable candidate.
  • Prevent TAG3 from being instantiated if S is A_Number<T>. For instance, we can create a type trait for A_Number:

    template <typename T>
    struct is_A_Number : std::false_type { };
    
    template <typename T>
    struct is_A_Number<A_Number<T>> : std::true_type { };
    
    // TAG3
    template < typename T, typename S, 
               typename = typename std::enable_if<!is_A_Number<S>::value>::type>
    auto operator+(const S &b, const A_Number< T > &a) -> decltype(a + b);
    

That will compile even in C++11.