正文或头文件中的默认C ++定义

时间:2017-05-02 09:01:16

标签: c++ c++11

Scott Meyer在 Effective C ++中指出:第30项:理解内联的细节构造函数和析构函数通常更适合内联。

在类定义中定义函数,请求(而不是命令)隐式地内联它们。根据编译器的质量,编译器决定(明确地或隐式地)定义的函数是否实际内联。

考虑到所有这些因素,更好的做法是在body文件中明确定义空/复制/移动构造函数,复制/移动赋值运算符和析构函数(即使用default关键字)而不是头文件?毕竟,default纯粹与实施相关,而不是双delete

2 个答案:

答案 0 :(得分:4)

如果没有读过“Effective C ++:Item 30”,我可以肯定地说,在.cpp中定义空洞的ctors / dtors是完全合理的:

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

    ...
}

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

这可能看起来像数字墨水的浪费,但这正是如何为具有大型继承列表或许多非平凡成员的繁重类所做的。

为什么我认为必须这样做?

因为如果你不这样做,那么在你创建或删除MyClass编译器的每个其他翻译单元中,必须为整个类层次结构发出内联代码,以创建/删除所有成员和/或基类。在大型项目中,这通常是构建需要数小时的主要原因之一。

为了说明,比较生成的汇编with non-inline ctor/dtorwithout。如果您使用虚拟类进行多级继承,那么生成的代码量就会非常快。有人称之为C ++代码膨胀。

基本上如果你的类中有内联函数,你在N个不同的cpp文件中使用该函数(或者在许多其他cpp文件使用的某些头文件中更糟糕),那么编译器必须在N中发出N次代码不同的目标文件,然后在链接时将所有这N个副本合并为一个版本。此规则基本上适用于任何其他函数,但是,在头文件中使大型函数内联并不常见(因为它很糟糕)。构造函数,析构函数和默认赋值运算符等的问题在于它们看起来可能看起来像空或没有c ++代码,而它们实际上需要递归地为所有成员和基类执行相同的操作,并且所有这些都导致非常大的数量生成的代码。

答案 1 :(得分:0)

在正文文件中定义析构函数= default的另一个用例是PImpl idiomstd::unique_ptr的组合。

标题文件:example.hpp

#include <memory>

// Example::Impl is an incomplete type.

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

正文档: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.

在销毁std::unique_ptr< Impl >的代码中,Example::Impl必须是完整类型。因此,在头文件中隐式或显式定义Example::~Example将无法编译。

类似的参数适用于移动赋值运算符(因为编译器生成的版本需要销毁原始Example::Impl)和移动构造函数(因为编译器生成的版本需要销毁原始{{1在例外情况下)。