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;
}
my version: 333
{ x: 33, str: 333}
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?
I have some related question about template specialization:
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);
}
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:
- 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;
}
};