Arcane C++ rules, namespaces and overloads

Last week I needed to help a colleague that had a mysterious compiler error.

The code, the original was split into multiple files, looked at first like this

#include <iostream>

namespace A {
	struct foo{};
}

inline std::ostream operator<<(std::ostream& os, A::foo) {return os;}

namespace B {
	struct bar{};
	inline std::ostream operator<<(std::ostream& os, bar) {return os;}
	void fun() {
		std::cout << A::foo{};
	}
}

int main() {
	B::fun();
}

GCC, trying to helpful, presented multiple pages of diagnostic, enumerating all overload it considered for operator<<: char, int, unsigned int, short, long, float, B::bar, and many other depending on your includes like std::string, std::regex and so on (the list was very long). But for some reason, it did not find the overload for A::foo, even if the definition and implementation, was right after the class, on the same file.

Since operator<< is in the global namespace, and not in namespace A, there is even no need to know about ADL. There seemed really to be no reason for the compiler not to find the right function. I was sure it had nothing to do with the problem, so maybe it was just sheer luck that I insisted to move operator<< to namespace A while we were trying to figure out the issue since it belonged logically there:

#include <iostream>

namespace A {
	struct foo{};
	inline std::ostream operator<<(std::ostream& os, A::foo) {return os;}
}

namespace B {
	struct bar{};
	inline std::ostream operator<<(std::ostream& os, bar) {return os;}
	void fun() {
		std::cout << A::foo{};
	}
}

int main() {
	B::fun();
}

As this code, unexpectedly, compiled without a hiccup, I thought it might have been a compiler bug.

As there are no special rules for operators compared to normal free functions, I double-checked that I could reproduce the issue with normal functions:

#include <iostream>

namespace A {
	struct foo{};
}

inline void test(std::ostream, A::foo) {}

namespace B {
	struct bar{};
	inline std::ostream operator<<(std::ostream& os, bar) {return os;}
	void fun() {
		test(A::foo{});
	}
}

int main() {
	B::fun();
}

No luck, this code compiled without issues, as expected. I also tried with different compilers, but they all behaved the same way, so the issue had to be somewhere else.

Since I had no better ideas, I tried to search online for some clue, until I found someone with a similar issue.

I realized that the rules about namespaces, and overloads, are more arcane than I thought. I have no difficulties believing that for some people C++ looks like black magic, some errors are baffling and hard to diagnose or search for.

The reason why the code did not compile with operator<<, but did with the function test, is because of overloads. There where other functions with the same name (and normally operator<< is overloaded a lot), while there was only one function named test in our code.

Thus by adding another function test in the namespace B, the code will not compile.

#include <iostream>

namespace A {
	struct foo{};
}

inline void test(std::ostream, A::foo) {}

namespace B {
	struct bar{};
	inline void test(std::ostream, B::bar) {}
	void fun() {
		test(A::foo{});
	}
}

int main() {
	B::fun();
}

If ::test is moved to namespace A, the code compiles again:

#include <iostream>

namespace A {
	struct foo{};
	inline void test(std::ostream, A::foo) {}
}

namespace B {
	struct bar{};
	inline void test(std::ostream, B::bar) {}
	void fun() {
		test(A::foo{});
	}
}

int main() {
	B::fun();
}

So, the question remains: why moving one function from the global namespace to a non-global, increases its visibility?

As the first argument of test is std::ostream, because of ADL, the compiler searches for possible matches in namespace std. For the same reason, it searches in namespace A, without finding anything. Then, it searches inside namespace B, as the code is located in this namespace.

Because it finds something in namespace B, as the current namespace is considered the best match (before resolving overloads!), the compiler does not search further in the global namespace. This happens not because the compiler is sloppy, but because the C++ standard mandates it.

After collecting all the functions, the compiler tries to find a match. Since it can’t find any, it tries to be helpful and prints all the overloads it could find. And for operator<<, there happens to be a lot, and in our case generated a report long a couple of pages.

Would it make sense to change the language rules in order to avoid this issue? Maybe, but the advantages will not be so great, there are at least two issues.

  • Compilation times will increase because with such a change the compiler needs to search all function overload in more scopes.

  • There might be some new ambiguities, as more functions will be considered.

The solution to this problem is simple: put all pieces of an API in the same namespace. Not only because the code is more robust in case of overloads, but it also helps to maintain it, as the logically correlated code is not distributed in different scopes.