5

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?
Bingo
  • 95
  • 7
  • what output did you expect? You have one overload to print a `MyType` and another to print a `Test`. `stream << t.str` calls the first because `t.str` is a `MyType` and `stream << t` calls the latter, because `t` is a `Test` – 463035818_is_not_an_ai Jun 12 '23 at 08:35
  • I expect the second line to use my specilized version for `MyType`. So expect output is `{ x: 33, str: my version: 333}` – Bingo Jun 12 '23 at 08:39
  • please include the expected output in the question and try to focus on a single question per question – 463035818_is_not_an_ai Jun 12 '23 at 08:39
  • Sorry, I have updated my question. @463035818_is_not_a_number – Bingo Jun 12 '23 at 08:43
  • @molbdnilo Yes. but t has a `MyType` member named `str`. see operator overload for `Test` class. – Bingo Jun 12 '23 at 08:45
  • 1
    this is more about overload resolution and array to pointer decay than template instantiation. – 463035818_is_not_an_ai Jun 12 '23 at 08:47
  • @molbdnilo This is just a demo from my real project. Actually I expect the second output to use my specilized version. The first output is just for debug. – Bingo Jun 12 '23 at 08:51
  • 2
    Another good question is why isn't the definition of `OS& operator<<(OS &os, const MyType& data)` infinitely recursive? – john Jun 12 '23 at 08:54
  • 1
    Removing `const` from the `Test` overload gives your expected output. I'll leave it to someone else to explain why though. :-) – super Jun 12 '23 at 08:55
  • the easy fix is to stay away from raw c-arrays and raw pointers for strings. Though, its a totally valid question that deserves a better answer than that – 463035818_is_not_an_ai Jun 12 '23 at 08:55
  • @super Yes, it works! But what if I cannot remove `const` from `Test` in my real project? Looking forward for your explaination. – Bingo Jun 12 '23 at 09:01
  • @463035818_is_not_a_number Actually `LogStream` class is from third party library in my real project. I just want to provide a specialized version for some cases. And it's hard to avoid `char*` in c++. – Bingo Jun 12 '23 at 09:06
  • its simple to avoid `char [N]` as member and `char*` in function signatures (unless you are dealing with de-/serialization, which doesnt seem to be the case here) though thats a topic for another question – 463035818_is_not_an_ai Jun 12 '23 at 09:09
  • I do not know why you made [godbolt link so complicated](https://godbolt.org/z/7n539hqWq) (your link uses cmake). Also take a look here at [cpp insights](https://cppinsights.io/s/9ed5ac19). – Marek R Jun 12 '23 at 09:16
  • @MarekR There is no need to use cmake. I just pick a random template. And thanks for your links. – Bingo Jun 12 '23 at 09:29
  • @john Look at the commented code. `LogStream& operator<<(LogStream &os, const MyType& data)` will results in infinitely recursive if there is no convertion for `data`. But It seems `OS& operator<<(OS &os, const MyType& data)` alwalys matches the member function overload. – Bingo Jun 12 '23 at 09:35
  • Overloads are preferred over templates, which is why 1) the member `operator<<` is preferred over any templates, and 2) you get an ambiguity if you "un-template" the templated `operator<<`. And note that `const MyType&` is *not* the same type as `const char(&)[81]`. – molbdnilo Jun 12 '23 at 09:45
  • @molbdnilo 1) if member `operator<<` is preferred over any templates, how do you explain the first line output. 2) ambiguous means there is no perferred matches, right? 3) Why `const MyType&` is different from `const char(&)[81]`, what `const MyType&` is if so? – Bingo Jun 12 '23 at 10:26
  • please 1 question per question – 463035818_is_not_an_ai Jun 13 '23 at 13:12

1 Answers1

1

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;
    }
};
sigma
  • 2,758
  • 1
  • 14
  • 18
  • Very appreciate for your professional answer!This is the explaination for my actual output. But I still have a question. Actually, `LogStream` is a library implementation, we can't change it. I just want to overload `operator<<(const char* str)` for type `char[81]`. How can I achive this goal? It seems imposible since `array-to-pointer` conversion is a `lvalue transformation`. I also have a code example [here](https://godbolt.org/z/K9Krooa4a) – Bingo Jun 14 '23 at 05:35
  • Another small issue. I relealized the output differs because of const issue from the comments. Then I go to verify that `data.str` is const. But I use `std::is_const_v` to check constness. It shows `data.str` is not const, which is different from your first `static_assert` result. So what's the difference between `is_const_v` and `same_as`? – Bingo Jun 14 '23 at 05:50
  • @Bingo: Is the char array also a requirement? As noted in the other comments, the easiest solution would be to avoid those and use a class type like `std::string`, `std::string_view` or even a custom wrapper class. – sigma Jun 14 '23 at 22:16
  • @Bingo As for the other comment, that is due to some other technical details I didn't mention. Namely, `decltype(data.str)` is not the same as `decltype((data.str))`: the former is just the typename of the member, `MyType` in this case, but the latter is the cv-ref-qualified type of the expression naming this member. You can use `is_const_v`, but then you have to write [`std::is_const_v>`](https://stackoverflow.com/questions/30806863/whats-the-equivalent-of-stdis-const-for-references-to-const). Isn't C++ fun! – sigma Jun 14 '23 at 22:22
  • Yes, C++ is fun... :( And char array is a requirement. I don't want to add a wrapper to char array because I have implemented a template log function to log any specilized struct type. It's not possible to add wrappers in this general log function. What I really need is a template specilization for char array... – Bingo Jun 15 '23 at 10:20
  • @Bingo Then I can see two options. One is to overload the operator for `const MyType*` instead and call it with `&data.str`. The other is to [create your own logger class](https://godbolt.org/z/6d4xW78dr) and provide a separate set of overloads. You could also write both as function templates. – sigma Jun 15 '23 at 17:12
  • Thanks. The second option seems resonable for me. – Bingo Jun 16 '23 at 02:37