Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Default C++ definitions in body or header files

Tags:

c++

c++11

Scott Meyer states in Effective C++: Item 30: Understand the ins and outs of inlining that constructors and destructors are often worse candidates for inlining.

Defining functions inside a class definition, requests (not commands) them implicitly to be inline. Depending on the quality of your compiler, the compiler decides whether or not (explicitly or implicitly) defined functions be actually inlined or not.

Taking all these into account, is it a better practice to explicitly define empty/copy/move constructors, copy/move assignment operators and destructors as default (i.e. with the default keyword) inside the body files than inside the header files? After all, default deals purely with implementation as opposed to the dual delete?

like image 771
Matthias Avatar asked Dec 18 '22 07:12

Matthias


2 Answers

Without ever reading "Effective C++: Item 30" I can definitely tell that it makes perfect sense to define empty-looking ctors/dtors inside .cpp:

// MyClass.h:
class MyClass
{
public:
    MyClass();
    ~MyClass();

    ...
}

// MyClass.cpp:
MyClass::MyClass() = default;
MyClass::~MyClass() = default;

This might look like waste for digital ink, but this is exactly how it has to be done for heavy classes that have large inheritance list or lots of non trivial members.

Why do I think it has to be done like this?

Because if you don't do that, then in every other translation unit where you create or delete MyClass compiler will have to emit inline code for entire class hierarchy to create/delete all members and/or base classes. In giant projects this is usually one of main reasons for builds that takes hours.

To illustrate, compare generated assembly with non-inline ctor/dtor and without. Not that if you have multi-level inheritance with virtual classes then amount of generated code grows very fast. Some call it C++ code bloat.

Basically if you have inline function in your class and you use that function in N different cpp files (or worse in some header files that are used by many other cpp files) then compiler would have to emit that code N times in N different object files, and then at link time merge all these N copies into one version. This rule applies basically to any other function, however, it's not very common to make large function inline in header files (because it's just bad). The issue with constructors, destructors and default assignment operators etc is that they may look like empty or no c++ code at all, while they actually need to perform that same operation recursively for all members and base classes and all of that results in very large amount of generated code.

like image 168
Pavel P Avatar answered Dec 20 '22 20:12

Pavel P


Another use case of defining a destructor = default inside the body file, is the PImpl idiom in combination with std::unique_ptr.

header file: example.hpp

#include <memory>

// Example::Impl is an incomplete type.

class Example
{
public:
    Example();
    ~Example();
private:
    struct Impl;
    std::unique_ptr< Impl > impl_ptr;
};

body file: example.cpp

#include "example.hpp"

struct Example::Impl
{
    ...
};

// Example::Impl is a complete type.

Example::Example()
   : impl_ptr(std::make_unique< Impl >())
{}

Example::~Example() = default; // Raw pointer in std::unique_ptr< Impl > points to a complete type so static_assert in its default deleter will not fail.

At the point in the code where std::unique_ptr< Impl > is destroyed, Example::Impl must be a complete type. Therefore, implicitly or explicitly defining Example::~Example in the header file will not compile.

A similar argument applies for the move assignment operator (since the compiler-generated version needs to destroy the original Example::Impl) and for the move constructor (since the compiler-generated version needs to destroy the original Example::Impl in case exceptions).

like image 24
Matthias Avatar answered Dec 20 '22 21:12

Matthias