When defining classes, it is now common to use = default
for the destructor/copy constructor and copy assignment. Looking at my code base, these are nearly always in the header files only but some colleagues have put them in the .cpp
file. What would be the best practice in this case?
Is the compiler generating these functions multiple times when it is in the header and relying on the linker to dedup them. Is it perhaps only worth putting them in the .cpp
file if you have a huge class? With our mostly old C++98 code, functions that do nothing are also often defined only in the header. Do nothing virtual destructors seem to have often been moved to the .cpp
file. Is (or was) it somehow important for virtual methods where their address is needed to populate the virtual method table.
Also is it recommended to ever put noexcept()
clauses on = default
functions? The compiler appears to derive this itself so it only serves as API documentation if it is there.
No. If you import the same header from two files, you get redefinition of function. However, it's usual if the function is inline. Every file needs it's definition to generate code, so people usually put the definition in header.
Your #include s should be of header files, and each file (source or header) should #include the header files it needs. Header files should #include the minimum header files necessary, and source files should also, though it's not as important for source files.
A header file is a file with extension . h which contains C function declarations and macro definitions to be shared between several source files. There are two types of header files: the files that the programmer writes and the files that comes with your compiler.
Inline functions are defined in header files because, in order to inline the function, the compiler needs to have the body of the function available when compiling the including source file. auto is pretty much never used.
What would be the best practice in this case?
I would recommend, as a rule of thumb, unless you explicitly and wantonly know what you are getting into, to always define explicitly-defaulted functions at their (first) declaration; i.e., placing = default
at the (first) declaration, meaning in (your case) the header (specifically, the class definition), as there are subtle but essential differences between the two w.r.t. whether a constructor is considered to be user-provided or not.
From [dcl.fct.def.default]/5 [extract, emphasis mine]:
[...] A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. [...]
Thus:
struct A {
A() = default; // NOT user-provided.
int a;
};
struct B {
B(); // user-provided.
int b;
};
// A user-provided explicitly-defaulted constructor.
B::B() = default;
Whether a constructor is user-provided or not does, in turn, affect the rules for which objects of the type are initialized. Particularly, a class type T
, when value-initialized, will first zero-initialize the object if T
's default constructor is not user-provided. Thus, this guarantee holds for A
above, but not for B
, and it can be quite surprising that a value-initialization of an object with a (user-provided!) defaulted constructor leaves data members of the object in an uninitialized state.
Quoting from cppreference [extract, emphasis mine]:
Value initialization
Value initialization is performed in these situations:
- [...]
- (4) when a named variable (automatic, static, or thread-local) is declared with the initializer consisting of a pair of braces.
The effects of value initialization are:
(1) if
T
is a class type with no default constructor or with a user-provided or deleted default constructor, the object is default-initialized;(2) if
T
is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;...
Let's apply this on the class types A
and B
above:
A a{};
// Empty brace direct-list-init:
// -> A has no user-provided constructor
// -> aggregate initialization
// -> data member 'a' is value-initialized
// -> data member 'a' is zero-initialized
B b{};
// Empty brace direct-list-init:
// -> B has a user-provided constructor
// -> value-initialization
// -> default-initialization
// -> the explicitly-defaulted constructor will
// not initialize the data member 'b'
// -> data member 'b' is left in an unititialized state
a.a = b.b; // reading uninitialized b.b: UB!
Thus, even for use cases where you will not end up shooting yourself in the foot, just the presence of a pattern in your code base where explicitly defaulted (special member) functions are not being defined at their (first) declarations may lead to other developers, unknowingly of the subtleties of this pattern, blindly following it and subsequently shooting themselves in their feet instead.
Functions declared with = default;
should go in the header file, and the compiler will automatically know when to mark them noexcept
. We can actually observe this behavior, and prove that it happens.
Let's say that we have two classes, Foo
and Bar
. The first class, Foo
, contains an int, and the second class, Bar
, contains a string. These are the definitions:
struct Foo {
int x;
Foo() = default;
Foo(Foo const&) = default;
Foo(Foo&&) = default;
};
struct Bar {
std::string s;
Bar() = default;
Bar(Bar const&) = default;
Bar(Bar&&) = default;
};
For Foo
, everything is noexcept
because creating, copying, and moving an integer is noexcept
. For Bar
on the other hand, creating and moving strings are noexcept
, but copy construction is not because it might require allocating memory, which might result in an exception if there is no more memory.
We can check if a function is noexcept by using noexcept:
std::cout << noexcept(Foo()) << '\n'; // Prints true, because `Foo()` is noexcept
Lets do this for all constructors in Foo
and Bar
:
// In C++, # will get a string representation of a macro argument
// So #x gets a string representation of x
#define IS_NOEXCEPT(x) \
std::cout << "noexcept(" #x ") = \t" << noexcept(x) << '\n';
int main() {
Foo f;
IS_NOEXCEPT(Foo()); // Prints true
IS_NOEXCEPT(Foo(f)) // Prints true
IS_NOEXCEPT(Foo(std::move(f))); // Prints true
Bar b;
IS_NOEXCEPT(Bar()); // Prints true
IS_NOEXCEPT(Bar(b)) // Copy constructor prints false
IS_NOEXCEPT(Bar(std::move(b))); // Prints true
}
This shows us that the compiler will automatically deduce whether or not a defaulted function is noexcept. You can run the code for yourself here
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With