4

Suppose we have a class foo from a namespace space which declares a friend function named bar, which is later on defined, like so:

namespace space {
    struct foo {
        friend void bar(foo);
    };
}

namespace space {
    void bar(foo f) { std::cout << "friend from a namespace\n"; }
}

To my understanding, friend void bar(foo); declares bar to be a free function inside space taking a foo by value. To use it, we can simply do:

auto f = space::foo();
bar(f);

My understanding is that we don't have to say space::bar, because ADL will see that bar is defined in the same namespace as foo (its argument) and allow us to omit the full name qualification. Nonetheless, we are permitted to qualify it:

auto f = space::foo();
space::bar(f);

which works (and should work) exactly the same.

Things started to get weird when I introduced other files. Suppose that we move the class and the declaration to foo.hpp:

#ifndef PROJECT_FOO_HPP
#define PROJECT_FOO_HPP

namespace space {
    struct foo {
        friend void bar(foo);
    };
}

#endif //PROJECT_FOO_HPP

and the definitions to foo.cpp:

#include "foo.hpp"
#include <iostream>

namespace space {
    void bar(foo f) { std::cout << "friend from a namespace\n"; }
}

notice that all I did was I moved (didn't change any code) stuff to a .hpp-.cpp pair.

What happened then? Well, assuming that we #include "foo.hpp", we still can do:

auto f = space::foo();
bar(f);

but, we are no longer able to do:

auto f = space::foo();
space::bar(f);

This fails saying that: error: 'bar' is not a member of 'space', which is, well, confusing. I am fairly certain that bar is a member of space, unless I misunderstood something heavily. What's also interesting is the fact that if we additionally declare (again!) bar, but outside of foo, it works. What I mean by that is if we change foo.hpp to this:

#ifndef PROJECT_FOO_HPP
#define PROJECT_FOO_HPP

namespace space {
    struct foo {
        friend void bar(foo);
    };
    
    void bar(foo); // the only change!
}

#endif //PROJECT_FOO_HPP

it now works.

Is there something with header / implementation files that alters the expected (at least for me) behaviour? Why is that? Is this a bug? I am using gcc version 10.2.0 (Rev9, Built by MSYS2 project).

Boann
  • 44,932
  • 13
  • 106
  • 138
Fureeish
  • 9,869
  • 3
  • 23
  • 46
  • Without the additional declaration outside of the `struct`, it's not *just* a `friend`, it's a **hidden** `friend`. That hidden friend's declaration is not injected into the namespace. – Eljay May 09 '21 at 16:31
  • @Eljay I was aware of the term *hidden friend*, but I had no idea it carried some actual, semantic meaning. Is it an actual term from the standard or there is a set or rules that is just interpreted as referring to *a hidden friend*? – Fureeish May 09 '21 at 16:35
  • Ehmm, unrelated, but you may consider `#pragma once` instead of traditional define guards – MatG May 09 '21 at 16:37
  • @MatG I am aware of `#pragma once`, of the fact that it is non-standard and of the fact that despite the previous statement, almost every relevant compiler supports it, but thank you for the comment :) – Fureeish May 09 '21 at 16:47
  • Zwol, the person who originally made `#pragma once` in GCC, advises not to use it. https://stackoverflow.com/a/34884735/4641116 – Eljay May 09 '21 at 17:12
  • @Eljay Wow, I'll stop using it! – MatG May 09 '21 at 19:30

1 Answers1

3

There's a slight subtlety that the friend declaration, while it doesn't require a previous declaration of the function or class your class is befriending, does not make the function visible for lookup except via ADL.

cppreference:

A name first declared in a friend declaration within a class or class template X becomes a member of the innermost enclosing namespace of X, but is not visible for lookup (except argument-dependent lookup that considers X) unless a matching declaration at the namespace scope is provided - see namespaces for details.

This is why you're able to find bar(f) (performs ADL) but not space::bar(f) (fully qualifying the name means ADL is not invoked).

Calling code that doesn't find bar via ADL needs to see a declaration. In the version where everything is in one file, calling code will see the entire definition of space::foo. When you split it into an HPP and a CPP file, calling code only sees the friend declaration which provides limited accessibility.

As you identified, if you want to make the function visible via ordinary lookup, put a declaration of foo in "foo.hpp":

#ifndef PROJECT_FOO_HPP
#define PROJECT_FOO_HPP

namespace space {
    struct foo {
        friend void bar(foo);
    };

    void bar(foo); // Now code that includes foo.hpp will see a declaration for bar
}

#endif //PROJECT_FOO_HPP
Nathan Pierson
  • 1,544
  • 1
  • 4
  • 14
  • "*In your first example, calling code will see the entire definition of `space::foo`*" - can you elaborate on that? Why would it see any definitons, since I am only `#include`ing `foo.hpp"`? I don't quite get the second paragraph of your answer. – Fureeish May 09 '21 at 16:37
  • @Fureeish By "first example" I mean your very first block of code, from before you split `namespace space` into separate files. – Nathan Pierson May 09 '21 at 16:38
  • Everything makes sense now, thanks. The quote from cppreference explains everything very clearly. Thank you! – Fureeish May 09 '21 at 16:51