Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should functions declared with `= default` only go in the header file

Tags:

c++

c++11

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.

like image 458
okapi Avatar asked Jun 25 '20 16:06

okapi


People also ask

Should functions be in header files?

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.

Should all Includes go in header file?

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.

Which functions declared in header 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.

Why don't we define functions in the header file?

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.


2 Answers

An explicitly-defaulted function is not necessarily not user-provided

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.

like image 97
dfrib Avatar answered Oct 17 '22 18:10

dfrib


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

like image 22
Alecto Irene Perez Avatar answered Oct 17 '22 18:10

Alecto Irene Perez