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 Declarationsexplicit
Originally posted as TotW #142 on January 29, 2018
Updated 2020-04-06
Quicklink: abseil.io/tips/142
“Explicit is better than implicit.” – PEP 20
Most constructors should be explicit
.
Prior to C++11, the explicit
keyword was meaningful only for constructors that
could be called with a single argument, and our style guide required its use for
such constructors so that they did not act as “converting constructors”. That
requirement was not applied for multi-parameter constructors. Indeed the style
guide used to discourage use of explicit
for multi-parameter constructors as
it had no meaning. That’s no longer the case.
In C++11, explicit
became meaningful for copy-initialization from braced
lists, such as when calling a function void f(std::pair<int, int>)
with f({1,
2})
or when initializing a variable bad
with std::vector<char> bad =
{"hello", "world"};
.
Wait a minute! In that last example the types don’t match. That can’t compile,
can it? std::vector<std::string> good = {"hello", "world"};
would be
reasonable, but a std::vector<char>
can’t hold two std::string
s. And yet it
does compile (or at least it does with all current C++ compilers). What’s up
with that? Let’s come back to that later, after we’ve talked more about
explicit
.
Constructors that aren’t marked as explicit
can be invoked by the compiler to
create a value without mentioning the type’s name. That’s great when we have the
value we want already but the types just don’t quite match – we have a const
char[]
and we need a std::string
, maybe, or we have two std::string
s and we
want a std::vector<std::string>
, or we have an int
and we want a BigNum
.
In short, it works well if the value is essentially the same before and after
the conversion.
// The coordinates of a point in the Cartesian plane, w.r.t. some basis. class Coordinate2D { public: Coordinate2D(double x, double y); // ... }; // Computes the Euclidean norm of a given point `p`. double EuclideanNorm(Coordinate2D p); // Uses of the non-explicit constructor: double norm = EuclideanNorm({3.0, 4.0}); // passing a function argument Coordinate2D origin = {0.0, 0.0}; // initializing with `=` Coordinate2D Translate(Coordinate2D p, Vector2D v) { return {p.x() + v.x(), p.y() + v.y()}; // returning a value from a function }
By declaring the constructor Coordinate2D(double, double)
without explicit
we allow for passing {3.0, 4.0}
to a function that takes a Coordinate2D
parameter. Given that {3.0, 4.0}
is a perfectly reasonable value for such an
object, no confusion arises from this affordance.
Implicitly calling a constructor isn’t such a good idea if its output is a different value than its input, or if it might have preconditions.
Consider a Request
class with a constructor Request(Server*, Connection*)
.
There’s no sense in which the value of the request object “is” the server and
connection — it’s just that we can create a request that uses them. There could
be many semantically different types that can be constructed from {server,
connection}
, such as a Response
. Such a constructor should be explicit
, so
that we can’t pass {server, connection}
to a function that accepts a Request
(or Response
) parameter. In such cases marking the constructor as explicit
makes the code clearer for readers by requiring the target type to be named when
it’s instantiated and helps to avoid bugs caused by unintended conversions.
// A line is defined by two distinct points. class Line { public: // Constructs the line passing through the given points. // REQUIRES: p1 != p2 explicit Line(Coordinate2D p1, Coordinate2D p2); // Determines whether this line contain a given point `p`. bool ContainsPoint(Coordinate2D p) const; }; Line line({0, 0}, {42, 1729}); // Computes the gradient of `line`. Returns an infinite value if `line` is // vertical. double Gradient(const Line& line);
By declaring the constructor Line(Coordinate2D, Coordinate2D)
as explicit
,
we prevent code from passing unrelated points to Gradient without first
deliberately turning them into a Line
object. The “value” of a Line
isn’t
the two points, and not any two points define a Line
.
As another example, the use of explicit
for ownership transfer from a raw
pointer in std::unique_ptr
prevents a double-delete
bug here:
std::vector<std::unique_ptr<int>> v; int* p = new int(-1); v.push_back(p); // error: cannot convert int* to std::unique_ptr<int> // ... v.push_back(p);
To pass a std::unique_ptr
the programmer must explicitly create one, which
leaves a visual clue for readers that ownership is being transferred. The
explicit
constructor helps to document the constraint that the raw pointer
must own its target.
explicit
.explicit
unless its arguments “are” the value of the
newly created object. (Note:
the Google style guide
currently requires all single-argument constructors to be explicit
).explicit
.explicit
. Sometimes those are better implemented
as factory functions (see
Tip #42: Prefer Factory Functions to Initializer Methods).This tip can be viewed as the flip-side of
Tip #88: Initialization: =, (), and {},
which advises us to use copy-initialization syntax (with =
) when initializing
from “the intended literal value”. The advice of the present tip is to omit
explicit
only when tip 88 says to use copy initialization (which would
otherwise be disallowed by the explicit
keyword).
Finally, a word of warning: The C++ Standard Library doesn’t always get this
right. Going back to our example (that mistakenly declared a std::vector<char>
rather than a container of strings):
std::vector<char> bad = {"hello", "world"};
we find that std::vector
has a templated “range” constructor taking a pair of
iterators, which matches here and deduces the parameter types as const char*
.
If the recommendations in this tip were applied, that constructor would be
explicit
, because the value of a std::vector<char>
is not two iterators
(rather, it’s a sequence of characters). As it is, explicit
was omitted, and
this example code gives undefined behavior as the second iterator is not
reachable from the first.