In an unprecedented feat of miraculousness, Freenode's ##C++ discussion channel saw — in the space of no more than twenty minutes — the same question asked twice of one of C++'s more subtle features. Just how do you declare a friend
from the global namespace?
To demonstrate the scenario, consider the following code:
int main() { N::S s; std::cout << s; // error: 'int N::S::x' is private }
Although the member variable N::S::x
is marked private, friend
ing the operator<<
function should allow access to it; therefore, the expected output is "1". But not so!
The reason is almost disappointingly inelaborate. friend
statements are, in fact, function declarations too.
That means that when you write friend ostream& operator<<(ostream& os, const S&)
you're saying "there's a function with this signature, and it's allowed to access stuff in me". The problem here is that the function declaration is inside the namespace N
, so you've managed to implicitly declare a function ostream& N::operator<<(ostream& os, const S&)
… but your actual operator<<
is in the global namespace. You've declared a function that you never define nor use.
Phew!
In itself this is harmless, but it does mean that your friend
statement has nothing to do with the global operator<<
that you actually use. Consequently, access to private or protected members from within it is still prohibited.
The most obvious fix here is to explicitly specify that you're declaring and friend
ing a function in the global namespace, using the "::
" operator (line 6):
namespace N { struct S { friend ostream& ::operator<<(ostream& os, const S&); S() : x(0) {}; private: int x; }; } ostream& operator<<(ostream& os, const N::S& s) { return os << s.x; }
int main() { N::S s; std::cout << s; }
(I've also added a couple of now-necessary forward declarations.)
Going further
This is all well and good, but let's take a look at what happens when we befriend
a different function. I've picked a simple one called F
that takes a ref-to-const-S
and returns an int
:
namespace N { struct S { friend int ::F(const S& s); S() : x(0) {}; private: int x; }; } int F(const N::S& s) { std::cout << s.x; return 0; }
int main() { N::S s; F(s); }
All that's really changed is that the function has a different signature.
The key difference here is that our operator<<
returned ostream&
, whereas our F
returns int
. One is a reference, and one is not. And the problem relates to parsing.
Let's take a look at the two friend
statements side by side:
In the first example, it's clear to the compiler what you are doing. ostream&
can only be a type (due to the placement of the &
symbol, and a few other things) and the rest falls into place.
But in the second example things aren't quite so clear. The compiler doesn't care about spaces quite as much as you or I, and in fact the code would mean the same in all the following variations:
Notice how int::F
could be seen as a function F
inside a namespace int
. However, int
is a keyword and this would be quite illegal. The compiler knows this, so the example compiles successfully and means what you expect.
But what if we didn't use int
? Still returning by value, let's use a return type that isn't a keyword:
namespace N { struct S { friend S ::F(const S& s); S() : x(0) {}; private: int x; }; } N::S F(const N::S& s) { std::cout << s.x; return s; }
int main() { N::S s; F(s); // error: ISO C++ forbids declaration of 'F' with no type }
The compiler now sees a declaration and befriending of a function S::F
that returns nothing, which is illegal. Parsing fails. And although we've tweaked around with the code a couple of times, we've ended up with something otherwise completely valid: this code structure is hardly unusual.
The solution to rule them all
Fortunately, we can rely on our trusty parentheses to put the topic to rest once and for all. Check out the new friend
statement:
namespace N { struct S { friend S (::F)(const S& s); S() : x(0) {}; private: int x; }; } N::S F(const N::S& s) { std::cout << s.x; return s; }
int main() { N::S s; F(s); }
Bootnote
It may seem like I've been describing an edge case, but this really does come up more often than one might think. Hopefully this article can serve as some kind of reference for when that day comes.