在定义类时,现在通常将= default
用于析构函数/复制构造函数和复制分配。从我的代码库来看,它们几乎总是仅存在于头文件中,但是一些同事已将它们放置在.cpp
文件中。在这种情况下,最佳做法是什么?
编译器是否在头文件中多次生成这些函数,并依靠链接器来对它们进行分解。如果您的班级很多,也许仅值得将它们放入.cpp
文件中吗?对于我们大多数都是较旧的C ++ 98代码,什么也不做的函数通常也只在标头中定义。什么也不做虚拟析构函数似乎经常被移到.cpp
文件中。对于(或曾经)对于需要填充虚拟方法表的地址的虚拟方法来说,它是否重要?
是否还建议在noexcept()
函数上放置= default
子句?编译器似乎是自身派生的,因此如果存在,它只会用作API文档。
答案 0 :(得分:11)
在这种情况下,最佳做法是什么?
根据经验,除非您明确且只想知道自己要进入的领域,否则我建议始终定义显式默认函数他们的(第一个)声明;即,将= 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
是具有默认构造函数的类类型,该构造函数既不是用户提供也不是删除的(也就是说,它可能是带有隐式类的类-定义或默认的默认构造函数),将对象初始化为零,然后使用默认的构造函数将其默认初始化;...
让我们将其应用于上面的类A
和B
:
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
。我们实际上可以观察到这种行为,并证明它发生了。
假设我们有两个类,Foo
和Bar
。第一类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
让Foo
和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
}
这向我们展示了编译器将自动推断默认函数是否为noexcept。 You can run the code for yourself here
答案 2 :(得分:2)
用
= default
声明的函数只能放在头文件中
通常,类定义是放置默认定义的理想位置。
但是,有时候这不是一个选择。特别是,如果类定义不能依赖于间接成员的定义。这种情况的一个例子是使用指向不透明类型的唯一指针来实现PIMPL模式。