string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally posted as totw/130 on 2017-02-17
By Titus Winters ([email protected])
The precision of naming takes away from the uniqueness of seeing — Pierre Bonnard
The earliest commit of the Google C++ Style Guide contains the guidance that many people are still using for namespace naming. Roughly, this can be summarized as “namespaces are derived from package paths.” Following on the heels of Java’s package naming requirements, this makes a lot of sense: we want to be able to uniquely identify symbols in C++ and we want there to be uniqueness and consistency in namespace choice.
Except in actuality, we don’t. We just didn’t realize for almost a decade.
Let’s start with how name lookup works in C++ and how it’s different from Java.
namespace foo {
namespace bar {
void f() {
Baz b;
}
}
}
In C++, lookup on an unqualified name (Baz
) will search expanding scopes for a
symbol of the same name: first in f()
(the function), then in bar
, then in
foo
, then in the global namespace.
In Java, there is no such thing as an unqualified symbol: either a symbol is a qualified name:
public void f() {
com.google.foo.bar.Baz b = new com.google.foo.bar.Baz();
}
Or it is imported, either as a single package member or via wildcard:
import com.google.foo.bar.Baz;
import com.google.foo.bar.*;
In no case is Baz
looked for outside of the package that is explicitly
provided: wildcards don’t descend into child packages, nor is search extended
into parent packages. As it turns out, this difference in how parent
packages/namespaces are handled within Java and C++ is fundamental to why
structural namespace naming (making the namespace structure match the package
hierarchy) is a mistake within C++.
The fundamental problem for building namespaces out of packages is that we
rarely rely on fully-qualified lookup in C++, normally writing std::unique_ptr
rather than ::std::unique_ptr
. Coupled with lookup in enclosing namespaces,
this means that for code in a deeply nested package
(::division::section::team::subteam::project
, for example) any symbol that is not
fully qualified (std::unique_ptr
) can in fact reference any of
::std::unique_ptr
::division::std::unique_ptr
::division::section::std::unique_ptr
::division::section::team::std::unique_ptr
::division::section::team::subteam::std::unique_ptr
::division::section::team::subteam::project::std::unique_ptr
And what’s worse: unqualified search starts at the bottom of that list and
stops as soon as there is a namespace match. This means that your build can be
broken if any of your transitive includes add a previously unused namespace that
matches the leading namespace of any symbol you use out of an unqualified
namespace. Strictly speaking, this doesn’t even have to be a build break: if
someone adds something with a matching name and a syntactically-compatible API,
the implementation of that API may be completely incompatible and cause
widespread havoc at runtime. Obviously this isn’t too bad with std
- nobody
should ever be adding a nested namespace std
- but what about more common
namespaces? How about things like testing
?
Names aren’t chosen to be unique. Since teams commonly create local utility
packages to handle common tasks relating to the infrastructure they rely on, we
wind up with local util
and pipeline
packages - and
sub-namespaces. This is a recipe for unnecessary and unintended collisions.
For comparison, the problem in Java is far reduced: if you wildcard-import from two packages in Java and one adds a new symbol with the same name as the other package, your build can break. This is easily and completely solved by forbidding wildcard imports as is done in many Java styles.
There are two features that prevent this build-break-at-a-distance:
search::foo::bar
) matches any top-level namespace
(::bar
) or a sub-namespace of any parent of that leaf (search::bar
), no
name collisions will occur.There are (at least) three ways to achieve this:
::
on every symbol.search::bar
if there is a ::bar
or a search::foo::bar
.The current style guide suggests the last
option, but
allows for the old style (namespaces match package names) if necessary. This is
largely because the Google didn’t want to cause too much anxiety or
trigger anyone re-namespacing things. That said, if we had it to do over again
in a fresh codebase we would unambiguously say this: one top-level namespace for
public interfaces per project. Ensure uniqueness of namespaces via a common
database. Thus we get (only) top-level namespaces like absl
, and can have no
ambiguity in lookup (barring collision between local symbols and those in the
global namespace, but modern rules discourage the global namespace anyway).
Because there is so much code that existed before this change, and so much code
following the old pattern even after this change, we find ourselves in a sort of
half-way space, with some namespaces that often need to be fully qualified
(::util
), and some that are obviously unique and never need to be (std
).
I regularly hear people express that small/nested namespaces “keep things
organized.” Putting things in their place feels right - why lump together
something like StrCat()
and make_unique()
other than being in Abseil these
have nothing to do with one another! Wouldn’t an absl::strings::utilities
namespace help differentiate from absl::smart_ptrs
?
In other languages this would probably be good - better organization with no downside. However, because of how lookup works (expanding into successive layers of containing namespace scopes) your fine-grained namespace is impacted by every symbol (and sub-namespace) added in every parent namespace. That is: while you don’t exactly “contain” the names from parent namespaces, name/namespace collisions matter nearly as much as if you do. Small/deeply-nested namespaces don’t shield you from this, they exacerbate it.
Practically speaking, the following is the best we can do given the realities of most codebases:
absl
, testing
, util
, etc. Try to give
sub-namespaces unique names that are unlikely to collide with future
top-levels.util
or other commonly-abused namespaces, try to avoid full
qualification, but qualify if necessary.The advice in TotW 119 also helps, for .cc
files: our concern with
fully-qualifying is not that it is bad, but that it is weird compared to C++
code in the rest of the world. Limited use in using-declarations strikes an
acceptable balance. However, even complete adherence to this suggestion doesn’t
fully mitigate the dangers from unqualified name lookup because we still have
header files and don’t want to fully qualify every symbol in every header.