string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable Declarations= delete
)Originally posted as TotW #143 on March 2, 2018
Updated 2020-04-06
Quicklink: abseil.io/tips/143
Interfaces, in a general sense, normally define the set of operations which can be invoked. Yet sometimes we may want to express the opposite: explicitly define a set of operations which should not be used. For example, disabling the copy constructor and copy assignment operator is a common way to restrict copy semantics for a particular type.
The language offers multiple options to effect such restrictions (and we’ll explore each one shortly):
The pre-C++11 techniques range from runtime checks (#1) to compile-time (#2) or link time (#3) diagnostics. While battle-proven, these techniques are far from perfect: a runtime check is not ideal for the majority of situations where the constraint is static, and the link-time check delays the diagnostic to very late in the build process. Moreover, link time diagnostics are not guaranteed (missing a definition for an ODR-used function is an ODR violation) and the actual diagnostic messages are rarely developer-friendly.
A compile time check is better, but still flawed. It only works for member functions and is based on accessibility constraints, which are verbose, error-prone and susceptible to loopholes. Moreover, the errors that result from referencing such functions can be misleading, referring as they do to access restrictions rather than interface misuse.
The application of #2 and #3 to disable copying would look like this:
class MyType { private: MyType(const MyType&); // Not defined anywhere. MyType& operator=(const MyType&); // Not defined anywhere. // ... };
Manually applying this for every class gets old really fast, so developers commonly package them in one of these ways:
The “mixin” approach (boost::noncopyable, non-copyable mixin)
class MyType : private NoCopySemantics { ... };
The macros approach
class MyType { private: DISALLOW_COPY_AND_ASSIGN(MyType); };
C++11 addressed the need for a better solution through a new language feature: deleted definitions [dcl.fct.def.delete]. (See “deleted definitions” in the C++ standard draft.) Any function can be explicitly defined as deleted:
void foo() = delete;
The syntax is straightforward, resembling defaulted functions, although with a couple of notable differences:
=default
, which works only with special member functions).=default
).The key thing to keep in mind is that =delete
is a function definition (it
does not remove or hide the declaration). The deleted function is thus defined
and participates in name lookup and overload resolution as any other function.
It’s a special kind of “radioactive” definition which says “don’t touch!”.
Attempts to use a deleted function result in a compile time error with a clear diagnostic, which is one of the key benefits over the pre-C++11 techniques.
class MyType { public: // Disable default constructor. MyType() = delete; // Disable copy (and move) semantics. MyType(const MyType&) = delete; MyType& operator=(const MyType&) = delete; //... };
// error: call to deleted constructor of 'MyType' // note: 'MyType' has been explicitly marked deleted here // MyType() = delete; MyType x; void foo(const MyType& val) { // error: call to deleted constructor of 'MyType' // note: 'MyType' has been explicitly marked deleted here // MyType(const MyType&) = delete; MyType copy = val; }
Note: by explicitly defining the copy operations as deleted we also suppress
the move operations (having user-declared copy operations inhibits the implicit
declaration of the move operations). If the intention is to define a move-only
type using the implicit move operations, =default
can be used to “bring
them back”, for example:
MyType(MyType&&) = default; MyType& operator=(MyType&&) = default;
While the examples above are centered on copy semantics (which is likely the most common case), any function (member or not) can be deleted.
Since deleted functions participate in overload resolution they can help catch
unintended uses. Let’s say we have the following overloaded print
function:
void print(int value); void print(absl::string_view str);
Calling print('x')
will print the integer value of ‘x’, when the developer
likely intended print("x")
. We can catch this:
void print(int value); void print(const char* str); // Use string literals ":" instead of character literals ':'. void print(char) = delete;
Note that =delete
doesn’t affect just function calls. Attempting to take the
address of a deleted function will also result in a compilation error:
void (*pfn1)(int) = &print; // ok void (*pfn2)(char) = &print; // error: attempt to use a deleted function
This example is extracted from a real world application: absl::StrCat(). Deleted functions are valuable any time a particular part of an interface must be restricted.
Defining destructors as deleted is stricter than making them private (although this is a big hammer and it may introduce more limitations than intended)
// A _very_ limited type: // 1. Dynamic storage only. // 2. Lives forever (can't be destructed). // 3. Can't be a member or base class. class ImmortalHeap { public: ~ImmortalHeap() = delete; //... };
Yet another example, this time we want to only allow the allocation of non-array objects ([real world example][crashpad]):
// Don't allow new T[]. class NoHeapArraysPlease { public: void* operator new[](std::size_t) = delete; void operator delete[](void*) = delete; }; auto p = new NoHeapArraysPlease; // OK // error: call to deleted function 'operator new[]' // note: candidate function has been explicitly deleted // void* operator new[](std::size_t) = delete; auto pa = new NoHeapArraysPlease[10];
=delete
offers an explicit way to express parts of an interface which should
not be referenced, also enabling better diagnostics than the pre-C++11 idioms.
No piece of code, including compiler generated code, can reference a deleted
function. For nuanced access control, the access specifiers or more elaborate
techniques (for example, the passkey idiom as discussed in Tip #134) are
more appropriate.
Important: Since the deleted definitions are part of the interface they should have the same access specifier as the other parts of the interface. Concretely, this means they should usually be public. In practice this also results in the best diagnostics (private and =delete doesn’t make much sense).
Credits: This tip includes key contributions and feedback from many people, special thanks to: Mark Mentovai, James Dennett, Bruce Dawson and Yitzhak Mandelbaum.