1

I wanted to create a std::set<X>, for a pod struct, X. I tried providing a free operator<. I did not want this operator to have a global effect (outside this compilation unit), so I set the operator< as static. (link to code)

struct X { int value; };

static bool operator< (const X& a, const X& b) { return a.value < b.value; }

void foo() {
    std::set<X> some_set;
    some_set.insert({1});
}

This compiles fine.

However, if I move the operator< in an unnamed namespace (link to code)

struct X { int value; };
namespace {
     bool operator< (const X& a, const X& b) { return a.value < b.value; }
}

void foo() {
    std::set<X> some_set;
    some_set.insert({1});
}

which should be equivalent,

then std::less<X> complains:

usr/local/include/c++/12.1.0/bits/stl_function.h:408:20: error: no match for 'operator<' (operand types are 'const X' and 'const X')
         { return __x < __y; }

Okay, I could have used a custom comparator instead.

But can anybody explain why the above does not work? In general, is it possible to provide a different operator implementation in compilation units of the same struct?

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Grim Fandango
  • 2,296
  • 1
  • 19
  • 27
  • 1
    This would violate One Definition Rule and lead to Undefined Behavior. – Marek R Jan 20 '23 at 11:00
  • 1
    @MarekR Not with `static` functions. – HolyBlackCat Jan 20 '23 at 11:05
  • 1
    @HolyBlackCat note there is `std::less` involved when using `std:set`, this example is quirky and interesting. I do not know what will happen if only `static` version is used (IMO UB), but version with anonymous namespace can do not compile and this can be nicely explained. – Marek R Jan 20 '23 at 11:07
  • Read about one definition rule – Alex Jan 20 '23 at 13:33
  • @Ronald, one definition rule has to do with global symbols. But I am clearly trying to to break this by setting the operators as static, or in an unnamed namespace. How do you suggest I am violating the ODR ? – Grim Fandango Jan 24 '23 at 10:56

1 Answers1

4

This boils down to following:

#include <iostream>

struct A {};

namespace
{
    void foo(A) {} // Your `operator<`.
}

namespace N
{
    void foo(int) {} // Some random `operator<` in `std`.

    template <typename T>
    void bar(T value)
    {
        foo(value);
    }
}

int main()
{
    N::bar(A{});
}

This doesn't work because N::foo shadows foo(A) in the unnamed namespace.

If there was no unnamed namespace, foo(A) would instead be found via ADL, because it would be in the same namespace as A.


Also note that this appears to violate one-definition rule. While your operator< is static, std::less is not, and its instantiations in different translation units will pick different operator<s, which appears to be illegal.

This could cause one of our operator<s to be used in every TU, when inline implementations of std::less from different TUs are ultimately merged by the linker.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • what you are saying implies that even if I create 2 files, each with a (different) `namespace {struct Comparator}`, to sort a different `set` variable, then we get an ODR vialation via `std::less`? I am pretty sure I've seen the exported symbols of the .obj files (using `nm`) containing class template instantiantions of classes in unnamed namespaces, and: - if the templated classes (eg `set`) contain instantiations of local classes (eg. `namespace{ struct Comparator}`) then they don't enter the symbolic table at also! – Grim Fandango Jan 21 '23 at 07:39
  • 1
    @GrimFandango If you specify a custom comparator, then `less` is not called (being the *default* comparator), so you're fine. This ODR violation only happens if you overload `operator<` differently in different files. – HolyBlackCat Jan 21 '23 at 08:22