5

I'm trying to define an equality operator for a type T defined in another namespace, and then use the equality operator on optional<T>. On clang (Apple LLVM 9.1.0), this code:

    namespace nsp {
        struct Foo {
        };
    }
    bool operator==(const nsp::Foo& a, const nsp::Foo& b);

    void foo() {
        optional<nsp::Foo> a = none;
        optional<nsp::Foo> b = none;
        if (a == b)
            ;
    }

Results in an error:

/usr/local/include/boost/optional/detail/optional_relops.hpp:29:34: error: invalid operands to binary expression ('const nsp::Foo' and 'const nsp::Foo')
{ return bool(x) && bool(y) ? *x == *y : bool(x) == bool(y); }
                      ~~ ^  ~~
MWE.cpp:40:19: note: in instantiation of function template specialization 'boost::operator==<what3words::engine::nsp::Foo>' requested here
            if (a == b)
                  ^
/usr/local/include/boost/optional/detail/optional_relops.hpp:28:6: note: candidate template ignored: could not match 'optional<type-parameter-0-0>' against 'const nsp::Foo'
bool operator == ( optional<T> const& x, optional<T> const& y )

What's happening? My guess is that it's something to do with the Koenig lookup rules...

Mohan
  • 7,302
  • 5
  • 32
  • 55

2 Answers2

5

Immediate fix

Do this:

namespace nsp {
  bool operator==(const Foo& a, const Foo& b);
}

to fix your problem.

If you have control over Foo, you can instead do:

namespace nsp {
  struct Foo {
    friend bool operator==(const Foo& a, const Foo& b) {
      return true;
    }
  };
}

which is optimal if Foo is a template class.

What went wrong with your solution

What is going on here is that optional is in std (or boost or whatever) and in that namespace it tries to do a nsp::Foo == nsp::Foo call.

There is a == that doesn't apply in the ::std namespace, so it won't look in ::; once it finds any == it stops looking, even if the arguments are completely unrelated. It also looks for == in the namespaces associated with the arguments -- in this case ::nsp. But it never looks in :: here either.

When adding operators to a type, always define the operator in the namespace of the type.

Namespaces can be reopened. So if you don't have control over the header file, you can create a new header file with the == in it. This == has to be visible at every point where optional<nsp::Foo>::operator== is invoked or your program is ill formed due to ODR violations (and, in this case, also generates a compiler error, which is useful to avoid heizenbugs).

The long version

When you invoke an operator (or a function), lookup follows a few simple steps.

First it looks around locally (in the local namespace). If it finds anything there, this search stops. (This includes using ns::identifier; names injected into the namespace, but usually not using namespace foo;). This "stop" occurs even if the function or operator won't work for the types in question; any == for any types whatsoever in a namespace stops this search.

If that fails to find a match, it starts looking in enclosing namespaces until it finds the function/operator, or reaches the root namespace. If there are using namespace foo; declarations, the functions/operators in those namespaces are considered to be in the "common parent" namespace of both the using namespace location and the namespace being imported. (So using namespace std; in namespace foo makes it seem like std is in ::, not in foo).

The result generates a collection of candidates for overload resolution.

Next, ADL (argument dependent lookup) is done. The associated namespaces of all function/operator arguments are examined. In addition, the associated namespaces of all the type arguments of the templates are also examined (recursively).

Operators/functions that match the name are collected. For ADL, parent namespaces are not examined.

These two collections of operators/functions are your candidates for overload resolution.

