Operators in C++ are usually considered to be an alternative syntax for functions/methods, especially in the context of overloading. If so, the two expressions below should be synonymous:
std::cout << 42;
operator<<(std::cout, 42);
In practise, the second statement leads to the following error:
call of overloaded ‘operator<<(std::ostream&, int)’ is ambiguous
As usual, such error message is accompanied with a list of possible candidates, these are:
operator<<(basic_ostream<_CharT, _Traits>& __out, char __c)
operator<<(basic_ostream<char, _Traits>& __out, char __c)
operator<<(basic_ostream<char, _Traits>& __out, signed char __c)
operator<<(basic_ostream<char, _Traits>& __out, unsigned char __c)
Such error raises at least two questions:
operator<<(basic_ostream<char, _Traits>& __out,
int
__c)
is missing?It seems, that infix and prefix notations are not fully interchangeable -- different syntax entails different name resolution tactics. What are the differences and where did they come from?
No, the two expressions should not be synonymous. std::cout << 42
is looked up as both operator<<(std::cout, 42)
and std::cout.operator<<(42)
. Both lookups produce viable candidates, but the second one is a better match.
These are the operator lookup rules from C++17 [over.match.oper/3] where I have edited for brevity by removing text that does not pertain to overloading operator<<
with the left operand being a class type; and bolded a section which I will explain later on:
For a binary operator
@
with a left operand of a type whose cv-unqualified version isT1
and a right operand of a type whose cv-unqualified version isT2
, three sets of candidate functions, designated member candidates, non-member candidates and built-in candidates, are constructed as follows:
- If
T1
is a complete class type or a class currently being defined, the set of member candidates is the result of the qualified lookup ofT1::operator@
(16.3.1.1.1); otherwise, the set of member candidates is empty.- The set of non-member candidates is the result of the unqualified lookup of
operator@
in the context of the expression according to the usual rules for name lookup in unqualified function calls except that all member functions are ignored.
The built-in candidates are empty here, that refers to searching functions that would implicitly convert both operands to integer types and apply the bit-shift operator; but there is no implicit conversion from iostreams to integer type.
The set of candidate functions for overload resolution is the union of the member candidates, the non-member candidates, and the built-in candidates.
What is the rationale is for having operator lookup differ from other function lookup and what does this all mean? I think this is best answered through a couple of examples. Firstly:
struct X{ operator int(); };
void f(X);
struct A
{
void f(int);
void g() { X x; f(x); } // Calls A::f
};
In this example there is a principle: if you try to call a member function of the class from another member function of the class; it should definitely find that member function, and not have the search polluted by outside functions (even including ADL).
So, part of the unqualified lookup rules is that if the non-ADL part of lookup finds a class member function, then ADL is not performed.
Without that rule, f(x)
would find both A::f
and ::f
and then overload resolution would select ::f
as better match, which we don't want.
Onto the second example:
struct X{};
std::ostream& operator<<(std::ostream&, X);
struct S
{
std::ostream& operator<<(int);
void f()
{
X x;
std::cout << x; // OK
// operator<<(std::cout, x); // FAIL
// std::cout.operator<<(x); // FAIL
}
};
As per the principle of the previous example -- if the rules were just that std::cout << 42;
is transformed to operator<<(std::cout, 24);
then name lookup would find S::operator<<
and stop. Whoops!
So I think it is not quite correct to say that the behaviour of the OK
line above comes from doing both of the lines marked FAIL
, as other answers/comments have suggested.
SUMMARY:
Now we can understand the actual wording of the standard quote at the top of my answer.
The code std::cout << x;
will:
std::cout.operator<<(x);
AND
operator<<(std::cout, x)
EXCEPT THAT member functions are ignored (and therefore, there is no ADL-suppression due to member function being found).Then overload resolution is performed on the union of those two sets.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With