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/99 on 2015-06-24
Revised 2017-10-10
The interface of a C++ class is not constrained to its members or to its definition. When evaluating an API, we must consider definitions beyond the class body that can be as much a part of its interface as its public members.
These external interface points include template specializations like hashers or
traits, nonmember operator overloads (e.g. logging, relationals), and other
canonical nonmember functions designed for use with argument-dependent lookup
(ADL), most notably swap()
.
A few of these are illustrated below for some sample class space::Key
:
namespace space {
class Key { ... };
bool operator==(const Key& a, const Key& b);
bool operator<(const Key& a, const Key& b);
void swap(Key& a, Key& b);
// standard streaming
std::ostream& operator<<(std::ostream& os, const Key& x);
// gTest printing
void PrintTo(const Key& x, std::ostream* os);
// new-style flag extension:
bool ParseFlag(const string& text, Key* dst, string* err);
string UnparseFlag(const Key& v);
} // namespace space
HASH_NAMESPACE_BEGIN
template <>
struct hash<space::Key> {
size_t operator()(const space::Key& x) const;
};
HASH_NAMESPACE_END
There are some important risks associated with making such extensions incorrectly, so this article will try to present some guidance.
Interface points that are functions are usually designed to be found by
argument-dependent lookup (ADL, see TotW 49). Operators and some
operator-like functions (notably swap()
) are designed to be found by ADL. This
protocol only works reliably when the function is defined in a namespace
associated with the type being customized. The associated namespaces include
those of its base classes and class template parameters. A common mistake is to
place these functions in the global namespace. To illustrate the problem,
consider the following code in which good(x)
and bad(x)
functions are called
with identical syntax:
namespace library {
struct Letter {};
void good(Letter);
} // namespace library
// bad is improperly placed in global namespace
void bad(library::Letter);
namespace client {
void good();
void bad();
void test(const library::Letter& x) {
good(x); // ok: 'library::good' is found by ADL.
bad(x); // oops: '::bad' is hidden by 'client::bad'.
}
} // namespace client
Note the difference between library::good()
and ::bad()
. The test()
function is relying on the absence of any function called bad()
in namespaces
enclosing the call site. The appearance of client::bad()
hides ::bad()
from
the test
caller. Meanwhile, the good()
function is found regardless of what
else exists in the test()
function’s enclosing scope. The C++ name lookup
sequence will only yield a global if a name search from closer lexical scopes to
the call site fails to find any names.
This is all very subtle, which is the point, really. It’s all much simpler if we default to defining functions alongside the data on which they operate.
There’s a way to add non-member functions to a class from within the class definition. Friend functions can be defined directly inside a class.
namespace library {
class Key {
public:
explicit Key(string s) : s_(std::move(s)) {}
friend bool operator<(const Key& a, const Key& b) { return a.s_ < b.s_; }
friend bool operator==(const Key& a, const Key& b) { return a.s_ == b.s_; }
friend void swap(Key& a, Key& b) {
swap(a.s_, b.s_);
}
private:
std::string s_;
};
} // namespace library
These friend functions have a special property of ONLY being visible through
ADL. They’re a little strange in that they are defined in the enclosing
namespace, but don’t appear in name lookup there. These in-class friend
definitions must have inlined bodies for this stealth property to kick in. See
“Friend
Definitions”
for more detail.
Such functions will not hide global functions from fragile call sites in their namespace or appear in diagnostic messages for unrelated calls to functions of the same name. They get out of the way, essentially. They’re also very convenient to define, easy to discover, and they have access to class internals. Probably the biggest drawback is that they will not be found in cases where the enclosing class is implicitly convertible from an argument type.
Note that access (i.e. public
, private
, and protected
) has no effect on
friend functions, but it might be polite to place these in the public section
anyway, just so they’ll be more visible during perusal of the public API points.
To avoid violations of the one-definition rule (ODR), any customizations on a
type’s interface should appear where they can’t accidentally be defined multiple
times. This usually means they should be packaged with the type in the same
header. It’s not appropriate to add this sort of nonmember customization in a
*_test.cc
file, or in a “utilities” header off to the side where it can be
overlooked. Force the compiler to see the customization and you will be more
likely to catch violations.
A function overload (including operator overloads) intended as a nonmember extension should be declared in a header that defines one of its arguments.
The same goes for template specializations. Template specializations can be packaged with the primary template definition, or packaged with the type on which it’s being specialized. For partial specializations or with multiple parameters it’s a judgment call. It’s usually pretty clear in practice which site is better. The important thing is that the specialization should not be hidden in client code: it should be as visible as the template and the user-defined types involved.
Don’t customize the behavior of a class in a test. This is dangerous and
unfortunately very common. Test source files are not immune to these dangers and
should follow the same rules as production code. We find a lot of inappropriate
operators in *_test.cc
files that are written with the intent to “get
EXPECT_EQ
to compile” or some other pragmatic concern. Unfortunately they are
still ODR risks (if not violations) and can make library maintenance
difficult. These rogue definitions might even stand in the way of library
maintenance as adding these operators upstream would break the very tests that
needed them and defined their own. For testing there are alternatives available
with a little more effort.
Note that ADL works from the original declaration point for a type. Typedefs, type aliases, alias templates, and using declarations do not create types and have no impact on ADL. This can make it a little tricky to locate the proper placement for customizations, but this cannot be helped, so just do it.
Don’t augment the interface to types generated by protobufs. This is another
common pitfall. You may own the .proto
file, but that doesn’t mean you own the
C++ API it generates, and your augmentations could block improvements to the
generated C++ API. This is an ODR risk because you can’t ensure that your
augmentations are seen whenever the generated header is included.
When defining T
, it may be tempting to define behavior for templates like
std::vector<T>
or std::pair<T,T>
. Though your customizations may take
precedence and may do what you expect, you may be conflicting with other
expected customizations defined on the broader class template.
It’s possible to define some customizations for raw pointers. It may be tempting
in some cases to supply these customizations for T*
along with T
. This is
not advised. It’s dangerous because the customization may conflict with the
expected ordinary behavior of pointers (e.g. the way they’re logged, swapped, or
compared). It’s best to leave pointers alone.
Following these guidelines can be challenging. Much of the inappropriate overloading and specialization seen in C++ code is motivated by a small set of root causes. Below is a partial listing with successful workarounds. If you run into a library API that can’t be worked with, please send a note to the owners to see about adding the appropriate customization hooks. Common APIs should be usable without breaking these interface packaging guidelines.
EXPECT_EQ
, etc.temptation: EXPECT_EQ
requires operator==
, and operator<<
or
GoogleTest’s PrintTo
.
workaround: Write lightweight gmock matchers with
MATCHER_P
instead of relying exclusively on EXPECT_EQ
etc.
workaround: Create local (this is essential) wrapper types you DO truly own and provide customizations on those, possibly using trivial inheritance as a shortcut.
T
as a Container Keytemptation: Container default functor types may rely on operator<
,
operator==
, and hash<T>
.
workaround: Use more custom comparators or custom hashers. Use more typedefs for your associative container types to hide these details from client code.
T
temptation: Defining operator<<
overloads for standard containers.
workaround: Don’t try to log the container directly.
A type’s behavior is not completely defined by its class definition. Non-member definitions and specializations also contribute. You may need to keep reading past that closing brace to really understand how a class works.
Be aware of when and where it’s safe to add these customizations. Adding inappropriate definitions may get your code to work for now, but you could be adding fragility and maintenance blockers for other engineers down the line to deal with.