Tip of the Week #142: Multi-parameter Constructors and explicit

Originally posted as TotW #142 on January 29, 2018

By James Dennett

Updated 2020-04-06

Quicklink: abseil.io/tips/142

“Explicit is better than implicit.” – PEP 20

TL;DR:

Most constructors should be explicit.

Introduction

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::strings. 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 Change Types, But Not Values

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::strings 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.

Constructors That Do More

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.

Recommendations

  • Copy constructors and move constructors should never be explicit.
  • Make a constructor 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).
  • In particular, constructors for types where identity (address) is relevant to the value should be explicit.
  • Constructors that impose additional constraints on values (i.e., that have preconditions) should be explicit. Sometimes those are better implemented as factory functions (see Tip #42: Prefer Factory Functions to Initializer Methods).

Closing Remarks

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.


Subscribe to the Abseil Blog