C: Classes and class hierarchies
A class is a user-defined type, for which a programmer can define the representation, operations, and interfaces. Class hierarchies are used to organize related classes into hierarchical structures.
Class rule summary:
- (C.1: Organize related data into structures (
struct
s orclass
es)) - (C.2: Use
class
if the class has an invariant; usestruct
if the data members can vary independently) - (C.3: Represent the distinction between an interface and an implementation using a class)
- (C.4: Make a function a member only if it needs direct access to the representation of a class)
- (C.5: Place helper functions in the same namespace as the class they support)
- (C.7: Don't define a class or enum and declare a variable of its type in the same statement)
- (C.8: Use
class
rather thanstruct
if any member is non-public) - (C.9: Minimize exposure of members)
Subsections:
- (C.concrete: Concrete types)
- (C.ctor: Constructors, assignments, and destructors)
- (C.con: Containers and other resource handles)
- (C.lambdas: Function objects and lambdas)
- (C.hier: Class hierarchies (OOP))
- (C.over: Overloading and overloaded operators)
- (C.union: Unions)
C.1: Organize related data into structures (struct
s or class
es)
Reason
Ease of comprehension. If data is related (for fundamental reasons), that fact should be reflected in code.
Example
void draw(int x, int y, int x2, int y2); // BAD: unnecessary implicit relationshipsvoid draw(Point from, Point to); // better
Note
A simple class without virtual functions implies no space or time overhead.
Note
From a language perspective class
and struct
differ only in the default visibility of their members.
Enforcement
Probably impossible. Maybe a heuristic looking for data items used together is possible.
C.2: Use class
if the class has an invariant; use struct
if the data members can vary independently
Reason
Readability.
Ease of comprehension.
The use of class
alerts the programmer to the need for an invariant.
This is a useful convention.
Note
An invariant is a logical condition for the members of an object that a constructor must establish for the public member functions to assume.
After the invariant is established (typically by a constructor) every member function can be called for the object.
An invariant can be stated informally (e.g., in a comment) or more formally using Expects
.
If all data members can vary independently of each other, no invariant is possible.
Example
struct Pair { // the members can vary independently string name; int volume;};
but:
class Date {public: // validate that {yy, mm, dd} is a valid date and initialize Date(int yy, Month mm, char dd); // ...private: int y; Month m; char d; // day};
Note
If a class has any private
data, a user cannot completely initialize an object without the use of a constructor.
Hence, the class definer will provide a constructor and must specify its meaning.
This effectively means the definer need to define an invariant.
See also:
- (define a class with private data as
class
) - (Prefer to place the interface first in a class)
- (minimize exposure of members)
- (Avoid
protected
data)
Enforcement
Look for struct
s with all data private and class
es with public members.
C.3: Represent the distinction between an interface and an implementation using a class
Reason
An explicit distinction between interface and implementation improves readability and simplifies maintenance.
Example
class Date {public: Date(); // validate that {yy, mm, dd} is a valid date and initialize Date(int yy, Month mm, char dd); int day() const; Month month() const; // ...private: // ... some representation ...};
For example, we can now change the representation of a Date
without affecting its users (recompilation is likely, though).
Note
Using a class in this way to represent the distinction between interface and implementation is of course not the only way. For example, we can use a set of declarations of freestanding functions in a namespace, an abstract base class, or a function template with concepts to represent an interface. The most important issue is to explicitly distinguish between an interface and its implementation "details." Ideally, and typically, an interface is far more stable than its implementation(s).
Enforcement
???
C.4: Make a function a member only if it needs direct access to the representation of a class
Reason
Less coupling than with member functions, fewer functions that can cause trouble by modifying object state, reduces the number of functions that needs to be modified after a change in representation.
Example
class Date { // ... relatively small interface ...};// helper functions:Date next_weekday(Date);bool operator==(Date, Date);
The "helper functions" have no need for direct access to the representation of a Date
.
Note
This rule becomes even better if C++ gets "uniform function call".
Exception
The language requires virtual
functions to be members, and not all virtual
functions directly access data.
In particular, members of an abstract class rarely do.
Note multi-methods.
Exception
The language requires operators =
, ()
, []
, and ->
to be members.
Exception
An overload set could have some members that do not directly access private
data:
class Foobar {public: void foo(long x) { /* manipulate private data */ } void foo(double x) { foo(std::lround(x)); } // ...private: // ...};
Exception
Similarly, a set of functions could be designed to be used in a chain:
x.scale(0.5).rotate(45).set_color(Color::red);
Typically, some but not all of such functions directly access private
data.
Enforcement
- Look for non-
virtual
member functions that do not touch data members directly. The snag is that many member functions that do not need to touch data members directly do. - Ignore
virtual
functions. - Ignore functions that are part of an overload set out of which at least one function accesses
private
members. - Ignore functions returning
this
.
C.5: Place helper functions in the same namespace as the class they support
Reason
A helper function is a function (usually supplied by the writer of a class) that does not need direct access to the representation of the class, yet is seen as part of the useful interface to the class. Placing them in the same namespace as the class makes their relationship to the class obvious and allows them to be found by argument dependent lookup.
Example
namespace Chrono { // here we keep time-related services class Time { /* ... */ }; class Date { /* ... */ }; // helper functions: bool operator==(Date, Date); Date next_weekday(Date); // ...}
Note
This is especially important for (overloaded operators).
Enforcement
- Flag global functions taking argument types from a single namespace.
C.7: Don't define a class or enum and declare a variable of its type in the same statement
Reason
Mixing a type definition and the definition of another entity in the same declaration is confusing and unnecessary.
Example, bad
struct Data { /*...*/ } data{ /*...*/ };
Example, good
struct Data { /*...*/ };Data data{ /*...*/ };
Enforcement
- Flag if the
}
of a class or enumeration definition is not followed by a;
. The;
is missing.
C.8: Use class
rather than struct
if any member is non-public
Reason
Readability. To make it clear that something is being hidden/abstracted. This is a useful convention.
Example, bad
struct Date { int d, m; Date(int i, Month m); // ... lots of functions ...private: int y; // year};
There is nothing wrong with this code as far as the C++ language rules are concerned, but nearly everything is wrong from a design perspective. The private data is hidden far from the public data. The data is split in different parts of the class declaration. Different parts of the data have different access. All of this decreases readability and complicates maintenance.
Note
Prefer to place the interface first in a class, (see NL.16).
Enforcement
Flag classes declared with struct
if there is a private
or protected
member.
C.9: Minimize exposure of members
Reason
Encapsulation. Information hiding. Minimize the chance of unintended access. This simplifies maintenance.
Example
template<typename T, typename U>struct pair { T a; U b; // ...};
Whatever we do in the //
-part, an arbitrary user of a pair
can arbitrarily and independently change its a
and b
.
In a large code base, we cannot easily find which code does what to the members of pair
.
This might be exactly what we want, but if we want to enforce a relation among members, we need to make them private
and enforce that relation (invariant) through constructors and member functions.
For example:
class Distance {public: // ... double meters() const { return magnitude*unit; } void set_unit(double u) { // ... check that u is a factor of 10 ... // ... change magnitude appropriately ... unit = u; } // ...private: double magnitude; double unit; // 1 is meters, 1000 is kilometers, 0.001 is millimeters, etc.};
Note
If the set of direct users of a set of variables cannot be easily determined, the type or usage of that set cannot be (easily) changed/improved.
For public
and protected
data, that's usually the case.
Example
A class can provide two interfaces to its users.
One for derived classes (protected
) and one for general users (public
).
For example, a derived class might be allowed to skip a run-time check because it has already guaranteed correctness:
class Foo {public: int bar(int x) { check(x); return do_bar(x); } // ...protected: int do_bar(int x); // do some operation on the data // ...private: // ... data ...};class Dir : public Foo { //... int mem(int x, int y) { /* ... do something ... */ return do_bar(x + y); // OK: derived class can bypass check }};void user(Foo& x){ int r1 = x.bar(1); // OK, will check int r2 = x.do_bar(2); // error: would bypass check // ...}
Note
(protected
data is a bad idea).
Note
Prefer the order public
members before protected
members before private
members; see (NL.16).
Enforcement
- (Flag protected data).
- Flag mixtures of
public
andprivate
data
C.concrete: Concrete types
Concrete type rule summary:
- (C.10: Prefer concrete types over class hierarchies)
- (C.11: Make concrete types regular)
- (C.12: Don't make data members
const
or references)
C.10: Prefer concrete types over class hierarchies
Reason
A concrete type is fundamentally simpler than a type in a class hierarchy: easier to design, easier to implement, easier to use, easier to reason about, smaller, and faster. You need a reason (use cases) for using a hierarchy.
Example
class Point1 { int x, y; // ... operations ... // ... no virtual functions ...};class Point2 { int x, y; // ... operations, some virtual ... virtual ~Point2();};void use(){ Point1 p11 {1, 2}; // make an object on the stack Point1 p12 {p11}; // a copy auto p21 = make_unique<Point2>(1, 2); // make an object on the free store auto p22 = p21->clone(); // make a copy // ...}
If a class is part of a hierarchy, we (in real code if not necessarily in small examples) must manipulate its objects through pointers or references. That implies more memory overhead, more allocations and deallocations, and more run-time overhead to perform the resulting indirections.
Note
Concrete types can be stack-allocated and be members of other classes.
Note
The use of indirection is fundamental for run-time polymorphic interfaces. The allocation/deallocation overhead is not (that's just the most common case). We can use a base class as the interface of a scoped object of a derived class. This is done where dynamic allocation is prohibited (e.g. hard-real-time) and to provide a stable interface to some kinds of plug-ins.
Enforcement
???
C.11: Make concrete types regular
Reason
Regular types are easier to understand and reason about than types that are not regular (irregularities requires extra effort to understand and use).
The C++ built-in types are regular, and so are standard-library classes such as string
, vector
, and map
. Concrete classes without assignment and equality can be defined, but they are (and should be) rare.
Example
struct Bundle { string name; vector<Record> vr;};bool operator==(const Bundle& a, const Bundle& b){ return a.name == b.name && a.vr == b.vr;}Bundle b1 { "my bundle", {r1, r2, r3}};Bundle b2 = b1;if (!(b1 == b2)) error("impossible!");b2.name = "the other bundle";if (b1 == b2) error("No!");
In particular, if a concrete type is copyable, prefer to also give it an equality comparison operator, and ensure that a = b
implies a == b
.
Note
For structs intended to be shared with C code, defining operator==
may not be feasible.
Note
Handles for resources that cannot be cloned, e.g., a scoped_lock
for a mutex
, are concrete types but typically cannot be copied (instead, they can usually be moved),
so they can't be regular; instead, they tend to be move-only.
Enforcement
???
C.12: Don't make data members const
or references
Reason
They are not useful, and make types difficult to use by making them either uncopyable or partially uncopyable for subtle reasons.
Example; bad
class bad { const int i; // bad string& s; // bad // ...};
The const
and &
data members make this class "only-sort-of-copyable" -- copy-constructible but not copy-assignable.
Note
If you need a member to point to something, use a pointer (raw or smart, and gsl::not_null
if it should not be null) instead of a reference.
Enforcement
Flag a data member that is const
, &
, or &&
.
C.ctor: Constructors, assignments, and destructors
These functions control the lifecycle of objects: creation, copy, move, and destruction. Define constructors to guarantee and simplify initialization of classes.
These are default operations:
- a default constructor:
X()
- a copy constructor:
X(const X&)
- a copy assignment:
operator=(const X&)
- a move constructor:
X(X&&)
- a move assignment:
operator=(X&&)
- a destructor:
~X()
By default, the compiler defines each of these operations if it is used, but the default can be suppressed.
The default operations are a set of related operations that together implement the lifecycle semantics of an object. By default, C++ treats classes as value-like types, but not all types are value-like.
Set of default operations rules:
- (C.20: If you can avoid defining any default operations, do)
- (C.21: If you define or
=delete
any copy, move, or destructor function, define or=delete
them all) - (C.22: Make default operations consistent)
Destructor rules:
- (C.30: Define a destructor if a class needs an explicit action at object destruction)
- (C.31: All resources acquired by a class must be released by the class's destructor)
- (C.32: If a class has a raw pointer (
T*
) or reference (T&
), consider whether it might be owning) - (C.33: If a class has an owning pointer member, define a destructor)
- (C.35: A base class destructor should be either public and virtual, or protected and non-virtual)
- (C.36: A destructor must not fail)
- (C.37: Make destructors
noexcept
)
Constructor rules:
- (C.40: Define a constructor if a class has an invariant)
- (C.41: A constructor should create a fully initialized object)
- (C.42: If a constructor cannot construct a valid object, throw an exception)
- (C.43: Ensure that a copyable class has a default constructor)
- (C.44: Prefer default constructors to be simple and non-throwing)
- (C.45: Don't define a default constructor that only initializes data members; use member initializers instead)
- (C.46: By default, declare single-argument constructors
explicit
) - (C.47: Define and initialize member variables in the order of member declaration)
- (C.48: Prefer in-class initializers to member initializers in constructors for constant initializers)
- (C.49: Prefer initialization to assignment in constructors)
- (C.50: Use a factory function if you need "virtual behavior" during initialization)
- (C.51: Use delegating constructors to represent common actions for all constructors of a class)
- (C.52: Use inheriting constructors to import constructors into a derived class that does not need further explicit initialization)
Copy and move rules:
- (C.60: Make copy assignment non-
virtual
, take the parameter byconst&
, and return by non-const&
) - (C.61: A copy operation should copy)
- (C.62: Make copy assignment safe for self-assignment)
- (C.63: Make move assignment non-
virtual
, take the parameter by&&
, and return by non-const&
) - (C.64: A move operation should move and leave its source in a valid state)
- (C.65: Make move assignment safe for self-assignment)
- (C.66: Make move operations
noexcept
) - (C.67: A polymorphic class should suppress public copy/move)
Other default operations rules:
- (C.80: Use
=default
if you have to be explicit about using the default semantics) - (C.81: Use
=delete
when you want to disable default behavior (without wanting an alternative)) - (C.82: Don't call virtual functions in constructors and destructors)
- (C.83: For value-like types, consider providing a
noexcept
swap function) - (C.84: A
swap
must not fail) - (C.85: Make
swap
noexcept
) - (C.86: Make
==
symmetric with respect of operand types andnoexcept
) - (C.87: Beware of
==
on base classes) - (C.89: Make a
hash
noexcept
) - (C.90: Rely on constructors and assignment operators, not memset and memcpy)
C.defop: Default Operations
By default, the language supplies the default operations with their default semantics. However, a programmer can disable or replace these defaults.
C.20: If you can avoid defining default operations, do
Reason
It's the simplest and gives the cleanest semantics.
Example
struct Named_map {public: // ... no default operations declared ...private: string name; map<int, int> rep;};Named_map nm; // default constructNamed_map nm2 {nm}; // copy construct
Since std::map
and string
have all the special functions, no further work is needed.
Note
This is known as "the rule of zero".
Enforcement
(Not enforceable) While not enforceable, a good static analyzer can detect patterns that indicate a possible improvement to meet this rule.
For example, a class with a (pointer, size) pair of member and a destructor that delete
s the pointer could probably be converted to a vector
.
C.21: If you define or =delete
any copy, move, or destructor function, define or =delete
them all
Reason
The semantics of copy, move, and destruction are closely related, so if one needs to be declared, the odds are that others need consideration too.
Declaring any copy/move/destructor function,
even as =default
or =delete
, will suppress the implicit declaration
of a move constructor and move assignment operator.
Declaring a move constructor or move assignment operator, even as
=default
or =delete
, will cause an implicitly generated copy constructor
or implicitly generated copy assignment operator to be defined as deleted.
So as soon as any of these are declared, the others should
all be declared to avoid unwanted effects like turning all potential moves
into more expensive copies, or making a class move-only.
Example, bad
struct M2 { // bad: incomplete set of copy/move/destructor operationspublic: // ... // ... no copy or move operations ... ~M2() { delete[] rep; }private: pair<int, int>* rep; // zero-terminated set of pairs};void use(){ M2 x; M2 y; // ... x = y; // the default assignment // ...}
Given that "special attention" was needed for the destructor (here, to deallocate), the likelihood that the implicitly-defined copy and move assignment operators will be correct is low (here, we would get double deletion).
Note
This is known as "the rule of five."
Note
If you want a default implementation (while defining another), write =default
to show you're doing so intentionally for that function.
If you don't want a generated default function, suppress it with =delete
.
Example, good
When a destructor needs to be declared just to make it virtual
, it can be
defined as defaulted.
class AbstractBase {public: virtual ~AbstractBase() = default; // ...};
To prevent slicing as per (C.67),
make the copy and move operations protected or =delete
d, and add a clone
:
class ClonableBase {public: virtual unique_ptr<ClonableBase> clone() const; virtual ~ClonableBase() = default; CloneableBase() = default; ClonableBase(const ClonableBase&) = delete; ClonableBase& operator=(const ClonableBase&) = delete; ClonableBase(ClonableBase&&) = delete; ClonableBase& operator=(ClonableBase&&) = delete; // ... other constructors and functions ...};
Defining only the move operations or only the copy operations would have the same effect here, but stating the intent explicitly for each special member makes it more obvious to the reader.
Note
Compilers enforce much of this rule and ideally warn about any violation.
Note
Relying on an implicitly generated copy operation in a class with a destructor is deprecated.
Note
Writing these functions can be error prone. Note their argument types:
class X {public: // ... virtual ~X() = default; // destructor (virtual if X is meant to be a base class) X(const X&) = default; // copy constructor X& operator=(const X&) = default; // copy assignment X(X&&) = default; // move constructor X& operator=(X&&) = default; // move assignment};
A minor mistake (such as a misspelling, leaving out a const
, using &
instead of &&
, or leaving out a special function) can lead to errors or warnings.
To avoid the tedium and the possibility of errors, try to follow the (rule of zero).
Enforcement
(Simple) A class should have a declaration (even a =delete
one) for either all or none of the copy/move/destructor functions.
C.22: Make default operations consistent
Reason
The default operations are conceptually a matched set. Their semantics are interrelated. Users will be surprised if copy/move construction and copy/move assignment do logically different things. Users will be surprised if constructors and destructors do not provide a consistent view of resource management. Users will be surprised if copy and move don't reflect the way constructors and destructors work.
Example, bad
class Silly { // BAD: Inconsistent copy operations class Impl { // ... }; shared_ptr<Impl> p;public: Silly(const Silly& a) : p(make_shared<Impl>()) { *p = *a.p; } // deep copy Silly& operator=(const Silly& a) { p = a.p; } // shallow copy // ...};
These operations disagree about copy semantics. This will lead to confusion and bugs.
Enforcement
- (Complex) A copy/move constructor and the corresponding copy/move assignment operator should write to the same member variables at the same level of dereference.
- (Complex) Any member variables written in a copy/move constructor should also be initialized by all other constructors.
- (Complex) If a copy/move constructor performs a deep copy of a member variable, then the destructor should modify the member variable.
- (Complex) If a destructor is modifying a member variable, that member variable should be written in any copy/move constructors or assignment operators.
C.dtor: Destructors
"Does this class need a destructor?" is a surprisingly insightful design question. For most classes the answer is "no" either because the class holds no resources or because destruction is handled by (the rule of zero); that is, its members can take care of themselves as concerns destruction. If the answer is "yes", much of the design of the class follows (see (the rule of five)).
C.30: Define a destructor if a class needs an explicit action at object destruction
Reason
A destructor is implicitly invoked at the end of an object's lifetime. If the default destructor is sufficient, use it. Only define a non-default destructor if a class needs to execute code that is not already part of its members' destructors.
Example
template<typename A>struct final_action { // slightly simplified A act; final_action(A a) : act{a} {} ~final_action() { act(); }};template<typename A>final_action<A> finally(A act) // deduce action type{ return final_action<A>{act};}void test(){ auto act = finally([] { cout << "Exit test\n"; }); // establish exit action // ... if (something) return; // act done here // ...} // act done here
The whole purpose of final_action
is to get a piece of code (usually a lambda) executed upon destruction.
Note
There are two general categories of classes that need a user-defined destructor:
- A class with a resource that is not already represented as a class with a destructor, e.g., a
vector
or a transaction class. - A class that exists primarily to execute an action upon destruction, such as a tracer or
final_action
.
Example, bad
class Foo { // bad; use the default destructorpublic: // ... ~Foo() { s = ""; i = 0; vi.clear(); } // clean upprivate: string s; int i; vector<int> vi;};
The default destructor does it better, more efficiently, and can't get it wrong.
Note
If the default destructor is needed, but its generation has been suppressed (e.g., by defining a move constructor), use =default
.
Enforcement
Look for likely "implicit resources", such as pointers and references. Look for classes with destructors even though all their data members have destructors.
C.31: All resources acquired by a class must be released by the class's destructor
Reason
Prevention of resource leaks, especially in error cases.
Note
For resources represented as classes with a complete set of default operations, this happens automatically.
Example
class X { ifstream f; // might own a file // ... no default operations defined or =deleted ...};
X
's ifstream
implicitly closes any file it might have open upon destruction of its X
.
Example, bad
class X2 { // bad FILE* f; // might own a file // ... no default operations defined or =deleted ...};
X2
might leak a file handle.
Note
What about a socket that won't close? A destructor, close, or cleanup operation (should never fail). If it does nevertheless, we have a problem that has no really good solution. For starters, the writer of a destructor does not know why the destructor is called and cannot "refuse to act" by throwing an exception. See (discussion). To make the problem worse, many "close/release" operations are not retryable. Many have tried to solve this problem, but no general solution is known. If at all possible, consider failure to close/cleanup a fundamental design error and terminate.
Note
A class can hold pointers and references to objects that it does not own.
Obviously, such objects should not be delete
d by the class's destructor.
For example:
Preprocessor pp { /* ... */ };Parser p { pp, /* ... */ };Type_checker tc { p, /* ... */ };
Here p
refers to pp
but does not own it.
Enforcement
- (Simple) If a class has pointer or reference member variables that are owners
(e.g., deemed owners by using
gsl::owner
), then they should be referenced in its destructor. - (Hard) Determine if pointer or reference member variables are owners when there is no explicit statement of ownership (e.g., look into the constructors).
C.32: If a class has a raw pointer (T*
) or reference (T&
), consider whether it might be owning
Reason
There is a lot of code that is non-specific about ownership.
Example
class legacy_class{ foo* m_owning; // Bad: change to unique_ptr<T> or owner<T*> bar* m_observer; // OK: keep}
The only way to determine ownership may be code analysis.
Note
Ownership should be clear in new code (and refactored legacy code) according to (R.20) for owning pointers and ((R.3) for non-owning pointers. References should never own R.4).
Enforcement
Look at the initialization of raw member pointers and member references and see if an allocation is used.
C.33: If a class has an owning pointer member, define a destructor
Reason
An owned object must be deleted
upon destruction of the object that owns it.
Example
A pointer member could represent a resource.
(A T*
should not do so), but in older code, that's common.
Consider a T*
a possible owner and therefore suspect.
template<typename T>class Smart_ptr { T* p; // BAD: vague about ownership of *p // ...public: // ... no user-defined default operations ...};void use(Smart_ptr<int> p1){ // error: p2.p leaked (if not nullptr and not owned by some other code) auto p2 = p1;}
Note that if you define a destructor, you must define or delete (all default operations):
template<typename T>class Smart_ptr2 { T* p; // BAD: vague about ownership of *p // ...public: // ... no user-defined copy operations ... ~Smart_ptr2() { delete p; } // p is an owner!};void use(Smart_ptr2<int> p1){ auto p2 = p1; // error: double deletion}
The default copy operation will just copy the p1.p
into p2.p
leading to a double destruction of p1.p
. Be explicit about ownership:
template<typename T>class Smart_ptr3 { owner<T*> p; // OK: explicit about ownership of *p // ...public: // ... // ... copy and move operations ... ~Smart_ptr3() { delete p; }};void use(Smart_ptr3<int> p1){ auto p2 = p1; // OK: no double deletion}
Note
Often the simplest way to get a destructor is to replace the pointer with a smart pointer (e.g., std::unique_ptr
) and let the compiler arrange for proper destruction to be done implicitly.
Note
Why not just require all owning pointers to be "smart pointers"? That would sometimes require non-trivial code changes and might affect ABIs.
Enforcement
- A class with a pointer data member is suspect.
- A class with an
owner<T>
should define its default operations.
C.35: A base class destructor should be either public and virtual, or protected and non-virtual
Reason
To prevent undefined behavior. If the destructor is public, then calling code can attempt to destroy a derived class object through a base class pointer, and the result is undefined if the base class's destructor is non-virtual. If the destructor is protected, then calling code cannot destroy through a base class pointer and the destructor does not need to be virtual; it does need to be protected, not private, so that derived destructors can invoke it. In general, the writer of a base class does not know the appropriate action to be done upon destruction.
Discussion
See (this in the Discussion section).
Example, bad
struct Base { // BAD: implicitly has a public non-virtual destructor virtual void f();};struct D : Base { string s {"a resource needing cleanup"}; ~D() { /* ... do some cleanup ... */ } // ...};void use(){ unique_ptr<Base> p = make_unique<D>(); // ...} // p's destruction calls ~Base(), not ~D(), which leaks D::s and possibly more
Note
A virtual function defines an interface to derived classes that can be used without looking at the derived classes. If the interface allows destroying, it should be safe to do so.
Note
A destructor must be non-private or it will prevent using the type:
class X { ~X(); // private destructor // ...};void use(){ X a; // error: cannot destroy auto p = make_unique<X>(); // error: cannot destroy}
Exception
We can imagine one case where you could want a protected virtual destructor: When an object of a derived type (and only of such a type) should be allowed to destroy another object (not itself) through a pointer to base. We haven't seen such a case in practice, though.
Enforcement
- A class with any virtual functions should have a destructor that is either public and virtual or else protected and non-virtual.
- If a class inherits publicly from a base class, the base class should have a destructor that is either public and virtual or else protected and non-virtual.
C.36: A destructor must not fail
Reason
In general we do not know how to write error-free code if a destructor should fail. The standard library requires that all classes it deals with have destructors that do not exit by throwing.
Example
class X {public: ~X() noexcept; // ...};X::~X() noexcept{ // ... if (cannot_release_a_resource) terminate(); // ...}
Note
Many have tried to devise a fool-proof scheme for dealing with failure in destructors. None have succeeded to come up with a general scheme. This can be a real practical problem: For example, what about a socket that won't close? The writer of a destructor does not know why the destructor is called and cannot "refuse to act" by throwing an exception. See (discussion). To make the problem worse, many "close/release" operations are not retryable. If at all possible, consider failure to close/cleanup a fundamental design error and terminate.
Note
Declare a destructor noexcept
. That will ensure that it either completes normally or terminates the program.
Note
If a resource cannot be released and the program must not fail, try to signal the failure to the rest of the system somehow (maybe even by modifying some global state and hope something will notice and be able to take care of the problem). Be fully aware that this technique is special-purpose and error-prone. Consider the "my connection will not close" example. Probably there is a problem at the other end of the connection and only a piece of code responsible for both ends of the connection can properly handle the problem. The destructor could send a message (somehow) to the responsible part of the system, consider that to have closed the connection, and return normally.
Note
If a destructor uses operations that could fail, it can catch exceptions and in some cases still complete successfully (e.g., by using a different clean-up mechanism from the one that threw an exception).
Enforcement
(Simple) A destructor should be declared noexcept
if it could throw.
C.37: Make destructors noexcept
Reason
(A destructor must not fail). If a destructor tries to exit with an exception, it's a bad design error and the program had better terminate.
Note
A destructor (either user-defined or compiler-generated) is implicitly declared noexcept
(independently of what code is in its body) if all of the members of its class have noexcept
destructors. By explicitly marking destructors noexcept
, an author guards against the destructor becoming implicitly noexcept(false)
through the addition or modification of a class member.
Example
Not all destructors are noexcept by default; one throwing member poisons the whole class hierarchy
struct X { Details x; // happens to have a throwing destructor // ... ~X() { } // implicitly noexcept(false); aka can throw};
So, if in doubt, declare a destructor noexcept.
Note
Why not then declare all destructors noexcept? Because that would in many cases -- especially simple cases -- be distracting clutter.
Enforcement
(Simple) A destructor should be declared noexcept
if it could throw.
C.ctor: Constructors
A constructor defines how an object is initialized (constructed).
C.40: Define a constructor if a class has an invariant
Reason
That's what constructors are for.
Example
class Date { // a Date represents a valid date // in the January 1, 1900 to December 31, 2100 range Date(int dd, int mm, int yy) :d{dd}, m{mm}, y{yy} { if (!is_valid(d, m, y)) throw Bad_date{}; // enforce invariant } // ...private: int d, m, y;};
It is often a good idea to express the invariant as an Ensures
on the constructor.
Note
A constructor can be used for convenience even if a class does not have an invariant. For example:
struct Rec { string s; int i {0}; Rec(const string& ss) : s{ss} {} Rec(int ii) :i{ii} {}};Rec r1 {7};Rec r2 {"Foo bar"};
Note
The C++11 initializer list rule eliminates the need for many constructors. For example:
struct Rec2{ string s; int i; Rec2(const string& ss, int ii = 0) :s{ss}, i{ii} {} // redundant};Rec2 r1 {"Foo", 7};Rec2 r2 {"Bar"};
The Rec2
constructor is redundant.
Also, the default for int
would be better done as a (member initializer).
See also: ((construct valid object) and constructor throws).
Enforcement
- Flag classes with user-defined copy operations but no constructor (a user-defined copy is a good indicator that the class has an invariant)
C.41: A constructor should create a fully initialized object
Reason
A constructor establishes the invariant for a class. A user of a class should be able to assume that a constructed object is usable.
Example, bad
class X1 { FILE* f; // call init() before any other function // ...public: X1() {} void init(); // initialize f void read(); // read from f // ...};void f(){ X1 file; file.read(); // crash or bad read! // ... file.init(); // too late // ...}
Compilers do not read comments.
Exception
If a valid object cannot conveniently be constructed by a constructor, (use a factory function).
Enforcement
- (Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default construction).
- (Unknown) If a constructor has an
Ensures
contract, try to see if it holds as a postcondition.
Note
If a constructor acquires a resource (to create a valid object), that resource should be (released by the destructor). The idiom of having constructors acquire resources and destructors release them is called (RAII) ("Resource Acquisition Is Initialization").
C.42: If a constructor cannot construct a valid object, throw an exception
Reason
Leaving behind an invalid object is asking for trouble.
Example
class X2 { FILE* f; // ...public: X2(const string& name) :f{fopen(name.c_str(), "r")} { if (!f) throw runtime_error{"could not open" + name}; // ... } void read(); // read from f // ...};void f(){ X2 file {"Zeno"}; // throws if file isn't open file.read(); // fine // ...}
Example, bad
class X3 { // bad: the constructor leaves a non-valid object behind FILE* f; // call is_valid() before any other function bool valid; // ...public: X3(const string& name) :f{fopen(name.c_str(), "r")}, valid{false} { if (f) valid = true; // ... } bool is_valid() { return valid; } void read(); // read from f // ...};void f(){ X3 file {"Heraclides"}; file.read(); // crash or bad read! // ... if (file.is_valid()) { file.read(); // ... } else { // ... handle error ... } // ...}
Note
For a variable definition (e.g., on the stack or as a member of another object) there is no explicit function call from which an error code could be returned.
Leaving behind an invalid object and relying on users to consistently check an is_valid()
function before use is tedious, error-prone, and inefficient.
Exception
There are domains, such as some hard-real-time systems (think airplane controls) where (without additional tool support) exception handling is not sufficiently predictable from a timing perspective.
There the is_valid()
technique must be used. In such cases, check is_valid()
consistently and immediately to simulate (RAII).
Alternative
If you feel tempted to use some "post-constructor initialization" or "two-stage initialization" idiom, try not to do that. If you really have to, look at (factory functions).
Note
One reason people have used init()
functions rather than doing the initialization work in a constructor has been to avoid code replication.
((Delegating constructors) and default member initialization) do that better.
Another reason has been to delay initialization until an object is needed; the solution to that is often (not to declare a variable until it can be properly initialized)
Enforcement
???
C.43: Ensure that a copyable class has a default constructor
Reason
That is, ensure that if a concrete class is copyable it also satisfies the rest of "semiregular."
Many language and library facilities rely on default constructors to initialize their elements, e.g. T a[10]
and std::vector<T> v(10)
.
A default constructor often simplifies the task of defining a suitable moved-from state for a type that is also copyable.
Example
class Date { // BAD: no default constructorpublic: Date(int dd, int mm, int yyyy); // ...};vector<Date> vd1(1000); // default Date needed herevector<Date> vd2(1000, Date{Month::October, 7, 1885}); // alternative
The default constructor is only auto-generated if there is no user-declared constructor, hence it's impossible to initialize the vector vd1
in the example above.
The absence of a default value can cause surprises for users and complicate its use, so if one can be reasonably defined, it should be.
Date
is chosen to encourage thought:
There is no "natural" default date (the big bang is too far back in time to be useful for most people), so this example is non-trivial.
{0, 0, 0}
is not a valid date in most calendar systems, so choosing that would be introducing something like floating-point's NaN
.
However, most realistic Date
classes have a "first date" (e.g. January 1, 1970 is popular), so making that the default is usually trivial.
class Date {public: Date(int dd, int mm, int yyyy); Date() = default; // [(See also)](/class#Rc-default) // ...private: int dd = 1; int mm = 1; int yyyy = 1970; // ...};vector<Date> vd1(1000);
Note
A class with members that all have default constructors implicitly gets a default constructor:
struct X { string s; vector<int> v;};X x; // means X{{}, {}}; that is the empty string and the empty vector
Beware that built-in types are not properly default constructed:
struct X { string s; int i;};void f(){ X x; // x.s is initialized to the empty string; x.i is uninitialized cout << x.s << ' ' << x.i << '\n'; ++x.i;}
Statically allocated objects of built-in types are by default initialized to 0
, but local built-in variables are not.
Beware that your compiler might default initialize local built-in variables, whereas an optimized build will not.
Thus, code like the example above might appear to work, but it relies on undefined behavior.
Assuming that you want initialization, an explicit default initialization can help:
struct X { string s; int i {}; // default initialize (to 0)};
Notes
Classes that don't have a reasonable default construction are usually not copyable either, so they don't fall under this guideline.
For example, a base class should not be copyable, and so does not necessarily need a default constructor:
// Shape is an abstract base class, not a copyable type.// It might or might not need a default constructor.struct Shape { virtual void draw() = 0; virtual void rotate(int) = 0; // =delete copy/move functions // ...};
A class that must acquire a caller-provided resource during construction often cannot have a default constructor, but it does not fall under this guideline because such a class is usually not copyable anyway:
// std::lock_guard is not a copyable type.// It does not have a default constructor.lock_guard g {mx}; // guard the mutex mxlock_guard g2; // error: guarding nothing
A class that has a "special state" that must be handled separately from other states by member functions or users causes extra work (and most likely more errors). Such a type can naturally use the special state as a default constructed value, whether or not it is copyable:
// std::ofstream is not a copyable type.// It does happen to have a default constructor// that goes along with a special "not open" state.ofstream out {"Foobar"};// ...out << log(time, transaction);
Similar special-state types that are copyable, such as copyable smart pointers that have the special state "==nullptr", should use the special state as their default constructed value.
However, it is preferable to have a default constructor default to a meaningful state such as std::string
s ""
and std::vector
s {}
.
Enforcement
- Flag classes that are copyable by
=
without a default constructor - Flag classes that are comparable with
==
but not copyable
C.44: Prefer default constructors to be simple and non-throwing
Reason
Being able to set a value to "the default" without operations that might fail simplifies error handling and reasoning about move operations.
Example, problematic
template<typename T>// elem points to space-elem element allocated using newclass Vector0 {public: Vector0() :Vector0{0} {} Vector0(int n) :elem{new T[n]}, space{elem + n}, last{elem} {} // ...private: own<T*> elem; T* space; T* last;};
This is nice and general, but setting a Vector0
to empty after an error involves an allocation, which might fail.
Also, having a default Vector
represented as {new T[0], 0, 0}
seems wasteful.
For example, Vector0<int> v[100]
costs 100 allocations.
Example
template<typename T>// elem is nullptr or elem points to space-elem element allocated using newclass Vector1 {public: // sets the representation to {nullptr, nullptr, nullptr}; doesn't throw Vector1() noexcept {} Vector1(int n) :elem{new T[n]}, space{elem + n}, last{elem} {} // ...private: own<T*> elem = nullptr; T* space = nullptr; T* last = nullptr;};
Using {nullptr, nullptr, nullptr}
makes Vector1{}
cheap, but a special case and implies run-time checks.
Setting a Vector1
to empty after detecting an error is trivial.
Enforcement
- Flag throwing default constructors
C.45: Don't define a default constructor that only initializes data members; use in-class member initializers instead
Reason
Using in-class member initializers lets the compiler generate the function for you. The compiler-generated function can be more efficient.
Example, bad
class X1 { // BAD: doesn't use member initializers string s; int i;public: X1() :s{"default"}, i{1} { } // ...};
Example
class X2 { string s = "default"; int i = 1;public: // use compiler-generated default constructor // ...};
Enforcement
(Simple) A default constructor should do more than just initialize member variables with constants.
C.46: By default, declare single-argument constructors explicit
Reason
To avoid unintended conversions.
Example, bad
class String {public: String(int); // BAD // ...};String s = 10; // surprise: string of size 10
Exception
If you really want an implicit conversion from the constructor argument type to the class type, don't use explicit
:
class Complex {public: Complex(double d); // OK: we want a conversion from d to {d, 0} // ...};Complex z = 10.7; // unsurprising conversion
See also: (Discussion of implicit conversions)
Note
Copy and move constructors should not be made explicit
because they do not perform conversions. Explicit copy/move constructors make passing and returning by value difficult.
Enforcement
(Simple) Single-argument constructors should be declared explicit
. Good single argument non-explicit
constructors are rare in most code bases. Warn for all that are not on a "positive list".
C.47: Define and initialize member variables in the order of member declaration
Reason
To minimize confusion and errors. That is the order in which the initialization happens (independent of the order of member initializers).
Example, bad
class Foo { int m1; int m2;public: Foo(int x) :m2{x}, m1{++x} { } // BAD: misleading initializer order // ...};Foo x(1); // surprise: x.m1 == x.m2 == 2
Enforcement
(Simple) A member initializer list should mention the members in the same order they are declared.
See also: (Discussion)
C.48: Prefer in-class initializers to member initializers in constructors for constant initializers
Reason
Makes it explicit that the same value is expected to be used in all constructors. Avoids repetition. Avoids maintenance problems. It leads to the shortest and most efficient code.
Example, bad
class X { // BAD int i; string s; int j;public: X() :i{666}, s{"qqq"} { } // j is uninitialized X(int ii) :i{ii} {} // s is "" and j is uninitialized // ...};
How would a maintainer know whether j
was deliberately uninitialized (probably a bad idea anyway) and whether it was intentional to give s
the default value ""
in one case and qqq
in another (almost certainly a bug)? The problem with j
(forgetting to initialize a member) often happens when a new member is added to an existing class.
Example
class X2 { int i {666}; string s {"qqq"}; int j {0};public: X2() = default; // all members are initialized to their defaults X2(int ii) :i{ii} {} // s and j initialized to their defaults // ...};
Alternative: We can get part of the benefits from default arguments to constructors, and that is not uncommon in older code. However, that is less explicit, causes more arguments to be passed, and is repetitive when there is more than one constructor:
class X3 { // BAD: inexplicit, argument passing overhead int i; string s; int j;public: X3(int ii = 666, const string& ss = "qqq", int jj = 0) :i{ii}, s{ss}, j{jj} { } // all members are initialized to their defaults // ...};
Enforcement
- (Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default construction).
- (Simple) Default arguments to constructors suggest an in-class initializer might be more appropriate.
C.49: Prefer initialization to assignment in constructors
Reason
An initialization explicitly states that initialization, rather than assignment, is done and can be more elegant and efficient. Prevents "use before set" errors.
Example, good
class A { // Good string s1;public: A(czstring p) : s1{p} { } // GOOD: directly construct (and the C-string is explicitly named) // ...};
Example, bad
class B { // BAD string s1;public: B(const char* p) { s1 = p; } // BAD: default constructor followed by assignment // ...};class C { // UGLY, aka very bad int* p;public: C() { cout << *p; p = new int{10}; } // accidental use before initialized // ...};
Example, better still
Instead of those const char*
s we could use C++17 std::string_view
or gsl::span<char>
as (a more general way to present arguments to a function):
class D { // Good string s1;public: D(string_view v) : s1{v} { } // GOOD: directly construct // ...};
C.50: Use a factory function if you need "virtual behavior" during initialization
Reason
If the state of a base class object must depend on the state of a derived part of the object, we need to use a virtual function (or equivalent) while minimizing the window of opportunity to misuse an imperfectly constructed object.
Note
The return type of the factory should normally be unique_ptr
by default; if some uses are shared, the caller can move
the unique_ptr
into a shared_ptr
. However, if the factory author knows that all uses of the returned object will be shared uses, return shared_ptr
and use make_shared
in the body to save an allocation.
Example, bad
class B {public: B() { /* ... */ f(); // BAD: C.82: Don't call virtual functions in constructors and destructors /* ... */ } virtual void f() = 0;};
Example
class B {protected: class Token {};public: explicit B(Token) { /* ... */ } // create an imperfectly initialized object virtual void f() = 0; template<class T> static shared_ptr<T> create() // interface for creating shared objects { auto p = make_shared<T>(typename T::Token{}); p->post_initialize(); return p; }protected: virtual void post_initialize() // called right after construction { /* ... */ f(); /* ... */ } // GOOD: virtual dispatch is safe};class D : public B { // some derived classprotected: class Token {};public: explicit D(Token) : B{ B::Token{} } {} void f() override { /* ... */ };protected: template<class T> friend shared_ptr<T> B::create();};shared_ptr<D> p = D::create<D>(); // creating a D object
make_shared
requires that the constructor is public. By requiring a protected Token
the constructor cannot be publicly called anymore, so we avoid an incompletely constructed object escaping into the wild.
By providing the factory function create()
, we make construction (on the free store) convenient.
Note
Conventional factory functions allocate on the free store, rather than on the stack or in an enclosing object.
See also: (Discussion)
C.51: Use delegating constructors to represent common actions for all constructors of a class
Reason
To avoid repetition and accidental differences.
Example, bad
class Date { // BAD: repetitive int d; Month m; int y;public: Date(int dd, Month mm, year yy) :d{dd}, m{mm}, y{yy} { if (!valid(d, m, y)) throw Bad_date{}; } Date(int dd, Month mm) :d{dd}, m{mm} y{current_year()} { if (!valid(d, m, y)) throw Bad_date{}; } // ...};
The common action gets tedious to write and might accidentally not be common.
Example
class Date2 { int d; Month m; int y;public: Date2(int dd, Month mm, year yy) :d{dd}, m{mm}, y{yy} { if (!valid(d, m, y)) throw Bad_date{}; } Date2(int dd, Month mm) :Date2{dd, mm, current_year()} {} // ...};
See also: If the "repeated action" is a simple initialization, consider (an in-class member initializer).
Enforcement
(Moderate) Look for similar constructor bodies.
C.52: Use inheriting constructors to import constructors into a derived class that does not need further explicit initialization
Reason
If you need those constructors for a derived class, re-implementing them is tedious and error-prone.
Example
std::vector
has a lot of tricky constructors, so if I want my own vector
, I don't want to reimplement them:
class Rec { // ... data and lots of nice constructors ...};class Oper : public Rec { using Rec::Rec; // ... no data members ... // ... lots of nice utility functions ...};
Example, bad
struct Rec2 : public Rec { int x; using Rec::Rec;};Rec2 r {"foo", 7};int val = r.x; // uninitialized
Enforcement
Make sure that every member of the derived class is initialized.
C.copy: Copy and move
Concrete types should generally be copyable, but interfaces in a class hierarchy should not. Resource handles might or might not be copyable. Types can be defined to move for logical as well as performance reasons.
C.60: Make copy assignment non-virtual
, take the parameter by const&
, and return by non-const&
Reason
It is simple and efficient. If you want to optimize for rvalues, provide an overload that takes a &&
(see (F.18)).
Example
class Foo {public: Foo& operator=(const Foo& x) { // GOOD: no need to check for self-assignment (other than performance) auto tmp = x; swap(tmp); // see C.83 return *this; } // ...};Foo a;Foo b;Foo f();a = b; // assign lvalue: copya = f(); // assign rvalue: potentially move
Note
The swap
implementation technique offers the (strong guarantee).
Example
But what if you can get significantly better performance by not making a temporary copy? Consider a simple Vector
intended for a domain where assignment of large, equal-sized Vector
s is common. In this case, the copy of elements implied by the swap
implementation technique could cause an order of magnitude increase in cost:
template<typename T>class Vector {public: Vector& operator=(const Vector&); // ...private: T* elem; int sz;};Vector& Vector::operator=(const Vector& a){ if (a.sz > sz) { // ... use the swap technique, it can't be bettered ... return *this; } // ... copy sz elements from *a.elem to elem ... if (a.sz < sz) { // ... destroy the surplus elements in *this and adjust size ... } return *this;}
By writing directly to the target elements, we will get only ((the basic guarantee) rather than the strong guarantee offered by the swap
technique. Beware of self-assignment).
Alternatives: If you think you need a virtual
assignment operator, and understand why that's deeply problematic, don't call it operator=
. Make it a named function like virtual void assign(const Foo&)
.
See (copy constructor vs. clone()
).
Enforcement
- (Simple) An assignment operator should not be virtual. Here be dragons!
- (Simple) An assignment operator should return
T&
to enable chaining, not alternatives likeconst T&
which interfere with composability and putting objects in containers. - (Moderate) An assignment operator should (implicitly or explicitly) invoke all base and member assignment operators. Look at the destructor to determine if the type has pointer semantics or value semantics.
C.61: A copy operation should copy
Reason
That is the generally assumed semantics. After x = y
, we should have x == y
.
After a copy x
and y
can be independent objects (value semantics, the way non-pointer built-in types and the standard-library types work) or refer to a shared object (pointer semantics, the way pointers work).
Example
class X { // OK: value semanticspublic: X(); X(const X&); // copy X void modify(); // change the value of X // ... ~X() { delete[] p; }private: T* p; int sz;};bool operator==(const X& a, const X& b){ return a.sz == b.sz && equal(a.p, a.p + a.sz, b.p, b.p + b.sz);}X::X(const X& a) :p{new T[a.sz]}, sz{a.sz}{ copy(a.p, a.p + sz, p);}X x;X y = x;if (x != y) throw Bad{};x.modify();if (x == y) throw Bad{}; // assume value semantics
Example
class X2 { // OK: pointer semanticspublic: X2(); X2(const X2&) = default; // shallow copy ~X2() = default; void modify(); // change the pointed-to value // ...private: T* p; int sz;};bool operator==(const X2& a, const X2& b){ return a.sz == b.sz && a.p == b.p;}X2 x;X2 y = x;if (x != y) throw Bad{};x.modify();if (x != y) throw Bad{}; // assume pointer semantics
Note
Prefer value semantics unless you are building a "smart pointer". Value semantics is the simplest to reason about and what the standard-library facilities expect.
Enforcement
(Not enforceable)
C.62: Make copy assignment safe for self-assignment
Reason
If x = x
changes the value of x
, people will be surprised and bad errors will occur (often including leaks).
Example
The standard-library containers handle self-assignment elegantly and efficiently:
std::vector<int> v = {3, 1, 4, 1, 5, 9};v = v;// the value of v is still {3, 1, 4, 1, 5, 9}
Note
The default assignment generated from members that handle self-assignment correctly handles self-assignment.
struct Bar { vector<pair<int, int>> v; map<string, int> m; string s;};Bar b;// ...b = b; // correct and efficient
Note
You can handle self-assignment by explicitly testing for self-assignment, but often it is faster and more elegant to cope without such a test (e.g., (using swap
)).
class Foo { string s; int i;public: Foo& operator=(const Foo& a); // ...};Foo& Foo::operator=(const Foo& a) // OK, but there is a cost{ if (this == &a) return *this; s = a.s; i = a.i; return *this;}
This is obviously safe and apparently efficient. However, what if we do one self-assignment per million assignments? That's about a million redundant tests (but since the answer is essentially always the same, the computer's branch predictor will guess right essentially every time). Consider:
Foo& Foo::operator=(const Foo& a) // simpler, and probably much better{ s = a.s; i = a.i; return *this;}
std::string
is safe for self-assignment and so are int
. All the cost is carried by the (rare) case of self-assignment.
Enforcement
(Simple) Assignment operators should not contain the pattern if (this == &a) return *this;
???
C.63: Make move assignment non-virtual
, take the parameter by &&
, and return by non-const&
Reason
It is simple and efficient.
See: (The rule for copy-assignment).
Enforcement
Equivalent to what is done for (copy-assignment).
- (Simple) An assignment operator should not be virtual. Here be dragons!
- (Simple) An assignment operator should return
T&
to enable chaining, not alternatives likeconst T&
which interfere with composability and putting objects in containers. - (Moderate) A move assignment operator should (implicitly or explicitly) invoke all base and member move assignment operators.
C.64: A move operation should move and leave its source in a valid state
Reason
That is the generally assumed semantics.
After y = std::move(x)
the value of y
should be the value x
had and x
should be in a valid state.
Example
class X { // OK: value semanticspublic: X(); X(X&& a) noexcept; // move X X& operator=(X&& a) noexcept; // move-assign X void modify(); // change the value of X // ... ~X() { delete[] p; }private: T* p; int sz;};X::X(X&& a) noexcept :p{a.p}, sz{a.sz} // steal representation{ a.p = nullptr; // set to "empty" a.sz = 0;}void use(){ X x{}; // ... X y = std::move(x); x = X{}; // OK} // OK: x can be destroyed
Note
Ideally, that moved-from should be the default value of the type. Ensure that unless there is an exceptionally good reason not to. However, not all types have a default value and for some types establishing the default value can be expensive. The standard requires only that the moved-from object can be destroyed. Often, we can easily and cheaply do better: The standard library assumes that it is possible to assign to a moved-from object. Always leave the moved-from object in some (necessarily specified) valid state.
Note
Unless there is an exceptionally strong reason not to, make x = std::move(y); y = z;
work with the conventional semantics.
Enforcement
(Not enforceable) Look for assignments to members in the move operation. If there is a default constructor, compare those assignments to the initializations in the default constructor.
C.65: Make move assignment safe for self-assignment
Reason
If x = x
changes the value of x
, people will be surprised and bad errors can occur. However, people don't usually directly write a self-assignment that turn into a move, but it can occur. However, std::swap
is implemented using move operations so if you accidentally do swap(a, b)
where a
and b
refer to the same object, failing to handle self-move could be a serious and subtle error.
Example
class Foo { string s; int i;public: Foo& operator=(Foo&& a); // ...};Foo& Foo::operator=(Foo&& a) noexcept // OK, but there is a cost{ if (this == &a) return *this; // this line is redundant s = std::move(a.s); i = a.i; return *this;}
The one-in-a-million argument against if (this == &a) return *this;
tests from the discussion of (self-assignment) is even more relevant for self-move.
Note
There is no known general way of avoiding an if (this == &a) return *this;
test for a move assignment and still get a correct answer (i.e., after x = x
the value of x
is unchanged).
Note
The ISO standard guarantees only a "valid but unspecified" state for the standard-library containers. Apparently this has not been a problem in about 10 years of experimental and production use. Please contact the editors if you find a counter example. The rule here is more caution and insists on complete safety.
Example
Here is a way to move a pointer without a test (imagine it as code in the implementation a move assignment):
// move from other.ptr to this->ptrT* temp = other.ptr;other.ptr = nullptr;delete ptr;ptr = temp;
Enforcement
- (Moderate) In the case of self-assignment, a move assignment operator should not leave the object holding pointer members that have been
delete
d or set tonullptr
. - (Not enforceable) Look at the use of standard-library container types (incl.
string
) and consider them safe for ordinary (not life-critical) uses.
C.66: Make move operations noexcept
Reason
A throwing move violates most people's reasonable assumptions. A non-throwing move will be used more efficiently by standard-library and language facilities.
Example
template<typename T>class Vector {public: Vector(Vector&& a) noexcept :elem{a.elem}, sz{a.sz} { a.sz = 0; a.elem = nullptr; } Vector& operator=(Vector&& a) noexcept { elem = a.elem; sz = a.sz; a.sz = 0; a.elem = nullptr; } // ...private: T* elem; int sz;};
These operations do not throw.
Example, bad
template<typename T>class Vector2 {public: Vector2(Vector2&& a) { *this = a; } // just use the copy Vector2& operator=(Vector2&& a) { *this = a; } // just use the copy // ...private: T* elem; int sz;};
This Vector2
is not just inefficient, but since a vector copy requires allocation, it can throw.
Enforcement
(Simple) A move operation should be marked noexcept
.
C.67: A polymorphic class should suppress public copy/move
Reason
A polymorphic class is a class that defines or inherits at least one virtual function. It is likely that it will be used as a base class for other derived classes with polymorphic behavior. If it is accidentally passed by value, with the implicitly generated copy constructor and assignment, we risk slicing: only the base portion of a derived object will be copied, and the polymorphic behavior will be corrupted.
If the class has no data, =delete
the copy/move functions. Otherwise, make them protected.
Example, bad
class B { // BAD: polymorphic base class doesn't suppress copyingpublic: virtual char m() { return 'B'; } // ... nothing about copy operations, so uses default ...};class D : public B {public: char m() override { return 'D'; } // ...};void f(B& b){ auto b2 = b; // oops, slices the object; b2.m() will return 'B'}D d;f(d);
Example
class B { // GOOD: polymorphic class suppresses copyingpublic: B() = default; B(const B&) = delete; B& operator=(const B&) = delete; virtual char m() { return 'B'; } // ...};class D : public B {public: char m() override { return 'D'; } // ...};void f(B& b){ auto b2 = b; // ok, compiler will detect inadvertent copying, and protest}D d;f(d);
Note
If you need to create deep copies of polymorphic objects, use clone()
functions: see (C.130).
Exception
Classes that represent exception objects need both to be polymorphic and copy-constructible.
Enforcement
- Flag a polymorphic class with a public copy operation.
- Flag an assignment of polymorphic class objects.
C.other: Other default operation rules
In addition to the operations for which the language offers default implementations,
there are a few operations that are so foundational that specific rules for their definition are needed:
comparisons, swap
, and hash
.
C.80: Use =default
if you have to be explicit about using the default semantics
Reason
The compiler is more likely to get the default semantics right and you cannot implement these functions better than the compiler.
Example
class Tracer { string message;public: Tracer(const string& m) : message{m} { cerr << "entering " << message << '\n'; } ~Tracer() { cerr << "exiting " << message << '\n'; } Tracer(const Tracer&) = default; Tracer& operator=(const Tracer&) = default; Tracer(Tracer&&) = default; Tracer& operator=(Tracer&&) = default;};
Because we defined the destructor, we must define the copy and move operations. The = default
is the best and simplest way of doing that.
Example, bad
class Tracer2 { string message;public: Tracer2(const string& m) : message{m} { cerr << "entering " << message << '\n'; } ~Tracer2() { cerr << "exiting " << message << '\n'; } Tracer2(const Tracer2& a) : message{a.message} {} Tracer2& operator=(const Tracer2& a) { message = a.message; return *this; } Tracer2(Tracer2&& a) :message{a.message} {} Tracer2& operator=(Tracer2&& a) { message = a.message; return *this; }};
Writing out the bodies of the copy and move operations is verbose, tedious, and error-prone. A compiler does it better.
Enforcement
(Moderate) The body of a special operation should not have the same accessibility and semantics as the compiler-generated version, because that would be redundant
C.81: Use =delete
when you want to disable default behavior (without wanting an alternative)
Reason
In a few cases, a default operation is not desirable.
Example
class Immortal {public: ~Immortal() = delete; // do not allow destruction // ...};void use(){ Immortal ugh; // error: ugh cannot be destroyed Immortal* p = new Immortal{}; delete p; // error: cannot destroy *p}
Example
A unique_ptr
can be moved, but not copied. To achieve that its copy operations are deleted. To avoid copying it is necessary to =delete
its copy operations from lvalues:
template<class T, class D = default_delete<T>> class unique_ptr {public: // ... constexpr unique_ptr() noexcept; explicit unique_ptr(pointer p) noexcept; // ... unique_ptr(unique_ptr&& u) noexcept; // move constructor // ... unique_ptr(const unique_ptr&) = delete; // disable copy from lvalue // ...};unique_ptr<int> make(); // make "something" and return it by movingvoid f(){ unique_ptr<int> pi {}; auto pi2 {pi}; // error: no move constructor from lvalue auto pi3 {make()}; // OK, move: the result of make() is an rvalue}
Note that deleted functions should be public.
Enforcement
The elimination of a default operation is (should be) based on the desired semantics of the class. Consider such classes suspect, but maintain a "positive list" of classes where a human has asserted that the semantics is correct.
C.82: Don't call virtual functions in constructors and destructors
Reason
The function called will be that of the object constructed so far, rather than a possibly overriding function in a derived class. This can be most confusing. Worse, a direct or indirect call to an unimplemented pure virtual function from a constructor or destructor results in undefined behavior.
Example, bad
class Base {public: virtual void f() = 0; // not implemented virtual void g(); // implemented with Base version virtual void h(); // implemented with Base version virtual ~Base(); // implemented with Base version};class Derived : public Base {public: void g() override; // provide Derived implementation void h() final; // provide Derived implementation Derived() { // BAD: attempt to call an unimplemented virtual function f(); // BAD: will call Derived::g, not dispatch further virtually g(); // GOOD: explicitly state intent to call only the visible version Derived::g(); // ok, no qualification needed, h is final h(); }};
Note that calling a specific explicitly qualified function is not a virtual call even if the function is virtual
.
See also (factory functions) for how to achieve the effect of a call to a derived class function without risking undefined behavior.
Note
There is nothing inherently wrong with calling virtual functions from constructors and destructors. The semantics of such calls is type safe. However, experience shows that such calls are rarely needed, easily confuse maintainers, and become a source of errors when used by novices.
Enforcement
- Flag calls of virtual functions from constructors and destructors.
C.83: For value-like types, consider providing a noexcept
swap function
Reason
A swap
can be handy for implementing a number of idioms, from smoothly moving objects around to implementing assignment easily to providing a guaranteed commit function that enables strongly error-safe calling code. Consider using swap to implement copy assignment in terms of copy construction. See also (destructors, deallocation, and swap must never fail).
Example, good
class Foo {public: void swap(Foo& rhs) noexcept { m1.swap(rhs.m1); std::swap(m2, rhs.m2); }private: Bar m1; int m2;};
Providing a non-member swap
function in the same namespace as your type for callers' convenience.
void swap(Foo& a, Foo& b){ a.swap(b);}
Enforcement
- Non-trivially copyable types should provide a member swap or a free swap overload.
- (Simple) When a class has a
swap
member function, it should be declarednoexcept
.
C.84: A swap
function must not fail
Reason
swap
is widely used in ways that are assumed never to fail and programs cannot easily be written to work correctly in the presence of a failing swap
. The standard-library containers and algorithms will not work correctly if a swap of an element type fails.
Example, bad
void swap(My_vector& x, My_vector& y){ auto tmp = x; // copy elements x = y; y = tmp;}
This is not just slow, but if a memory allocation occurs for the elements in tmp
, this swap
could throw and would make STL algorithms fail if used with them.
Enforcement
(Simple) When a class has a swap
member function, it should be declared noexcept
.
C.85: Make swap
noexcept
Reason
(A swap
must not fail).
If a swap
tries to exit with an exception, it's a bad design error and the program had better terminate.
Enforcement
(Simple) When a class has a swap
member function, it should be declared noexcept
.
C.86: Make ==
symmetric with respect to operand types and noexcept
Reason
Asymmetric treatment of operands is surprising and a source of errors where conversions are possible.
==
is a fundamental operation and programmers should be able to use it without fear of failure.
Example
struct X { string name; int number;};bool operator==(const X& a, const X& b) noexcept { return a.name == b.name && a.number == b.number;}
Example, bad
class B { string name; int number; bool operator==(const B& a) const { return name == a.name && number == a.number; } // ...};
B
's comparison accepts conversions for its second operand, but not its first.
Note
If a class has a failure state, like double
's NaN
, there is a temptation to make a comparison against the failure state throw.
The alternative is to make two failure states compare equal and any valid state compare false against the failure state.
Note
This rule applies to all the usual comparison operators: !=
, <
, <=
, >
, and >=
.
Enforcement
- Flag an
operator==()
for which the argument types differ; same for other comparison operators:!=
,<
,<=
,>
, and>=
. - Flag member
operator==()
s; same for other comparison operators:!=
,<
,<=
,>
, and>=
.
C.87: Beware of ==
on base classes
Reason
It is really hard to write a foolproof and useful ==
for a hierarchy.
Example, bad
class B { string name; int number; virtual bool operator==(const B& a) const { return name == a.name && number == a.number; } // ...};
B
's comparison accepts conversions for its second operand, but not its first.
class D : B { char character; virtual bool operator==(const D& a) const { return name == a.name && number == a.number && character == a.character; } // ...};B b = ...D d = ...b == d; // compares name and number, ignores d's characterd == b; // error: no == definedD d2;d == d2; // compares name, number, and characterB& b2 = d2;b2 == d; // compares name and number, ignores d2's and d's character
Of course there are ways of making ==
work in a hierarchy, but the naive approaches do not scale
Note
This rule applies to all the usual comparison operators: !=
, <
, <=
, >
, >=
, and <=>
.
Enforcement
- Flag a virtual
operator==()
; same for other comparison operators:!=
,<
,<=
,>
,>=
, and<=>
.
C.89: Make a hash
noexcept
Reason
Users of hashed containers use hash indirectly and don't expect simple access to throw. It's a standard-library requirement.
Example, bad
template<>struct hash<My_type> { // thoroughly bad hash specialization using result_type = size_t; using argument_type = My_type; size_t operator()(const My_type & x) const { size_t xs = x.s.size(); if (xs < 4) throw Bad_My_type{}; // "Nobody expects the Spanish inquisition!" return hash<size_t>()(x.s.size()) ^ trim(x.s); }};int main(){ unordered_map<My_type, int> m; My_type mt{ "asdfg" }; m[mt] = 7; cout << m[My_type{ "asdfg" }] << '\n';}
If you have to define a hash
specialization, try simply to let it combine standard-library hash
specializations with ^
(xor).
That tends to work better than "cleverness" for non-specialists.
Enforcement
- Flag throwing
hash
es.
C.90: Rely on constructors and assignment operators, not memset
and memcpy
Reason
The standard C++ mechanism to construct an instance of a type is to call its constructor. As specified in guideline (C.41): a constructor should create a fully initialized object. No additional initialization, such as by memcpy
, should be required.
A type will provide a copy constructor and/or copy assignment operator to appropriately make a copy of the class, preserving the type's invariants. Using memcpy to copy a non-trivially copyable type has undefined behavior. Frequently this results in slicing, or data corruption.
Example, good
struct base { virtual void update() = 0; std::shared_ptr<int> sp;};struct derived : public base { void update() override {}};
Example, bad
void init(derived& a){ memset(&a, 0, sizeof(derived));}
This is type-unsafe and overwrites the vtable.
Example, bad
void copy(derived& a, derived& b){ memcpy(&a, &b, sizeof(derived));}
This is also type-unsafe and overwrites the vtable.
Enforcement
- Flag passing a non-trivially-copyable type to
memset
ormemcpy
.
C.con: Containers and other resource handles
A container is an object holding a sequence of objects of some type; std::vector
is the archetypical container.
A resource handle is a class that owns a resource; std::vector
is the typical resource handle; its resource is its sequence of elements.
Summary of container rules:
- (C.100: Follow the STL when defining a container)
- (C.101: Give a container value semantics)
- (C.102: Give a container move operations)
- (C.103: Give a container an initializer list constructor)
- (C.104: Give a container a default constructor that sets it to empty)
- ???
- (C.109: If a resource handle has pointer semantics, provide
*
and->
)
See also: (Resources)
C.100: Follow the STL when defining a container
Reason
The STL containers are familiar to most C++ programmers and a fundamentally sound design.
Note
There are of course other fundamentally sound design styles and sometimes reasons to depart from the style of the standard library, but in the absence of a solid reason to differ, it is simpler and easier for both implementers and users to follow the standard.
In particular, std::vector
and std::map
provide useful relatively simple models.
Example
// simplified (e.g., no allocators):template<typename T>class Sorted_vector { using value_type = T; // ... iterator types ... Sorted_vector() = default; Sorted_vector(initializer_list<T>); // initializer-list constructor: sort and store Sorted_vector(const Sorted_vector&) = default; Sorted_vector(Sorted_vector&&) = default; Sorted_vector& operator=(const Sorted_vector&) = default; // copy assignment Sorted_vector& operator=(Sorted_vector&&) = default; // move assignment ~Sorted_vector() = default; Sorted_vector(const std::vector<T>& v); // store and sort Sorted_vector(std::vector<T>&& v); // sort and "steal representation" const T& operator[](int i) const { return rep[i]; } // no non-const direct access to preserve order void push_back(const T&); // insert in the right place (not necessarily at back) void push_back(T&&); // insert in the right place (not necessarily at back) // ... cbegin(), cend() ...private: std::vector<T> rep; // use a std::vector to hold elements};template<typename T> bool operator==(const Sorted_vector<T>&, const Sorted_vector<T>&);template<typename T> bool operator!=(const Sorted_vector<T>&, const Sorted_vector<T>&);// ...
Here, the STL style is followed, but incompletely.
That's not uncommon.
Provide only as much functionality as makes sense for a specific container.
The key is to define the conventional constructors, assignments, destructors, and iterators
(as meaningful for the specific container) with their conventional semantics.
From that base, the container can be expanded as needed.
Here, special constructors from std::vector
were added.
Enforcement
???
C.101: Give a container value semantics
Reason
Regular objects are simpler to think and reason about than irregular ones. Familiarity.
Note
If meaningful, make a container Regular
(the concept).
In particular, ensure that an object compares equal to its copy.
Example
void f(const Sorted_vector<string>& v){ Sorted_vector<string> v2 {v}; if (v != v2) cout << "Behavior against reason and logic.\n"; // ...}
Enforcement
???
C.102: Give a container move operations
Reason
Containers tend to get large; without a move constructor and a copy constructor an object can be expensive to move around, thus tempting people to pass pointers to it around and getting into resource management problems.
Example
Sorted_vector<int> read_sorted(istream& is){ vector<int> v; cin >> v; // assume we have a read operation for vectors Sorted_vector<int> sv = v; // sorts return sv;}
A user can reasonably assume that returning a standard-like container is cheap.
Enforcement
???
C.103: Give a container an initializer list constructor
Reason
People expect to be able to initialize a container with a set of values. Familiarity.
Example
Sorted_vector<int> sv {1, 3, -1, 7, 0, 0}; // Sorted_vector sorts elements as needed
Enforcement
???
C.104: Give a container a default constructor that sets it to empty
Reason
To make it Regular
.
Example
vector<Sorted_sequence<string>> vs(100); // 100 Sorted_sequences each with the value ""
Enforcement
???
C.109: If a resource handle has pointer semantics, provide *
and ->
Reason
That's what is expected from pointers. Familiarity.
Example
???
Enforcement
???
C.lambdas: Function objects and lambdas
A function object is an object supplying an overloaded ()
so that you can call it.
A lambda expression (colloquially often shortened to "a lambda") is a notation for generating a function object.
Function objects should be cheap to copy (and therefore (passed by value)).
Summary:
- (F.50: Use a lambda when a function won't do (to capture local variables, or to write a local function))
- (F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms)
- (F.53: Avoid capturing by reference in lambdas that will be used non-locally, including returned, stored on the heap, or passed to another thread)
- (ES.28: Use lambdas for complex initialization, especially of
const
variables)
C.hier: Class hierarchies (OOP)
A class hierarchy is constructed to represent a set of hierarchically organized concepts (only). Typically base classes act as interfaces. There are two major uses for hierarchies, often named implementation inheritance and interface inheritance.
Class hierarchy rule summary:
- (C.120: Use class hierarchies to represent concepts with inherent hierarchical structure (only))
- (C.121: If a base class is used as an interface, make it a pure abstract class)
- (C.122: Use abstract classes as interfaces when complete separation of interface and implementation is needed)
Designing rules for classes in a hierarchy summary:
- (C.126: An abstract class typically doesn't need a user-written constructor)
- (C.127: A class with a virtual function should have a virtual or protected destructor)
- (C.128: Virtual functions should specify exactly one of
virtual
,override
, orfinal
) - (C.129: When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance)
- (C.130: For making deep copies of polymorphic classes prefer a virtual
clone
function instead of public copy construction/assignment) - (C.131: Avoid trivial getters and setters)
- (C.132: Don't make a function
virtual
without reason) - (C.133: Avoid
protected
data) - (C.134: Ensure all non-
const
data members have the same access level) - (C.135: Use multiple inheritance to represent multiple distinct interfaces)
- (C.136: Use multiple inheritance to represent the union of implementation attributes)
- (C.137: Use
virtual
bases to avoid overly general base classes) - (C.138: Create an overload set for a derived class and its bases with
using
) - (C.139: Use
final
on classes sparingly) - (C.140: Do not provide different default arguments for a virtual function and an overrider)
Accessing objects in a hierarchy rule summary:
- (C.145: Access polymorphic objects through pointers and references)
- C.146: Use
dynamic_cast
where class hierarchy navigation is unavoidable - (C.147: Use
dynamic_cast
to a reference type when failure to find the required class is considered an error) - (C.148: Use
dynamic_cast
to a pointer type when failure to find the required class is considered a valid alternative) - (C.149: Use
unique_ptr
orshared_ptr
to avoid forgetting todelete
objects created usingnew
) - C.150: Use
make_unique()
to construct objects owned byunique_ptr
s - C.151: Use
make_shared()
to construct objects owned byshared_ptr
s - (C.152: Never assign a pointer to an array of derived class objects to a pointer to its base)
- (C.153: Prefer virtual function to casting)
C.120: Use class hierarchies to represent concepts with inherent hierarchical structure (only)
Reason
Direct representation of ideas in code eases comprehension and maintenance. Make sure the idea represented in the base class exactly matches all derived types and there is not a better way to express it than using the tight coupling of inheritance.
Do not use inheritance when simply having a data member will do. Usually this means that the derived type needs to override a base virtual function or needs access to a protected member.
Example
class DrawableUIElement {public: virtual void render() const = 0; // ...};class AbstractButton : public DrawableUIElement {public: virtual void onClick() = 0; // ...};class PushButton : public AbstractButton { void render() const override; void onClick() override; // ...};class Checkbox : public AbstractButton {// ...};
Example, bad
Do not represent non-hierarchical domain concepts as class hierarchies.
template<typename T>class Container {public: // list operations: virtual T& get() = 0; virtual void put(T&) = 0; virtual void insert(Position) = 0; // ... // vector operations: virtual T& operator[](int) = 0; virtual void sort() = 0; // ... // tree operations: virtual void balance() = 0; // ...};
Here most overriding classes cannot implement most of the functions required in the interface well.
Thus the base class becomes an implementation burden.
Furthermore, the user of Container
cannot rely on the member functions actually performing meaningful operations reasonably efficiently;
it might throw an exception instead.
Thus users have to resort to run-time checking and/or
not using this (over)general interface in favor of a particular interface found by a run-time type inquiry (e.g., a dynamic_cast
).
Enforcement
- Look for classes with lots of members that do nothing but throw.
- Flag every use of a non-public base class
B
where the derived classD
does not override a virtual function or access a protected member inB
, andB
is not one of the following: empty, a template parameter or parameter pack ofD
, a class template specialized withD
.
C.121: If a base class is used as an interface, make it a pure abstract class
Reason
A class is more stable (less brittle) if it does not contain data. Interfaces should normally be composed entirely of public pure virtual functions and a default/empty virtual destructor.
Example
class My_interface {public: // ...only pure virtual functions here ... virtual ~My_interface() {} // or =default};
Example, bad
class Goof {public: // ...only pure virtual functions here ... // no virtual destructor};class Derived : public Goof { string s; // ...};void use(){ unique_ptr<Goof> p {new Derived{"here we go"}}; f(p.get()); // use Derived through the Goof interface g(p.get()); // use Derived through the Goof interface} // leak
The Derived
is delete
d through its Goof
interface, so its string
is leaked.
Give Goof
a virtual destructor and all is well.
Enforcement
- Warn on any class that contains data members and also has an overridable (non-
final
) virtual function that wasn't inherited from a base class.
C.122: Use abstract classes as interfaces when complete separation of interface and implementation is needed
Reason
Such as on an ABI (link) boundary.
Example
struct Device { virtual ~Device() = default; virtual void write(span<const char> outbuf) = 0; virtual void read(span<char> inbuf) = 0;};class D1 : public Device { // ... data ... void write(span<const char> outbuf) override; void read(span<char> inbuf) override;};class D2 : public Device { // ... different data ... void write(span<const char> outbuf) override; void read(span<char> inbuf) override;};
A user can now use D1
s and D2
s interchangeably through the interface provided by Device
.
Furthermore, we can update D1
and D2
in ways that are not binary compatible with older versions as long as all access goes through Device
.
Enforcement
???
C.hierclass: Designing classes in a hierarchy:
C.126: An abstract class typically doesn't need a user-written constructor
Reason
An abstract class typically does not have any data for a constructor to initialize.
Example
class Shape {public: // no user-written constructor needed in abstract base class virtual Point center() const = 0; // pure virtual virtual void move(Point to) = 0; // ... more pure virtual functions... virtual ~Shape() {} // destructor};class Circle : public Shape {public: Circle(Point p, int rad); // constructor in derived class Point center() const override { return x; }};
Exception
- A base class constructor that does work, such as registering an object somewhere, might need a constructor.
- In extremely rare cases, you might find it reasonable for an abstract class to have a bit of data shared by all derived classes (e.g., use statistics data, debug information, etc.); such classes tend to have constructors. But be warned: Such classes also tend to be prone to requiring virtual inheritance.
Enforcement
Flag abstract classes with constructors.
C.127: A class with a virtual function should have a virtual or protected destructor
Reason
A class with a virtual function is usually (and in general) used via a pointer to base. Usually, the last user has to call delete on a pointer to base, often via a smart pointer to base, so the destructor should be public and virtual. Less commonly, if deletion through a pointer to base is not intended to be supported, the destructor should be protected and non-virtual; see (C.35).
Example, bad
struct B { virtual int f() = 0; // ... no user-written destructor, defaults to public non-virtual ...};// bad: derived from a class without a virtual destructorstruct D : B { string s {"default"}; // ...};void use(){ unique_ptr<B> p = make_unique<D>(); // ...} // undefined behavior, might call B::~B only and leak the string
Note
There are people who don't follow this rule because they plan to use a class only through a shared_ptr
: std::shared_ptr<B> p = std::make_shared<D>(args);
Here, the shared pointer will take care of deletion, so no leak will occur from an inappropriate delete
of the base. People who do this consistently can get a false positive, but the rule is important -- what if one was allocated using make_unique
? It's not safe unless the author of B
ensures that it can never be misused, such as by making all constructors private and providing a factory function to enforce the allocation with make_shared
.
Enforcement
- A class with any virtual functions should have a destructor that is either public and virtual or else protected and non-virtual.
- Flag
delete
of a class with a virtual function but no virtual destructor.
C.128: Virtual functions should specify exactly one of virtual
, override
, or final
Reason
Readability.
Detection of mistakes.
Writing explicit virtual
, override
, or final
is self-documenting and enables the compiler to catch mismatch of types and/or names between base and derived classes. However, writing more than one of these three is both redundant and a potential source of errors.
It's simple and clear:
virtual
means exactly and only "this is a new virtual function."override
means exactly and only "this is a non-final overrider."final
means exactly and only "this is a final overrider."
Example, bad
struct B { void f1(int); virtual void f2(int) const; virtual void f3(int); // ...};struct D : B { void f1(int); // bad (hope for a warning): D::f1() hides B::f1() void f2(int) const; // bad (but conventional and valid): no explicit override void f3(double); // bad (hope for a warning): D::f3() hides B::f3() // ...};
Example, good
struct Better : B { void f1(int) override; // error (caught): Better::f1() hides B::f1() void f2(int) const override; void f3(double) override; // error (caught): Better::f3() hides B::f3() // ...};
Discussion
We want to eliminate two particular classes of errors:
- implicit virtual: the programmer intended the function to be implicitly virtual and it is (but readers of the code can't tell); or the programmer intended the function to be implicitly virtual but it isn't (e.g., because of a subtle parameter list mismatch); or the programmer did not intend the function to be virtual but it is (because it happens to have the same signature as a virtual in the base class)
- implicit override: the programmer intended the function to be implicitly an overrider and it is (but readers of the code can't tell); or the programmer intended the function to be implicitly an overrider but it isn't (e.g., because of a subtle parameter list mismatch); or the programmer did not intend the function to be an overrider but it is (because it happens to have the same signature as a virtual in the base class -- note this problem arises whether or not the function is explicitly declared virtual, because the programmer might have intended to create either a new virtual function or a new non-virtual function)
Note: On a class defined as final
, it doesn't matter whether you put override
or final
on an individual virtual function.
Note: Use final
on functions sparingly. It does not necessarily lead to optimization, and it precludes further overriding.
Enforcement
- Compare virtual function names in base and derived classes and flag uses of the same name that does not override.
- Flag overrides with neither
override
norfinal
. - Flag function declarations that use more than one of
virtual
,override
, andfinal
.
C.129: When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance
Reason
Implementation details in an interface make the interface brittle; that is, make its users vulnerable to having to recompile after changes in the implementation. Data in a base class increases the complexity of implementing the base and can lead to replication of code.
Note
Definition:
- interface inheritance is the use of inheritance to separate users from implementations, in particular to allow derived classes to be added and changed without affecting the users of base classes.
- implementation inheritance is the use of inheritance to simplify implementation of new facilities by making useful operations available for implementers of related new operations (sometimes called "programming by difference").
A pure interface class is simply a set of pure virtual functions; see (I.25).
In early OOP (e.g., in the 1980s and 1990s), implementation inheritance and interface inheritance were often mixed and bad habits die hard. Even now, mixtures are not uncommon in old code bases and in old-style teaching material.
The importance of keeping the two kinds of inheritance increases
- with the size of a hierarchy (e.g., dozens of derived classes),
- with the length of time the hierarchy is used (e.g., decades), and
- with the number of distinct organizations in which a hierarchy is used (e.g., it can be difficult to distribute an update to a base class)
Example, bad
class Shape { // BAD, mixed interface and implementationpublic: Shape(); Shape(Point ce = {0, 0}, Color co = none): cent{ce}, col {co} { /* ... */} Point center() const { return cent; } Color color() const { return col; } virtual void rotate(int) = 0; virtual void move(Point p) { cent = p; redraw(); } virtual void redraw(); // ...private: Point cent; Color col;};class Circle : public Shape {public: Circle(Point c, int r) : Shape{c}, rad{r} { /* ... */ } // ...private: int rad;};class Triangle : public Shape {public: Triangle(Point p1, Point p2, Point p3); // calculate center // ...};
Problems:
- As the hierarchy grows and more data is added to
Shape
, the constructors get harder to write and maintain. - Why calculate the center for the
Triangle
? we might never use it. - Add a data member to
Shape
(e.g., drawing style or canvas) and all classes derived fromShape
and all code usingShape
will need to be reviewed, possibly changed, and probably recompiled.
The implementation of Shape::move()
is an example of implementation inheritance:
we have defined move()
once and for all for all derived classes.
The more code there is in such base class member function implementations and the more data is shared by placing it in the base,
the more benefits we gain - and the less stable the hierarchy is.
Example
This Shape hierarchy can be rewritten using interface inheritance:
class Shape { // pure interfacepublic: virtual Point center() const = 0; virtual Color color() const = 0; virtual void rotate(int) = 0; virtual void move(Point p) = 0; virtual void redraw() = 0; // ...};
Note that a pure interface rarely has constructors: there is nothing to construct.
class Circle : public Shape {public: Circle(Point c, int r, Color c) : cent{c}, rad{r}, col{c} { /* ... */ } Point center() const override { return cent; } Color color() const override { return col; } // ...private: Point cent; int rad; Color col;};
The interface is now less brittle, but there is more work in implementing the member functions.
For example, center
has to be implemented by every class derived from Shape
.
Example, dual hierarchy
How can we gain the benefit of stable hierarchies from implementation hierarchies and the benefit of implementation reuse from implementation inheritance? One popular technique is dual hierarchies. There are many ways of implementing the idea of dual hierarchies; here, we use a multiple-inheritance variant.
First we devise a hierarchy of interface classes:
class Shape { // pure interfacepublic: virtual Point center() const = 0; virtual Color color() const = 0; virtual void rotate(int) = 0; virtual void move(Point p) = 0; virtual void redraw() = 0; // ...};class Circle : public virtual Shape { // pure interfacepublic: virtual int radius() = 0; // ...};
To make this interface useful, we must provide its implementation classes (here, named equivalently, but in the Impl
namespace):
class Impl::Shape : public virtual ::Shape { // implementationpublic: // constructors, destructor // ... Point center() const override { /* ... */ } Color color() const override { /* ... */ } void rotate(int) override { /* ... */ } void move(Point p) override { /* ... */ } void redraw() override { /* ... */ } // ...};
Now Shape
is a poor example of a class with an implementation,
but bear with us because this is just a simple example of a technique aimed at more complex hierarchies.
class Impl::Circle : public virtual ::Circle, public Impl::Shape { // implementationpublic: // constructors, destructor int radius() override { /* ... */ } // ...};
And we could extend the hierarchies by adding a Smiley class (:-)):
class Smiley : public virtual Circle { // pure interfacepublic: // ...};class Impl::Smiley : public virtual ::Smiley, public Impl::Circle { // implementationpublic: // constructors, destructor // ...}
There are now two hierarchies:
- interface: Smiley -> Circle -> Shape
- implementation: Impl::Smiley -> Impl::Circle -> Impl::Shape
Since each implementation is derived from its interface as well as its implementation base class we get a lattice (DAG):
Smiley -> Circle -> Shape ^ ^ ^ | | |Impl::Smiley -> Impl::Circle -> Impl::Shape
As mentioned, this is just one way to construct a dual hierarchy.
The implementation hierarchy can be used directly, rather than through the abstract interface.
void work_with_shape(Shape&);int user(){ Impl::Smiley my_smiley{ /* args */ }; // create concrete shape // ... my_smiley.some_member(); // use implementation class directly // ... work_with_shape(my_smiley); // use implementation through abstract interface // ...}
This can be useful when the implementation class has members that are not offered in the abstract interface
or if direct use of a member offers optimization opportunities (e.g., if an implementation member function is final
)
Note
Another (related) technique for separating interface and implementation is (Pimpl).
Note
There is often a choice between offering common functionality as (implemented) base class functions and free-standing functions (in an implementation namespace). Base classes gives a shorter notation and easier access to shared data (in the base) at the cost of the functionality being available only to users of the hierarchy.
Enforcement
- Flag a derived to base conversion to a base with both data and virtual functions (except for calls from a derived class member to a base class member)
- ???
C.130: For making deep copies of polymorphic classes prefer a virtual clone
function instead of public copy construction/assignment
Reason
Copying a polymorphic class is discouraged due to the slicing problem, see (C.67). If you really need copy semantics, copy deeply: Provide a virtual clone
function that will copy the actual most-derived type and return an owning pointer to the new object, and then in derived classes return the derived type (use a covariant return type).
Example
class B {public: B() = default; virtual ~B() = default; virtual gsl::owner<B*> clone() const = 0;protected: B(const B&) = default; B& operator=(const B&) = default; B(B&&) = default; B& operator=(B&&) = default; // ...};class D : public B {public: gsl::owner<D*> clone() const override { return new D{*this}; };};
Generally, it is recommended to use smart pointers to represent ownership (see ((R.20)). However, because of language rules, the covariant return type cannot be a smart pointer: D::clone
can't return a unique_ptr<D>
while B::clone
returns unique_ptr<B>
. Therefore, you either need to consistently return unique_ptr<B>
in all overrides, or use owner<>
utility from the Guidelines Support Library).
C.131: Avoid trivial getters and setters
Reason
A trivial getter or setter adds no semantic value; the data item could just as well be public
.
Example
class Point { // Bad: verbose int x; int y;public: Point(int xx, int yy) : x{xx}, y{yy} { } int get_x() const { return x; } void set_x(int xx) { x = xx; } int get_y() const { return y; } void set_y(int yy) { y = yy; } // no behavioral member functions};
Consider making such a class a struct
-- that is, a behaviorless bunch of variables, all public data and no member functions.
struct Point { int x {0}; int y {0};};
Note that we can put default initializers on member variables: (C.49: Prefer initialization to assignment in constructors).
Note
The key to this rule is whether the semantics of the getter/setter are trivial. While it is not a complete definition of "trivial", consider whether there would be any difference beyond syntax if the getter/setter was a public data member instead. Examples of non-trivial semantics would be: maintaining a class invariant or converting between an internal type and an interface type.
Enforcement
Flag multiple get
and set
member functions that simply access a member without additional semantics.
C.132: Don't make a function virtual
without reason
Reason
Redundant virtual
increases run-time and object-code size.
A virtual function can be overridden and is thus open to mistakes in a derived class.
A virtual function ensures code replication in a templated hierarchy.
Example, bad
template<class T>class Vector {public: // ... virtual int size() const { return sz; } // bad: what good could a derived class do?private: T* elem; // the elements int sz; // number of elements};
This kind of "vector" isn't meant to be used as a base class at all.
Enforcement
- Flag a class with virtual functions but no derived classes.
- Flag a class where all member functions are virtual and have implementations.
C.133: Avoid protected
data
Reason
protected
data is a source of complexity and errors.
protected
data complicates the statement of invariants.
protected
data inherently violates the guidance against putting data in base classes, which usually leads to having to deal with virtual inheritance as well.
Example, bad
class Shape {public: // ... interface functions ...protected: // data for use in derived classes: Color fill_color; Color edge_color; Style st;};
Now it is up to every derived Shape
to manipulate the protected data correctly.
This has been popular, but also a major source of maintenance problems.
In a large class hierarchy, the consistent use of protected data is hard to maintain because there can be a lot of code,
spread over a lot of classes.
The set of classes that can touch that data is open: anyone can derive a new class and start manipulating the protected data.
Often, it is not possible to examine the complete set of classes, so any change to the representation of the class becomes infeasible.
There is no enforced invariant for the protected data; it is much like a set of global variables.
The protected data has de facto become global to a large body of code.
Note
Protected data often looks tempting to enable arbitrary improvements through derivation.
Often, what you get is unprincipled changes and errors.
(Prefer private
data) with a well-specified and enforced invariant.
Alternative, and often better, (keep data out of any class used as an interface).
Note
Protected member function can be just fine.
Enforcement
Flag classes with protected
data.
C.134: Ensure all non-const
data members have the same access level
Reason
Prevention of logical confusion leading to errors.
If the non-const
data members don't have the same access level, the type is confused about what it's trying to do.
Is it a type that maintains an invariant or simply a collection of values?
Discussion
The core question is: What code is responsible for maintaining a meaningful/correct value for that variable?
There are exactly two kinds of data members:
- A: Ones that don't participate in the object's invariant. Any combination of values for these members is valid.
- B: Ones that do participate in the object's invariant. Not every combination of values is meaningful (else there'd be no invariant). Therefore all code that has write access to these variables must know about the invariant, know the semantics, and know (and actively implement and enforce) the rules for keeping the values correct.
Data members in category A should just be public
(or, more rarely, protected
if you only want derived classes to see them). They don't need encapsulation. All code in the system might as well see and manipulate them.
Data members in category B should be private
or const
. This is because encapsulation is important. To make them non-private
and non-const
would mean that the object can't control its own state: An unbounded amount of code beyond the class would need to know about the invariant and participate in maintaining it accurately -- if these data members were public
, that would be all calling code that uses the object; if they were protected
, it would be all the code in current and future derived classes. This leads to brittle and tightly coupled code that quickly becomes a nightmare to maintain. Any code that inadvertently sets the data members to an invalid or unexpected combination of values would corrupt the object and all subsequent uses of the object.
Most classes are either all A or all B:
- All public: If you're writing an aggregate bundle-of-variables without an invariant across those variables, then all the variables should be
public
. (By convention, declare such classesstruct
rather thanclass
) - All private: If you're writing a type that maintains an invariant, then all the non-
const
variables should be private -- it should be encapsulated.
Exception
Occasionally classes will mix A and B, usually for debug reasons. An encapsulated object might contain something like non-const
debug instrumentation that isn't part of the invariant and so falls into category A -- it isn't really part of the object's value or meaningful observable state either. In that case, the A parts should be treated as A's (made public
, or in rarer cases protected
if they should be visible only to derived classes) and the B parts should still be treated like B's (private
or const
).
Enforcement
Flag any class that has non-const
data members with different access levels.
C.135: Use multiple inheritance to represent multiple distinct interfaces
Reason
Not all classes will necessarily support all interfaces, and not all callers will necessarily want to deal with all operations. Especially to break apart monolithic interfaces into "aspects" of behavior supported by a given derived class.
Example
class iostream : public istream, public ostream { // very simplified // ...};
istream
provides the interface to input operations; ostream
provides the interface to output operations.
iostream
provides the union of the istream
and ostream
interfaces and the synchronization needed to allow both on a single stream.
Note
This is a very common use of inheritance because the need for multiple different interfaces to an implementation is common and such interfaces are often not easily or naturally organized into a single-rooted hierarchy.
Note
Such interfaces are typically abstract classes.
Enforcement
???
C.136: Use multiple inheritance to represent the union of implementation attributes
Reason
Some forms of mixins have state and often operations on that state. If the operations are virtual the use of inheritance is necessary, if not using inheritance can avoid boilerplate and forwarding.
Example
class iostream : public istream, public ostream { // very simplified // ...};
istream
provides the interface to input operations (and some data); ostream
provides the interface to output operations (and some data).
iostream
provides the union of the istream
and ostream
interfaces and the synchronization needed to allow both on a single stream.
Note
This a relatively rare use because implementation can often be organized into a single-rooted hierarchy.
Example
Sometimes, an "implementation attribute" is more like a "mixin" that determine the behavior of an implementation and inject
members to enable the implementation of the policies it requires.
For example, see std::enable_shared_from_this
or various bases from boost.intrusive (e.g. list_base_hook
or intrusive_ref_counter
).
Enforcement
???
C.137: Use virtual
bases to avoid overly general base classes
Reason
Allow separation of shared data and interface. To avoid all shared data to being put into an ultimate base class.
Example
struct Interface { virtual void f(); virtual int g(); // ... no data here ...};class Utility { // with data void utility1(); virtual void utility2(); // customization pointpublic: int x; int y;};class Derive1 : public Interface, virtual protected Utility { // override Interface functions // Maybe override Utility virtual functions // ...};class Derive2 : public Interface, virtual protected Utility { // override Interface functions // Maybe override Utility virtual functions // ...};
Factoring out Utility
makes sense if many derived classes share significant "implementation details."
Note
Obviously, the example is too "theoretical", but it is hard to find a small realistic example.
Interface
is the root of an (interface hierarchy)
and Utility
is the root of an (implementation hierarchy).
Here is a slightly more realistic example with an explanation.
Note
Often, linearization of a hierarchy is a better solution.
Enforcement
Flag mixed interface and implementation hierarchies.
C.138: Create an overload set for a derived class and its bases with using
Reason
Without a using declaration, member functions in the derived class hide the entire inherited overload sets.
Example, bad
#include <iostream>class B {public: virtual int f(int i) { std::cout << "f(int): "; return i; } virtual double f(double d) { std::cout << "f(double): "; return d; } virtual ~B() = default;};class D: public B {public: int f(int i) override { std::cout << "f(int): "; return i + 1; }};int main(){ D d; std::cout << d.f(2) << '\n'; // prints "f(int): 3" std::cout << d.f(2.3) << '\n'; // prints "f(int): 3"}
Example, good
class D: public B {public: int f(int i) override { std::cout << "f(int): "; return i + 1; } using B::f; // exposes f(double)};
Note
This issue affects both virtual and non-virtual member functions
For variadic bases, C++17 introduced a variadic form of the using-declaration,
template<class... Ts>struct Overloader : Ts... { using Ts::operator()...; // exposes operator() from every base};
Enforcement
Diagnose name hiding
C.139: Use final
on classes sparingly
Reason
Capping a hierarchy with final
classes is rarely needed for logical reasons and can be damaging to the extensibility of a hierarchy.
Example, bad
class Widget { /* ... */ };// nobody will ever want to improve My_widget (or so you thought)class My_widget final : public Widget { /* ... */ };class My_improved_widget : public My_widget { /* ... */ }; // error: can't do that
Note
Not every class is meant to be a base class.
Most standard-library classes are examples of that (e.g., std::vector
and std::string
are not designed to be derived from).
This rule is about using final
on classes with virtual functions meant to be interfaces for a class hierarchy.
Note
Capping an individual virtual function with final
is error-prone as final
can easily be overlooked when defining/overriding a set of functions.
Fortunately, the compiler catches such mistakes: You cannot re-declare/re-open a final
member in a derived class.
Note
Claims of performance improvements from final
should be substantiated.
Too often, such claims are based on conjecture or experience with other languages.
There are examples where final
can be important for both logical and performance reasons.
One example is a performance-critical AST hierarchy in a compiler or language analysis tool.
New derived classes are not added every year and only by library implementers.
However, misuses are (or at least have been) far more common.
Enforcement
Flag uses of final
on classes.
C.140: Do not provide different default arguments for a virtual function and an overrider
Reason
That can cause confusion: An overrider does not inherit default arguments.
Example, bad
class Base {public: virtual int multiply(int value, int factor = 2) = 0; virtual ~Base() = default;};class Derived : public Base {public: int multiply(int value, int factor = 10) override;};Derived d;Base& b = d;b.multiply(10); // these two calls will call the same function butd.multiply(10); // with different arguments and so different results
Enforcement
Flag default arguments on virtual functions if they differ between base and derived declarations.
C.hier-access: Accessing objects in a hierarchy
C.145: Access polymorphic objects through pointers and references
Reason
If you have a class with a virtual function, you don't (in general) know which class provided the function to be used.
Example
struct B { int a; virtual int f(); virtual ~B() = default };struct D : B { int b; int f() override; };void use(B b){ D d; B b2 = d; // slice B b3 = b;}void use2(){ D d; use(d); // slice}
Both d
s are sliced.
Exception
You can safely access a named polymorphic object in the scope of its definition, just don't slice it.
void use3(){ D d; d.f(); // OK}
See also
(A polymorphic class should suppress copying)
Enforcement
Flag all slicing.
C.146: Use dynamic_cast
where class hierarchy navigation is unavoidable
Reason
dynamic_cast
is checked at run time.
Example
struct B { // an interface virtual void f(); virtual void g(); virtual ~B();};struct D : B { // a wider interface void f() override; virtual void h();};void user(B* pb){ if (D* pd = dynamic_cast<D*>(pb)) { // ... use D's interface ... } else { // ... make do with B's interface ... }}
Use of the other casts can violate type safety and cause the program to access a variable that is actually of type X
to be accessed as if it were of an unrelated type Z
:
void user2(B* pb) // bad{ D* pd = static_cast<D*>(pb); // I know that pb really points to a D; trust me // ... use D's interface ...}void user3(B* pb) // unsafe{ if (some_condition) { D* pd = static_cast<D*>(pb); // I know that pb really points to a D; trust me // ... use D's interface ... } else { // ... make do with B's interface ... }}void f(){ B b; user(&b); // OK user2(&b); // bad error user3(&b); // OK *if* the programmer got the some_condition check right}
Note
Like other casts, dynamic_cast
is overused.
(Prefer virtual functions to casting).
Prefer static polymorphism to hierarchy navigation where it is possible (no run-time resolution necessary)
and reasonably convenient.
Note
Some people use dynamic_cast
where a typeid
would have been more appropriate;
dynamic_cast
is a general "is kind of" operation for discovering the best interface to an object,
whereas typeid
is a "give me the exact type of this object" operation to discover the actual type of an object.
The latter is an inherently simpler operation that ought to be faster.
The latter (typeid
) is easily hand-crafted if necessary (e.g., if working on a system where RTTI is -- for some reason -- prohibited),
the former (dynamic_cast
) is far harder to implement correctly in general.
Consider:
struct B { const char* name {"B"}; // if pb1->id() == pb2->id() *pb1 is the same type as *pb2 virtual const char* id() const { return name; } // ...};struct D : B { const char* name {"D"}; const char* id() const override { return name; } // ...};void use(){ B* pb1 = new B; B* pb2 = new D; cout << pb1->id(); // "B" cout << pb2->id(); // "D" if (pb1->id() == "D") { // looks innocent D* pd = static_cast<D*>(pb1); // ... } // ...}
The result of pb2->id() == "D"
is actually implementation defined.
We added it to warn of the dangers of home-brew RTTI.
This code might work as expected for years, just to fail on a new machine, new compiler, or a new linker that does not unify character literals.
If you implement your own RTTI, be careful.
Exception
If your implementation provided a really slow dynamic_cast
, you might have to use a workaround.
However, all workarounds that cannot be statically resolved involve explicit casting (typically static_cast
) and are error-prone.
You will basically be crafting your own special-purpose dynamic_cast
.
So, first make sure that your dynamic_cast
really is as slow as you think it is (there are a fair number of unsupported rumors about)
and that your use of dynamic_cast
is really performance critical.
We are of the opinion that current implementations of dynamic_cast
are unnecessarily slow.
For example, under suitable conditions, it is possible to perform a dynamic_cast
in fast constant time.
However, compatibility makes changes difficult even if all agree that an effort to optimize is worthwhile.
In very rare cases, if you have measured that the dynamic_cast
overhead is material, you have other means to statically guarantee that a downcast will succeed (e.g., you are using CRTP carefully), and there is no virtual inheritance involved, consider tactically resorting static_cast
with a prominent comment and disclaimer summarizing this paragraph and that human attention is needed under maintenance because the type system can't verify correctness. Even so, in our experience such "I know what I'm doing" situations are still a known bug source.
Exception
Consider:
template<typename B>class Dx : B { // ...};
Enforcement
- Flag all uses of
static_cast
for downcasts, including C-style casts that perform astatic_cast
. - This rule is part of the (type-safety profile).
C.147: Use dynamic_cast
to a reference type when failure to find the required class is considered an error
Reason
Casting to a reference expresses that you intend to end up with a valid object, so the cast must succeed. dynamic_cast
will then throw if it does not succeed.
Example
std::string f(Base& b){ return dynamic_cast<Derived&>(b).to_string();}
Enforcement
???
C.148: Use dynamic_cast
to a pointer type when failure to find the required class is considered a valid alternative
Reason
The dynamic_cast
conversion allows to test whether a pointer is pointing at a polymorphic object that has a given class in its hierarchy. Since failure to find the class merely returns a null value, it can be tested during run time. This allows writing code that can choose alternative paths depending on the results.
Contrast with (C.147), where failure is an error, and should not be used for conditional execution.
Example
The example below describes the add
function of a Shape_owner
that takes ownership of constructed Shape
objects. The objects are also sorted into views, according to their geometric attributes.
In this example, Shape
does not inherit from Geometric_attributes
. Only its subclasses do.
void add(Shape* const item){ // Ownership is always taken owned_shapes.emplace_back(item); // Check the Geometric_attributes and add the shape to none/one/some/all of the views if (auto even = dynamic_cast<Even_sided*>(item)) { view_of_evens.emplace_back(even); } if (auto trisym = dynamic_cast<Trilaterally_symmetrical*>(item)) { view_of_trisyms.emplace_back(trisym); }}
Notes
A failure to find the required class will cause dynamic_cast
to return a null value, and de-referencing a null-valued pointer will lead to undefined behavior.
Therefore the result of the dynamic_cast
should always be treated as if it might contain a null value, and tested.
Enforcement
- (Complex) Unless there is a null test on the result of a
dynamic_cast
of a pointer type, warn upon dereference of the pointer.
C.149: Use unique_ptr
or shared_ptr
to avoid forgetting to delete
objects created using new
Reason
Avoid resource leaks.
Example
void use(int i){ auto p = new int {7}; // bad: initialize local pointers with new auto q = make_unique<int>(9); // ok: guarantee the release of the memory-allocated for 9 if (0 < i) return; // maybe return and leak delete p; // too late}
Enforcement
- Flag initialization of a naked pointer with the result of a
new
- Flag
delete
of local variable
C.150: Use make_unique()
to construct objects owned by unique_ptr
s
See R.23
C.151: Use make_shared()
to construct objects owned by shared_ptr
s
See R.22
C.152: Never assign a pointer to an array of derived class objects to a pointer to its base
Reason
Subscripting the resulting base pointer will lead to invalid object access and probably to memory corruption.
Example
struct B { int x; };struct D : B { int y; };void use(B*);D a[] = {{1, 2}, {3, 4}, {5, 6}};B* p = a; // bad: a decays to &a[0] which is converted to a B*p[1].x = 7; // overwrite a[0].yuse(a); // bad: a decays to &a[0] which is converted to a B*
Enforcement
- Flag all combinations of array decay and base to derived conversions.
- Pass an array as a
span
rather than as a pointer, and don't let the array name suffer a derived-to-base conversion before getting into thespan
C.153: Prefer virtual function to casting
Reason
A virtual function call is safe, whereas casting is error-prone. A virtual function call reaches the most derived function, whereas a cast might reach an intermediate class and therefore give a wrong result (especially as a hierarchy is modified during maintenance).
Example
???
Enforcement
See C.146 and ???
C.over: Overloading and overloaded operators
You can overload ordinary functions, function templates, and operators. You cannot overload function objects.
Overload rule summary:
- (C.160: Define operators primarily to mimic conventional usage)
- (C.161: Use non-member functions for symmetric operators)
- (C.162: Overload operations that are roughly equivalent)
- (C.163: Overload only for operations that are roughly equivalent)
- (C.164: Avoid implicit conversion operators)
- (C.165: Use
using
for customization points) - (C.166: Overload unary
&
only as part of a system of smart pointers and references) - (C.167: Use an operator for an operation with its conventional meaning)
- (C.168: Define overloaded operators in the namespace of their operands)
- (C.170: If you feel like overloading a lambda, use a generic lambda)
C.160: Define operators primarily to mimic conventional usage
Reason
Minimize surprises.
Example
class X {public: // ... X& operator=(const X&); // member function defining assignment friend bool operator==(const X&, const X&); // == needs access to representation // after a = b we have a == b // ...};
Here, the conventional semantics is maintained: (Copies compare equal).
Example, bad
X operator+(X a, X b) { return a.v - b.v; } // bad: makes + subtract
Note
Non-member operators should be either friends or defined in (the same namespace as their operands). (Binary operators should treat their operands equivalently).
Enforcement
Possibly impossible.
C.161: Use non-member functions for symmetric operators
Reason
If you use member functions, you need two.
Unless you use a non-member function for (say) ==
, a == b
and b == a
will be subtly different.
Example
bool operator==(Point a, Point b) { return a.x == b.x && a.y == b.y; }
Enforcement
Flag member operator functions.
C.162: Overload operations that are roughly equivalent
Reason
Having different names for logically equivalent operations on different argument types is confusing, leads to encoding type information in function names, and inhibits generic programming.
Example
Consider:
void print(int a);void print(int a, int base);void print(const string&);
These three functions all print their arguments (appropriately). Conversely:
void print_int(int a);void print_based(int a, int base);void print_string(const string&);
These three functions all print their arguments (appropriately). Adding to the name just introduced verbosity and inhibits generic code.
Enforcement
???
C.163: Overload only for operations that are roughly equivalent
Reason
Having the same name for logically different functions is confusing and leads to errors when using generic programming.
Example
Consider:
void open_gate(Gate& g); // remove obstacle from garage exit lanevoid fopen(const char* name, const char* mode); // open file
The two operations are fundamentally different (and unrelated) so it is good that their names differ. Conversely:
void open(Gate& g); // remove obstacle from garage exit lanevoid open(const char* name, const char* mode ="r"); // open file
The two operations are still fundamentally different (and unrelated) but the names have been reduced to their (common) minimum, opening opportunities for confusion. Fortunately, the type system will catch many such mistakes.
Note
Be particularly careful about common and popular names, such as open
, move
, +
, and ==
.
Enforcement
???
C.164: Avoid implicit conversion operators
Reason
Implicit conversions can be essential (e.g., double
to int
) but often cause surprises (e.g., String
to C-style string).
Note
Prefer explicitly named conversions until a serious need is demonstrated.
By "serious need" we mean a reason that is fundamental in the application domain (such as an integer to complex number conversion)
and frequently needed. Do not introduce implicit conversions (through conversion operators or non-explicit
constructors)
just to gain a minor convenience.
Example
struct S1 { string s; // ... operator char*() { return s.data(); } // BAD, likely to cause surprises};struct S2 { string s; // ... explicit operator char*() { return s.data(); }};void f(S1 s1, S2 s2){ char* x1 = s1; // OK, but can cause surprises in many contexts char* x2 = s2; // error (and that's usually a good thing) char* x3 = static_cast<char*>(s2); // we can be explicit (on your head be it)}
The surprising and potentially damaging implicit conversion can occur in arbitrarily hard-to spot contexts, e.g.,
S1 ff();char* g(){ return ff();}
The string returned by ff()
is destroyed before the returned pointer into it can be used.
Enforcement
Flag all non-explicit conversion operators.
C.165: Use using
for customization points
Reason
To find function objects and functions defined in a separate namespace to "customize" a common function.
Example
Consider swap
. It is a general (standard-library) function with a definition that will work for just about any type.
However, it is desirable to define specific swap()
s for specific types.
For example, the general swap()
will copy the elements of two vector
s being swapped, whereas a good specific implementation will not copy elements at all.
namespace N { My_type X { /* ... */ }; void swap(X&, X&); // optimized swap for N::X // ...}void f1(N::X& a, N::X& b){ std::swap(a, b); // probably not what we wanted: calls std::swap()}
The std::swap()
in f1()
does exactly what we asked it to do: it calls the swap()
in namespace std
.
Unfortunately, that's probably not what we wanted.
How do we get N::X
considered?
void f2(N::X& a, N::X& b){ swap(a, b); // calls N::swap}
But that might not be what we wanted for generic code. There, we typically want the specific function if it exists and the general function if not. This is done by including the general function in the lookup for the function:
void f3(N::X& a, N::X& b){ using std::swap; // make std::swap available swap(a, b); // calls N::swap if it exists, otherwise std::swap}
Enforcement
Unlikely, except for known customization points, such as swap
.
The problem is that the unqualified and qualified lookups both have uses.
C.166: Overload unary &
only as part of a system of smart pointers and references
Reason
The &
operator is fundamental in C++.
Many parts of the C++ semantics assume its default meaning.
Example
class Ptr { // a somewhat smart pointer Ptr(X* pp) : p(pp) { /* check */ } X* operator->() { /* check */ return p; } X operator[](int i); X operator*();private: T* p;};class X { Ptr operator&() { return Ptr{this}; } // ...};
Note
If you "mess with" operator &
be sure that its definition has matching meanings for ->
, []
, *
, and .
on the result type.
Note that operator .
currently cannot be overloaded so a perfect system is impossible.
We hope to remedy that: Operator Dot (R2).
Note that std::addressof()
always yields a built-in pointer.
Enforcement
Tricky. Warn if &
is user-defined without also defining ->
for the result type.
C.167: Use an operator for an operation with its conventional meaning
Reason
Readability. Convention. Reusability. Support for generic code
Example
void cout_my_class(const My_class& c) // confusing, not conventional,not generic{ std::cout << /* class members here */;}std::ostream& operator<<(std::ostream& os, const my_class& c) // OK{ return os << /* class members here */;}
By itself, cout_my_class
would be OK, but it is not usable/composable with code that rely on the <<
convention for output:
My_class var { /* ... */ };// ...cout << "var = " << var << '\n';
Note
There are strong and vigorous conventions for the meaning of most operators, such as
- comparisons (
==
,!=
,<
,<=
,>
,>=
, and<=>
), - arithmetic operations (
+
,-
,*
,/
, and%
) - access operations (
.
,->
, unary*
, and[]
) - assignment (
=
)
Don't define those unconventionally and don't invent your own names for them.
Enforcement
Tricky. Requires semantic insight.
C.168: Define overloaded operators in the namespace of their operands
Reason
Readability. Ability for find operators using ADL. Avoiding inconsistent definition in different namespaces
Example
struct S { };bool operator==(S, S); // OK: in the same namespace as S, and even next to SS s;bool x = (s == s);
This is what a default ==
would do, if we had such defaults.
Example
namespace N { struct S { }; bool operator==(S, S); // OK: in the same namespace as S, and even next to S}N::S s;bool x = (s == s); // finds N::operator==() by ADL
Example, bad
struct S { };S s;namespace N { S::operator!(S a) { return true; } S not_s = !s;}namespace M { S::operator!(S a) { return false; } S not_s = !s;}
Here, the meaning of !s
differs in N
and M
.
This can be most confusing.
Remove the definition of namespace M
and the confusion is replaced by an opportunity to make the mistake.
Note
If a binary operator is defined for two types that are defined in different namespaces, you cannot follow this rule. For example:
Vec::Vector operator*(const Vec::Vector&, const Mat::Matrix&);
This might be something best avoided.
See also
This is a special case of the rule that (helper functions should be defined in the same namespace as their class).
Enforcement
- Flag operator definitions that are not in the namespace of their operands
C.170: If you feel like overloading a lambda, use a generic lambda
Reason
You cannot overload by defining two different lambdas with the same name.
Example
void f(int);void f(double);auto f = [](char); // error: cannot overload variable and functionauto g = [](int) { /* ... */ };auto g = [](double) { /* ... */ }; // error: cannot overload variablesauto h = [](auto) { /* ... */ }; // OK
Enforcement
The compiler catches the attempt to overload a lambda.
C.union: Unions
A union
is a struct
where all members start at the same address so that it can hold only one member at a time.
A union
does not keep track of which member is stored so the programmer has to get it right;
this is inherently error-prone, but there are ways to compensate.
A type that is a union
plus an indicator of which member is currently held is called a tagged union, a discriminated union, or a variant.
Union rule summary:
- (C.180: Use
union
s to save Memory) - (C.181: Avoid "naked"
union
s) - (C.182: Use anonymous
union
s to implement tagged unions) - (C.183: Don't use a
union
for type punning) - ???
C.180: Use union
s to save memory
Reason
A union
allows a single piece of memory to be used for different types of objects at different times.
Consequently, it can be used to save memory when we have several objects that are never used at the same time.
Example
union Value { int x; double d;};Value v = { 123 }; // now v holds an intcout << v.x << '\n'; // write 123v.d = 987.654; // now v holds a doublecout << v.d << '\n'; // write 987.654
But heed the warning: (Avoid "naked" union
s)
Example
// Short-string optimizationconstexpr size_t buffer_size = 16; // Slightly larger than the size of a pointerclass Immutable_string {public: Immutable_string(const char* str) : size(strlen(str)) { if (size < buffer_size) strcpy_s(string_buffer, buffer_size, str); else { string_ptr = new char[size + 1]; strcpy_s(string_ptr, size + 1, str); } } ~Immutable_string() { if (size >= buffer_size) delete[] string_ptr; } const char* get_str() const { return (size < buffer_size) ? string_buffer : string_ptr; }private: // If the string is short enough, we store the string itself // instead of a pointer to the string. union { char* string_ptr; char string_buffer[buffer_size]; }; const size_t size;};
Enforcement
???
C.181: Avoid "naked" union
s
Reason
A naked union is a union without an associated indicator which member (if any) it holds, so that the programmer has to keep track. Naked unions are a source of type errors.
Example, bad
union Value { int x; double d;};Value v;v.d = 987.654; // v holds a double
So far, so good, but we can easily misuse the union
:
cout << v.x << '\n'; // BAD, undefined behavior: v holds a double, but we read it as an int
Note that the type error happened without any explicit cast.
When we tested that program the last value printed was 1683627180
which is the integer value for the bit pattern for 987.654
.
What we have here is an "invisible" type error that happens to give a result that could easily look innocent.
And, talking about "invisible", this code produced no output:
v.x = 123;cout << v.d << '\n'; // BAD: undefined behavior
Alternative
Wrap a union
in a class together with a type field.
The C++17 variant
type (found in <variant>
) does that for you:
variant<int, double> v;v = 123; // v holds an intint x = get<int>(v);v = 123.456; // v holds a doublew = get<double>(v);
Enforcement
???
C.182: Use anonymous union
s to implement tagged unions
Reason
A well-designed tagged union is type safe. An anonymous union simplifies the definition of a class with a (tag, union) pair.
Example
This example is mostly borrowed from TC++PL4 pp216-218. You can look there for an explanation.
The code is somewhat elaborate.
Handling a type with user-defined assignment and destructor is tricky.
Saving programmers from having to write such code is one reason for including variant
in the standard.
class Value { // two alternative representations represented as a unionprivate: enum class Tag { number, text }; Tag type; // discriminant union { // representation (note: anonymous union) int i; string s; // string has default constructor, copy operations, and destructor };public: struct Bad_entry { }; // used for exceptions ~Value(); Value& operator=(const Value&); // necessary because of the string variant Value(const Value&); // ... int number() const; string text() const; void set_number(int n); void set_text(const string&); // ...};int Value::number() const{ if (type != Tag::number) throw Bad_entry{}; return i;}string Value::text() const{ if (type != Tag::text) throw Bad_entry{}; return s;}void Value::set_number(int n){ if (type == Tag::text) { s.~string(); // explicitly destroy string type = Tag::number; } i = n;}void Value::set_text(const string& ss){ if (type == Tag::text) s = ss; else { new(&s) string{ss}; // placement new: explicitly construct string type = Tag::text; }}Value& Value::operator=(const Value& e) // necessary because of the string variant{ if (type == Tag::text && e.type == Tag::text) { s = e.s; // usual string assignment return *this; } if (type == Tag::text) s.~string(); // explicit destroy switch (e.type) { case Tag::number: i = e.i; break; case Tag::text: new(&s) string(e.s); // placement new: explicit construct } type = e.type; return *this;}Value::~Value(){ if (type == Tag::text) s.~string(); // explicit destroy}
Enforcement
???
C.183: Don't use a union
for type punning
Reason
It is undefined behavior to read a union
member with a different type from the one with which it was written.
Such punning is invisible, or at least harder to spot than using a named cast.
Type punning using a union
is a source of errors.
Example, bad
union Pun { int x; unsigned char c[sizeof(int)];};
The idea of Pun
is to be able to look at the character representation of an int
.
void bad(Pun& u){ u.x = 'x'; cout << u.c[0] << '\n'; // undefined behavior}
If you wanted to see the bytes of an int
, use a (named) cast:
void if_you_must_pun(int& x){ auto p = reinterpret_cast<std::byte*>(&x); cout << p[0] << '\n'; // OK; better // ...}
Accessing the result of a reinterpret_cast
from the object's declared type to char*
, unsigned char*
, or std::byte*
is defined behavior. (Using reinterpret_cast
is discouraged,
but at least we can see that something tricky is going on.)
Note
Unfortunately, union
s are commonly used for type punning.
We don't consider "sometimes, it works as expected" a conclusive argument.
C++17 introduced a distinct type std::byte
to facilitate operations on raw object representation. Use that type instead of unsigned char
or char
for these operations.
Enforcement
???