应该将以== default声明的函数仅放在头文件中

时间:2020-06-25 16:40:51

标签: c++ c++11

在定义类时,现在通常将= default用于析构函数/复制构造函数和复制分配。从我的代码库来看,它们几乎总是仅存在于头文件中,但是一些同事已将它们放置在.cpp文件中。在这种情况下,最佳做法是什么?

编译器是否在头文件中多次生成这些函数,并依靠链接器来对它们进行分解。如果您的班级很多,也许仅值得将它们放入.cpp文件中吗?对于我们大多数都是较旧的C ++ 98代码,什么也不做的函数通常也只在标头中定义。什么也不做虚拟析构函数似乎经常被移到.cpp文件中。对于(或曾经)对于需要填充虚拟方法表的地址的虚拟方法来说,它是否重要?

是否还建议在noexcept()函数上放置= default子句?编译器似乎是自身派生的,因此如果存在,它只会用作API文档。

3 个答案:

答案 0 :(得分:11)

显式默认的功能不一定是 not 用户提供的

在这种情况下,最佳做法是什么?

根据经验,除非您明确且只想知道自己要进入的领域,否则我建议始终定义显式默认函数他们的(第一个)声明;即,将= default放在(第一个)声明中,意味着(在您的情况下)标头(具体是类定义),因为两者之间存在细微但本质上的区别。构造函数是否被认为是用户提供的

来自[dcl.fct.def.default]/5 [摘录,重点我的]:

[...]如果函数是由用户声明的,并且未在其第一个声明中明确默认或删除,则由用户提供。 [...]

因此:

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;

构造函数是否由用户提供,反过来影响初始化该类型对象的规则。特别是,如果 T是默认构造函数,则类类型T value-initialized 时将首先 zero-initialize 对象。不是用户提供的。因此,该保证适用于以上A,但不适用于B,并且使用(用户提供的!)默认构造函数<< / em>将对象的数据成员保持为未初始化状态。

引用from cppreference [摘录,强调我的]:

值初始化

在以下情况下执行值初始化:

  • [...]
  • (4),当使用由一对大括号组成的初始化程序声明命名变量(自动,静态或线程本地)时。

值初始化的影响是:

  • (1),如果T是没有默认构造函数的类类型,或者是具有用户提供的或已删除的默认构造函数对象已默认初始化;

  • (2),如果T是具有默认构造函数的类类型,该构造函数既不是用户提供也不是删除的(也就是说,它可能是带有隐式类的类-定义或默认的默认构造函数),将对象初始化为零,然后使用默认的构造函数将其默认初始化

  • ...

让我们将其应用于上面的类AB

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!

因此,即使在最终不会用脚射击的用例中,也只是在代码库中没有定义明确默认(特殊成员)功能的存在模式在他们的(第一个)声明中,可能会导致 other 开发人员,他们不知不觉地意识到了这种模式的微妙之处,盲目地跟随它,然后开枪自杀。

答案 1 :(得分:5)

= default;声明的函数应该放在头文件中,并且编译器将自动知道何时标记它们noexcept 。我们实际上可以观察到这种行为,并证明它发生了。

假设我们有两个类,FooBar。第一类Foo包含一个int,第二类Bar包含一个字符串。这些是定义:

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;
};

对于Foo,一切都是noexcept,因为创建,复制和移动整数是noexcept。另一方面,对于Bar,创建和移动字符串是noexcept,但是复制构造并不是因为它可能需要分配内存,如果没有更多的内存,则可能导致异常。

我们可以使用noexcept来检查函数是否为noexcept:

std::cout << noexcept(Foo()) << '\n'; // Prints true, because `Foo()` is noexcept

FooBar中的所有构造函数都这样做:

// 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
}

这向我们展示了编译器将自动推断默认函数是否为noexcept。 You can run the code for yourself here

答案 2 :(得分:2)

= default声明的函数只能放在头文件中

通常,类定义是放置默认定义的理想位置。

但是,有时候这不是一个选择。特别是,如果类定义不能依赖于间接成员的定义。这种情况的一个例子是使用指向不透明类型的唯一指针来实现PIMPL模式。