Search code examples
c++templatesoperator-overloadingoverload-resolution

Overload resolution for template operator<< not as expected


Question A

Given the code example here:

#include <iostream>
#include <string>

class LogStream {
public:
    LogStream& operator<<(int x) {
        std::cout << x;
        return *this;
    }
    LogStream& operator<<(const char* src) {
        std::cout << src;
        return *this;
    }
};

typedef char MyType[81];

template <typename OS>
OS& operator<<(OS &os, const MyType& data) {
  return os << "my version: " << data;
}

// error: use of overloaded operator '<<' is ambiguous
//        (with operand types 'LogStream' and 'char const[81]')

/* LogStream& operator<<(LogStream &os, const MyType& data) {
  return os << "my version2: " << (const char*)data;
} */


struct Test {
    int x;
    MyType str;
};

template <typename OS>
OS& operator<<(OS &os, const Test& data) {
  return os << "{ x: " << data.x << ", str: " << data.str << "}";
}

int main() {
    Test t = { 33, "333" };
    LogStream stream;

    stream << t.str;
    std::cout << std::endl;
    stream << t;
}

Actual Output

my version: 333
{ x: 33, str: 333}

Expected Output

my version: 333
{ x: 33, str: my version: 333}

Online compiler: https://godbolt.org/z/6os8xEars

My problem is: why does the first output use my specialized version of MyType, but the second one doesn't?

Question B

I have some related question about template specialization:

  1. What is the priority between function templates and regular functions when implicit conversion is needed, e.g:
struct MyType{};

template <typename T>
void test(T t, char (&data)[16]);

void test(MyType t, const char* data);

int main() {
    MyType mt;
    char src[16] = { "abc" };
    test(mt, src);
}
  1. Are there any tools visualize the overload resolution process, even when a program compiles successfully? Are there any ways to debug template code?

Solution

  • The short answer to the main question is: t is not const, but the Test parameter to your second operator template is. So, the expression t.str is a MyType&, but data.str is a const MyType&:

    template <typename OS>
    OS& operator<<(OS &os, const Test& data) {
        static_assert(std::same_as<const MyType&, decltype((data.str))>);
        return os << "{ x: " << data.x << ", str: " << data.str << "}";
    }
    
    int main() {
        Test t = { 33, "333" };
        static_assert(std::same_as<MyType&, decltype((t.str))>);
        LogStream stream;
    
        stream << t.str;
        std::cout << std::endl;
        stream << t;
    }
    

    This kind of difference can affect overload resolution, because a key aspect is the so-called implicit conversion sequence (ICS) required to transform a function argument to the type of the corresponding parameter.

    Unfortunately, overload resolution is not trivial, so there are quite a few things to unpack. For the expression stream << t.str, the viable functions and ICSs will be like this:

    // argument is MyType&
    LogStream& LogStream::operator<<(const char*); // MyType& -> char* -> const char*
    LogStream& operator<<(LogStream&, const MyType&); // identity
    

    The second version is counted as the identity conversion because

    Binding of a reference parameter directly to the argument expression is either Identity or a derived-to-base Conversion

    To decide whether one of two candidates is a better match, the compiler will consider numerous aspects of the viable functions and their conversion sequences. In this case rule 3a applies:

    S1 is a subsequence of S2, excluding lvalue transformations. The identity conversion sequence is considered a subsequence of any other conversion

    Therefore, the second ICS is better, making the template version the best viable function.

    For the second output:

    // argument is const MyType&
    LogStream& LogStream::operator<<(const char*); // const MyType& -> const char*
    LogStream& operator<<(LogStream&, const MyType&); // identity
    

    In this case, rule 3a does not apply since, excluding the array-to-pointer conversion, neither ICS is a proper subsequence of the other. None of the other rules apply, so the ICSs are indistinguishable. As a result, the non-template operator is now the best viable function:

    1. or, if not that, F1 is a non-template function while F2 is a template specialization

    This is also why the operator you commented out would be ambiguous. It is no longer ambiguous if you also comment out the line stream << t;.

    Additionally, this is the only point where it matters that one of the overloads is a template, of course apart from the requirement that it be a valid instantiation. So, in question B1, it is again the case that the function template is selected because it has the better ICS.

    As for question B2, I'm not aware of any specific tools, although it may be possible to get this kind of output from clang. Nowadays I use Compiler Explorer to figure out problems like these. I know the rules roughly, but you can bet I have to reread them closely before answering this kind of question. Now that you have these explanations, it should give you some idea of the (many) things to look for when you have a problem with overloads.

    For more reading, the official wording of the rules for operator overloading is in the section [over.match.best] of the standard.

    Edit: My preferred solution would wrap the "special" string type in a class. However, if you really must use C-style char arrays, you can still achieve the desired result by introducing a separate logging class:

    class MyLogStream
    {
        LogStream m_base{};
    public:    
        MyLogStream& operator<<(const MyType& data) {
            m_base << "my custom operator: " << (const char*)data;
            return *this;
        }
    
        MyLogStream& operator<<(const auto& data) {
            m_base << data;
            return *this;
        }
    };