Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Infix vs prefix syntax: name lookup differences

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:

  1. In what way are the two statements different (in terms of name lookup)?
  2. Why 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?

like image 335
Krzysztof Abramowicz Avatar asked Aug 17 '14 19:08

Krzysztof Abramowicz


2 Answers

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.

like image 57
Igor Tandetnik Avatar answered Nov 15 '22 13:11

Igor Tandetnik


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 is T1 and a right operand of a type whose cv-unqualified version is T2 , 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 of T1::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:

  • Look up as std::cout.operator<<(x); AND
  • Look up as 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.

like image 20
M.M Avatar answered Nov 15 '22 12:11

M.M