How do C++ using-directives work?
Recall that in C++ there’s a difference between a using-declaration and a using-directive.
using std::string; // using-declaration
using namespace std; // using-directive
Using-declarations
A using-declaration is semantically similar to a declaration; it introduces into the current scope a new meaning for a name. If that name already had one or more meanings from outer scopes, those existing meanings are hidden, or shadowed, by the new declaration. Godbolt:
namespace AnimalUtils {
int foo(Zoo::Animal);
}
namespace Outer {
int foo(Zoo::Lion);
namespace Inner {
int foo(Zoo::Cat); // declaration hides Outer::foo
int test1() {
return foo(Zoo::Lion());
}
int test2() {
using AnimalUtils::foo; // using-declaration hides Inner::foo
return foo(Zoo::Lion()); // calls AnimalUtils::foo
}
}
}
The declaration of foo
in Inner
hides, or shadows, the previous meaning that foo
had had;
so when we look up the name foo
inside test1
, we find only Inner::foo
.
We don’t even consider Outer::foo
as a candidate.
The using-declaration of foo
in test2
hides, or shadows, the previous meaning that foo
had had; so when we look up the name foo
inside test2
, we find only AnimalUtils::foo
.
We don’t even consider Inner::foo
as a candidate.
(So how does the std::swap
two-step work?
Well, besides the normal unqualified lookup, we also do a separate argument-dependent lookup,
and merge the two candidate sets. So in this particular case, overload resolution would also
consider any meanings of foo
declared in namespace Zoo
. But there is no such foo
in
this example.)
Using-directives
Using-directives are subtler than using-declarations. But the good news is that
you should never use them!
Seriously, don’t ever write using namespace Whatever
; and then you won’t have
any trouble with them. Just pretend they don’t exist.
But, let’s talk about them anyway.
Using-directives also introduce new meanings for names into scopes. But the surprising thing is that they don’t introduce those meanings into the current scope! A using-directive introduces its new meanings into the scope which is the lowest common ancestor of the current scope and the target namespace’s own scope.
In a directed acyclic graph (DAG), the “lowest common ancestor” of two nodes A and B can be found by tracing a path from A up to the root, and tracing a path from B up to the root — the lowest common ancestor is the place where the two paths first meet up.
Alternatively, start tracing a “fully qualified path” downward from the root; the lowest common ancestor corresponds to the longest common prefix of these fully qualified names. For example, consider this hierarchy of namespaces:
namespace NA { }
namespace NB {
namespace NC {
namespace N1 { }
namespace N2 { }
}
}
The lowest common ancestor of NB::NC::N1
and NB::NC::N2
is NB::NC
.
The lowest common ancestor of NA
and NB::NC::N2
is the root (the global namespace).
So, consider this snippet of C++ code (Godbolt):
namespace NA {
int foo(Zoo::Lion);
}
namespace NB {
int foo(Zoo::Lion);
namespace NC {
namespace N1 {
int foo(Zoo::Cat);
}
namespace N2 {
int test() {
using namespace N1;
using namespace NA;
return foo(Zoo::Lion());
}
}
}
}
You might reasonably expect that when test
passes a Lion
to foo
,
it would result in a call to NA::foo(Lion)
, because we “using-directive’d”
namespace NA
. Or at least it might call NB::foo(Lion)
, since that’s
what would happen in the absence of any using-directives at all.
But in fact it calls N1::foo(Cat)
!
As shown in the diagram below: using namespace N1
causes a declaration of
N1::foo(Cat)
to be injected into the least common ancestor of N1
and N2
,
which is NC
. This declaration of the name foo
hides any and all meanings
of foo
introduced in higher-up scopes, such as NB::foo(Lion)
.
using namespace NA
causes a declaration of NA::foo(Lion)
to be injected
into the least common ancestor of NA
and N2
, which is the global namespace.
This injected declaration ends up being hidden by the declaration of NB::foo(Lion)
,
which is in turn hidden by the declaration of N1::foo(Cat)
which has been
injected into namespace NC
.
By the way, even though the declaration of N1::foo
has been injected into NC
,
you can’t actually refer to it as NC::foo
. It has been injected only for the purposes
of unqualified lookups — and only for the purposes of unqualified lookups that happen
within this scope. Nobody outside of the scope of test
is going to “see” that
declaration of foo
injected into namespace NC
.
A std::swap
mis-step
Consider the following bad code (Godbolt):
namespace detail {
struct Impl {};
void swap(Impl&, Impl&);
template<class T>
void example(T& x, T& y) {
using namespace std;
swap(x, y);
}
}
It’s bad for three reasons. Number one, it fails to use the hidden friend idiom
for swap(Impl&, Impl&)
. Number two, it uses using namespace std
. (Both of these are
red flags that should insta-fail any code review you do.)
Number three, its use of using namespace
is wrong in terms of the “std::swap
two-step.”
(See “What is the std::swap
two-step?” (2020-07-11).)
In the two-step, we want to bring the meaning of swap
from namespace std
into our current scope,
so that swap(x, y)
will consider that meaning in addition to any meaning assigned
by ADL. But what we’re doing here, instead, is to bring the meaning of swap
(and every
other name in std
) from namespace std
into the least common ancestor namespace of
std
and detail
; i.e., into the global namespace.
So when we try to instantiate example<int>
, unqualified lookup looks for declarations of
swap
in our current scope working outward, and it finds detail::swap(Impl&, Impl&)
and stops
there. It never finds the templated std::swap
declared in the global namespace. Oops!
That’s why the correct version of the std::swap
two-step uses a using-declaration and
not a using-directive.
Using-directives in namespace scope
You can also put a using-directive at namespace scope, like this (Godbolt):
namespace ND::N3 {
int foo(Zoo::Cat);
}
namespace NE {
using namespace ND::N3;
int foo(Zoo::Lion);
}
namespace ND {
namespace N4 {
int test3() {
using namespace NE;
return foo(Zoo::Lion());
}
}
}
Using-directives are “transitive”; since NE
visibly contains a using-directive
for ND::N3
, it’s as if test3
contains both using namespace NE
and using namespace ND::N3
.
In this contrived example, the names from NE
are injected into the least common ancestor
of ND::N4
and NE
(i.e., the global namespace). The names from ND::N3
are injected into the
least common ancestor of ND::N4
and ND::N3
(i.e., ND
). Then, unqualified
lookup for foo
scans outward from ND::N4::test3
, stopping as soon as it finds the declaration of
ND::N3::foo(Cat)
that has been injected into namespace ND
. That declaration hides
the ostensibly better-matching NE::foo(Lion)
that was injected into the global namespace.
Note that the “transitivity” of using-directives applies only to using-directives that are directly
within the using’ed namespace. If test3
had said using namespace NE::N5
, it would have
transitively picked up any using-directives from NE::N5
, but not from NE::N5
’s parent namespace NE
(and not from the global namespace).
And how does all this interact with inline namespace
?
Well, let’s save that for another day.
Some of these examples probably surprised you. I hope the explanations made sense anyway, in retrospect. But is any of this information useful to a good C++ programmer? I would have to say no it is not. Good C++ programmers don’t use using-directives — and I hope this blog post has convinced you to join them!
Don’t use
using namespace
.
As for why using-directives work in this bizarrely unintuitive way, I’ve asked that question over on Stack Overflow. So if you’ve got a good answer, that’s a great place to type it up!