When you declare a class in C++, the compiler automatically generates the default constructor, copy constructor, copy assignment operator, move constructor, move assignment operator and the destructor, if you don't explicitly declare them. Nevertheless, as soon as you start declaring some constructor with arguments, the compiler no longer generates the default constructor. The above mechanism forces you to create empty and redundant constructor declarations just to keep the basic initialization available or to make it impossible to perform some operations like copying. The = default default and = delete delete specifiers enable the explicit control over the generated functions and disallowed functions.
Let us see the issue with automatic generation of constructor:
class Widget {
private:
int m_id;
public:
Widget(int id) : m_id{ id } {}
};
int main() {
// Widget w1; // Compiler error: no default constructor exists
Widget w2{ 42 }; // OK
return 0;
}
Since a user-defined constructor was declared, the compiler stops generation of the default constructor. To be able to create a Widget widget without an argument, you have to provide a default constructor. However, this approach had some technical drawbacks. The empty body Widget() {} turns the constructor into user-provided and changes the compiler treatment of the class, making it impossible to be considered as aggregate or trivial and disabling optimizations.
To avoid all the drawbacks and return the default constructor, you can use the = default default specifier:
The = default default specifier forces the compiler to generate the operation in accordance with memberwise rules at the specified point. As a result, the corresponding function may be defined as deleted due to the absence of this function for some base or member. Defaulting outside the class affects triviality and is supposed to be deliberate.
class Widget {
private:
int m_id{ 0 }; // In-class initializer
public:
Widget() = default; // Compiler generates the default constructor
Widget(int id) : m_id{ id } {}
};
int main() {
Widget w1; // OK, m_id is initialized to 0
Widget w2{ 42 }; // OK
return 0;
}
Appending the = default default specifier to the function declaration means to ask the compiler to generate the default constructor in exactly the way, the compiler did it before. This allows the constructor to remain non-user-provided and enables the optimization of the object creation and memory layout. You can use the = default default specifier with any of the special member functions, namely copy constructors, move constructors and destructors.
While the = default default specifier forces the compiler to generate a function, the = delete delete specifier does the opposite thing. It explicitly prohibits calling the function. Such behavior is very useful when some operations cannot be allowed like copying of an object. If your class provides management of a unique system resource, for example a file descriptor, the object should never be copied since it leads to the closing of the same file more than once.
It is possible to prohibit copying in the following way:
The = delete delete specifier participates in overload resolution and produces diagnostics if it is selected. It is possible to forbid copying, prevent unwanted conversions or forbid some overloads. Deleting the broad template or numeric overload needs to be done carefully since it forbids calls that could be used some safe conversion otherwise.
#include <utility>
class UniqueFile {
private:
int m_file_descriptor;
public:
UniqueFile(int fd) : m_file_descriptor{ fd } {}
// Prevent copying
UniqueFile(const UniqueFile& other) = delete;
UniqueFile& operator=(const UniqueFile& other) = delete;
// Allow moving
UniqueFile(UniqueFile&& other) noexcept : m_file_descriptor{ other.m_file_descriptor } {
other.m_file_descriptor = -1;
}
UniqueFile& operator=(UniqueFile&& other) noexcept {
if (this != &other) {
m_file_descriptor = other.m_file_descriptor;
other.m_file_descriptor = -1;
}
return *this;
}
};
int main() {
UniqueFile file1{ 3 };
// UniqueFile file2{ file1 }; // Compiler error: use of deleted function
UniqueFile file3{ std::move(file1) }; // OK: moving is allowed
return 0;
}
Any attempt to call a function that is marked as deleted results in compilation errors. Thus, the bugs related to resource management are shifted from run-time to compile-time checks.
The = delete delete specifier is not limited with the usage with special member functions only. You can use it with regular member functions and free functions in order to prohibit some implicit type conversions. For example, when your function takes an int integer argument, the C++ language allows callers to pass a double floating-point number as an argument, converting it with truncation. You can prevent this behavior by deleting the corresponding double overload of the function:
class NumberBox {
public:
void set_value(int val) {
// Process integer
}
// Delete the double overload to prevent implicit truncation
void set_value(double) = delete;
};
int main() {
NumberBox box;
box.set_value(10); // OK
// box.set_value(3.14); // Compiler error: use of deleted function
return 0;
}
Be careful about the interaction between deleted functions and overload resolution. The deletion of the function overload does not remove the function from the search list of the compiler. On the contrary, the compiler uses it during the overload resolution.
As soon as the compiler detects that the deleted function is the best match for the call, it terminates the search and produces the compilation error, rather than selects some other compatible overload. Such behavior guarantees the prohibition of unwanted implicit conversions.
Exercise: Deleted and Defaulted FunctionsStarter code, hints and solutionOpenClose
Practice: Deleted and Defaulted Functions
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Add a const id accessor to Widget, create w3 with id 7, and print the ids of all three Widgets. Explain the defaulted versus int-taking constructor choices.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
class Widget {
private:
int m_id{ 0 }; // In-class initializer
public:
Widget() = default; // Compiler generates the default constructor
Widget(int id) : m_id{ id } {}
};
int main() {
Widget w1; // OK, m_id is initialized to 0
Widget w2{ 42 }; // OK
return 0;
}Hints
Widget() = default requests the generated default constructor, which uses the m_id initializer of 0. Widget{42} selects the int-taking constructor instead.
A const int-returning member can expose m_id without permitting mutation. Read the in-class initializer when predicting w1.
Worked solution
A third Widget is initialized with 7 and a const accessor exposes the three ids. The defaulted constructor uses the in-class initializer 0, while the int-taking constructor uses the supplied argument. An accessor is a normal function call, not construction.
#include <iostream>
class Widget {
private:
int m_id{ 0 }; // In-class initializer
public:
int id() const { return m_id; }
Widget() = default; // Compiler generates the default constructor
Widget(int id) : m_id{ id } {}
};
int main() {
Widget w1; // OK, m_id is initialized to 0
Widget w2{ 42 }; // OK
Widget w3{7};
std::cout << "Widget ids: " << w1.id() << ' ' << w2.id() << ' ' << w3.id() << '\n';
return 0;
}
Example output
Widget ids: 0 42 7
0 comments
No comments yet. Be the first.