In your case, the namespace where == is called is boost. boost has plenty of == operators (even if they don't apply), so all of the == in boost are candidates.

Next, we examine the namespace of the arguments -- nsp::Foo in this case. We look in nsp and see no ==.

Then we run overload resolution on those. No candidates work, and you get a compiler error.

Now, when you move your user-defined == into namespace nsp, it is then added to the set of == found in the ADL step. And as it matches, it is called.

Overload resolution (what it does with the candidates) is its own complex subject. The short form is that it tries to find the overload that involves the least amount of conversion; and if two cases match exactly as well as each other, it prefers non-template over template and non-variardic over variardic.

There is a lot of detail in "least amount of conversion" and "exactly" that can mislead programmers. The most common is is that converting a Foo lvalue to Foo const& is a small amount of conversion, convering it to template<class T> T&& or T& is no conversion.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • It is indeed a duplicate, but I can't find any canonicals for that. May be it's time to create one? – SergeyA Aug 21 '18 at 14:48
  • @SergeyA https://stackoverflow.com/questions/3891402/operator-overloading-and-namespaces is close, but it asks "where to put it" not "what is going wrong". I'm not sure if a full description of all of the Koenig lookup rules work and best practices etc would be a good duplicate target; it would be a book chapter. – Yakk - Adam Nevraumont Aug 21 '18 at 14:50
  • I think, we can make this one a canonical, but I also think, we need slight elaboration on why the global namespace is not going to be looked up in this case. Also may be add some generalization (mentioning that there are tons of overloads for all common operators in `std` and `boost` namespaces) – SergeyA Aug 21 '18 at 14:52
  • @SergeyA Well, I tried to write a long version in non-standardese. There are certainly errors, omissions and simplifications. – Yakk - Adam Nevraumont Aug 21 '18 at 15:05
  • I think it is good, but you have a typo: `If that fails to find a matcT`. I will start using it as a duplicate for similar questions. – SergeyA Aug 21 '18 at 15:08
  • @vdavid why would you do this? Just define the whole logic of equality operator in the struct namespace, do not put another overload in global namespace (why would you pollute it?) – SergeyA Aug 21 '18 at 15:09
  • @vdavid, yes, but you are trying to forward the actual logic to `==` defined in global namespace, and I can't understand why. – SergeyA Aug 21 '18 at 15:30
  • @SergeyA Because I simply forgot this was the whole purpose of the OP ~.~ I will delete my comments which add unnecessary confusion. – vdavid Aug 21 '18 at 15:39
  • This is detailed and informative. Unfortunately I simplified the original problem a bit. I want to overload `template ostream& operator<<(ostream& os, const optional>&)`. (Only in debugging code, so don't worry about side-effects.) It's not obvious what namespace to use... `boost` doesn't do the trick. Although I may have done something else wrong... – Mohan Aug 21 '18 at 17:55
  • Actually, even overriding `operator<<` for `unique_ptr` seems to be tricky. – Mohan Aug 21 '18 at 18:02
  • 2
    @Mohan The answer to that question is "you cannot". There is no good legal way to do that for a generic type `T`; I could explain why, but it would take a new question/answer to do so, or you could trust me. You can do it for a type you own `Foo` (in which case, put it in `Foo`'s namespace), but not for a generic type `T`. – Yakk - Adam Nevraumont Aug 21 '18 at 18:06
  • Adam: I don't need to per se, but I'm genuinely interested, so I'll ask the Q. Thanks in advance! – Mohan Aug 21 '18 at 18:08
  • https://stackoverflow.com/questions/51954454/why-is-it-not-possible-to-override-operator-for-template-classes-involving-thi – Mohan Aug 21 '18 at 18:16
0

Indeed, you should enable ADL by declaring the operator== implementation inside an associated namespace for Foo. This fixes that:

#include <boost/optional.hpp>

namespace nsp {
    struct Foo { };
    bool operator==(const Foo&, const Foo&) { return false; }
}

int main() {
    boost::optional<nsp::Foo> a;
    boost::optional<nsp::Foo> b;
    return (a == b)? 0 : 1;
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I consider it is good practice is to declare binary operators as a friend inside the class definition, especially when using templates. – Gold Aug 21 '18 at 15:02
  • @Gold Me too. However, when using templates, declaring them friend can run into some compiler limitations – sehe Aug 21 '18 at 15:03