Search code examples
c++c++11templatesdecltype

Changing a template return type appears to have an effect on an overload resolution


Have been writing an arithmetic wrapper that could help detecting over/underflow errors, however stuck with a rather devious problem in the process.

Suppose we have a class, that handles everything that is able to cause an overflow via some overloaded operators and is implicitly castable to the underlying type for everything else. This example contains only a binary plus operator:

template<typename T_>
class Wrapper
{
    public:
    Wrapper(T_ val_) : m_value(val_) { } // converting constructor
    operator T_(void) const { return m_value; } // underlying type conversion
    // some other methods

    // binary plus operators:
    template<typename U_>
    const Wrapper<decltype(T_() + U_())> operator +(U_ val_) const
    {
        // supposed to handle 'Wrapped + Unwrapped' case
        return m_value + val_;
    }
    template<typename U_>
    const Wrapper<decltype(T_() + U_())> operator +(Wrapper<U_> other_) const
    {
        // supposed to handle 'Wrapped + Wrapped' case
        return m_value + other_.m_value;
    }

    template<typename U0_, typename U1_>
    friend const Wrapper<decltype(U0_() + U1_())> operator +(U0_ val_, Wrapper<U1_> wrapper_)
    {
        // supposed to handle 'Unwrapped + Wrapped' case
        return val_ + wrapper_.m_value;
    }

    private:
    T_ m_value;
};

This (if I didn't miss something while pasting it here) compiles fine and works as expected in situations like these (every possible one of them, basically):

Wrapper<int> val = 3.14f;
::std::cout << val + 42 << ::std::endl; // Wrapped + Unwrapped
::std::cout << 42 + val << ::std::endl; // Unwrapped + Wrapped
::std::cout << val + val << ::std::endl; // Wrapped + Wrapped

However, whenever I try to make an alias for the decltype(...) part of either the 'Wrapped + Unwrapped' or the 'Unwrapped + Wrapped' for example like this:

template<typename T0_, typename T1_>
struct Result
{
    typedef decltype(T0_() + T1_()) Type;
};

template<typename T_>
class Wrapper
{
    //...
    template<typename U_>
    const Wrapper<typename Result<T_, U_>::Type> operator +(U_ val_) const
    //...
    template<typename U0_, typename U1_>
    friend const Wrapper<typename Result<U0_, U1_>::Type> operator +(U0_ val_, Wrapper<U1_> wrapper_)
    //...
};

the 'Wrapped + Wrapped' example doesn't want to compile, because the overload resolution appears to change towards the undesired variant. It throws an error about unavailability of a default constructor for Wrapper<int>, impying the attempt to use either the 'Wrapped + Unwrapped' or 'Unwrapped + Wrapped', both of which are not suited to handle the case in question properly.

And that confuses me greatly as it looks like change in a return type leads to a change in an overload resolution behavior. Will appreciate any advice regarding the matter.


Solution

  • Here's roughly how overload resolution works:

    1. Name lookup to find candidate functions and function templates.
    2. Deduce template arguments for each function template, and substitute deduced arguments into the template to generate a single function template specialization as the candidate. Throw out any function template for which deduction fails (including substitution failure).
    3. Compare the candidates. Pick the best one or complain if there isn't one.

    If step 2 triggers a hard error - by forming an invalid construct outside the immediate context of the function template signature - then your program is ill-formed; you never get to step 3. It doesn't matter if the candidate wouldn't have been chosen in step 3 if the error wasn't there.

    Here, decltype(T_() + U_()) was in the immediate context originally. Therefore, when U_ got deduced to Wrapper<...> and substituted into the signature, that expression is ill-formed, but the error is in the immediate context and so it's a substitution failure. But when you move that expression into a separate class template Result, the error is no longer in the immediate context of the function template's signature, so it's a hard error instead.

    If you don't want to repeat the expression multiple times, use an alias template:

    template<class T, class U>
    using result = decltype(T() + U());