Search code examples
c++operator-overloadingargument-dependent-lookup

Why ADL has a different behavior for operator function than other functions?


I have created a C class inside a NS_C namespace that way:

#include <iostream>

namespace NS_C {
  template <typename T>
  class C {
    public:
      C operator+(long) {
        std::cout << "NS_C::C::operator+\n";
        return *this;
      }

      void not_operator(C<T>, long) {
        std::cout << "NS_C::C::not_operator\n";
      }

      void call() {
        *this + 0;
        not_operator(*this, 0);
      }
  };
}

The function call is supposed to call NS_C::C::operator+ then NS_C::C::not_operator. To test this behavior, I run this small program:

int main()
{
  NS_C::C<int> ci;
  ci.call();

  return 0;
}

The output is what I expected:

> g++ -o example example.cpp && ./example
NS_C::C::operator+
NS_C::C::not_operator

Now, I want to create a new class A within a separate namespace NS_A and add to this namespace two generic overloads of operator+ and not_operator functions:

#include <iostream>

namespace NS_A {
  class A {};

  template <typename T>
  T operator+(T t, int)
  {
    std::cout << "NS_A::operator+\n";
    return t;
  }

  template <typename T>
  void not_operator(T, int)
  {
    std::cout << "NS_A::not_operator\n";
  }
}

Thanks to ADL, a call to call member function from an NS_C::C<NS_A> object will call the overloaded NS_A::operator+ as it is better match (the second parameter is int in NS_A::operator+ and long in NS_C::C::operator+). However, I don't understand why the same behavior doesn't occur for my not_operator function. Indeed, NS_C::C::not_operator will still be called from call function.

Let's use the following main function:

int main()
{
  NS_C::C<NS_A::A> ca;
  ca.call();

  return 0;
}

I have the following output:

NS_A::operator+
NS_C::C::not_operator

Why NS_A::not_operator is not called in that case?


Here is the complete code to reproduce the issue:

#include <iostream>

namespace NS_A {
  class A {};

  template <typename T>
  T operator+(T t, int)
  {
    std::cout << "NS_A::operator+\n";
    return t;
  }

  template <typename T>
  void not_operator(T, int)
  {
    std::cout << "NS_A::not_operator\n";
  }
}

namespace NS_C {
  template <typename T>
  class C {
    public:
      C operator+(long) {
        std::cout << "NS_C::C::operator+\n";
        return *this;
      } 

      void not_operator(C<T>, long) {
        std::cout << "NS_C::C::not_operator\n";
      }

      void call() {
        *this + 0;
        not_operator(*this, 0);
      }
  };
}   

int main()
{
  NS_C::C<int> ci;
  ci.call();

  NS_C::C<NS_A::A> ca;
  ca.call();

  return 0;
}

Solution

  • From overload_resolution#Call_to_an_overloaded_operator:

    We have in overload sets of candidates for overloaded operator:

    1) member candidates: if T1 is a complete class or a class currently being defined, the set of member candidates is the result of qualified name lookup of T1::operator@. In all other cases, the set of member candidates is empty.

    2) non-member candidates: For the operators where operator overloading permits non-member forms, all declarations found by unqualified name lookup of operator@ in the context of the expression (which may involve ADL), except that member function declarations are ignored and do not prevent the lookup from continuing into the next enclosing scope. If both operands of a binary operator or the only operand of a unary operator has enumeration type, the only functions from the lookup set that become non-member candidates are the ones whose parameter has that enumeration type (or reference to that enumeration type)

    Whereas for the other, we only have unqualified_lookup

    There is even an example in unqualified_lookup#Overloaded_operator showing difference between operator+(a, a) and a + a