复制构造函数是始终隐式定义,还是仅在使用时定义?

Are copy constructors defined implicitly always, or only when they are used?

提问人:R_Kapp 提问时间:6/22/2018 最后编辑:T.C.R_Kapp 更新时间:6/22/2018 访问量:352

问:

请考虑以下代码:

#include <memory>
#include <vector>

class A
{
private:
  std::vector<std::unique_ptr<int>> _vals;
};

int main()
{
  A a;
  //A a2(a);
  return 0;
}

编译器 A 编译它没有问题,除非我取消注释掉该行,此时它抱怨复制构造函数被删除,因此我无法复制构造。然而,编译器 B 提出这种抱怨,即使我把那一行注释掉了。也就是说,编译器 A 仅在我实际尝试使用它时生成隐式定义的复制构造函数,而编译器 B 则无条件地这样做。哪一个是正确的?请注意,如果我正确地使用而不是两个编译器,则隐式删除复制构造函数和赋值运算符(具有显式删除的复制构造函数,而没有)。A a2(a);std::unique_ptrAstd::unique_ptr<int> _vals;std::vector<std::unique_ptr<int>> _vals;std::unique_ptrstd::vector

(注意:在编译器 B 中编译代码非常简单 - 只需显式删除复制构造函数和赋值运算符,它就可以正常工作。这不是问题的重点;这是为了理解正确的行为。

C++ C++11 语言律师 复制构造函数 赋值运算符

评论

2赞 François Andrieux 6/22/2018
哪个编译器是编译器 B?随意使用您正在使用的编译器的真实姓名。
2赞 HolyBlackCat 6/22/2018
我无法在这里使用任何英特尔编译器进行复制......你用的是一些非常旧的版本吗?
1赞 NathanOliver 6/22/2018
编译器不应在注释时给出错误。如果是这样,它有一个错误。
9赞 NathanOliver 6/22/2018
@R_Kapp 这是应该在问题中的信息。你的“mvce”并不代表你实际在做什么。
3赞 JaMiT 6/22/2018
由于它只在使用 时发生,因此这应该是 MCVE 的一部分。(特别是如果声明要导出的类可能会导致其隐式定义的构造函数被实例化为 DLL 接口。__declspec(dllexport)

答:

0赞 SergeyA 6/22/2018 #1

虽然我无法确认这种行为(我无法访问 Windows 编译器,并且 OP 声称该错误发生在 Windows 平台上的 icc 上),但从表面上看问题,答案是 - 编译器 B 有一个严重的错误。

特别是,隐式声明的复制构造函数被定义为已删除,当...

T 具有无法复制的非静态数据成员(已删除、 无法访问或模棱两可的复制构造函数);

https://en.cppreference.com/w/cpp/language/copy_constructor

因此,符合要求的编译器必须在语义上生成已删除的复制构造函数,并成功编译程序,因为该构造函数从未调用过。

评论

0赞 R_Kapp 6/22/2018
std::vector不过,是可复制的。正如我在问题中提到的,如果我替换为 ,我不会遇到问题 - 不可复制,因此复制构造函数被正确隐式删除。,不过,这是编译器所具有的,是可复制的。std::vector<std::unique_ptr<int>> _valsstd::unique_ptr<int> _valsstd::unique_ptrstd::vector
0赞 SergeyA 6/22/2018
@R_Kapp为什么你认为 s 是可复制的?std::vectorstd::unique_ptr
0赞 Dietrich Epp 6/22/2018
@R_Kapp:不是一个类型。 不可复制。std::vectorstd::vector<std::unique_ptr<int>>
0赞 R_Kapp 6/22/2018
std::vector<T>定义了复制构造函数。它不会被删除、无法访问或模棱两可。碰巧的是,它在本例中使用的模板参数 () 是不可复制的,但这不会改变std::unique_ptrstd::vector
0赞 R_Kapp 6/22/2018
请参阅此链接作为与本答案中提供的描述不匹配的证据。std::vector<std::unique_ptr<int>>
9赞 Barry 6/22/2018 #2

[class.copy.ctor]/12

当 odr 使用 ([basic.def.odr])、常量求值需要 ([expr.const]) 或在第一次声明后显式默认时,默认且未定义为已删除的复制/移动构造函数是隐式定义的

A的复制构造函数是默认的,因此仅当它被 ODR 使用时才会隐式定义。 就是这样一个 ODR 用法 - 所以正是该语句触发了它的定义,这将使程序格式不正确。在使用 odr 之前,不应定义它。A a2(a);

编译器 B 拒绝程序是错误的。

评论

0赞 Pixelchemist 6/22/2018
你的结论是正确的 iff * 编译器 B* 拒绝代码,因为它实际上是写在问题中的。由于评论显示 __declspec(dllexport) 也在游戏中,我认为事情可能不那么微不足道。
3赞 Pixelchemist 6/22/2018 #3

注意:我的回答是基于您的评论:

[...]它仅在 Windows 上,并且只有当我将类 A 明确列为 DLL 导出(例如,通过类 __declspec(dllexport) A))时才会发生这种情况。[...]

MSDN 上,我们可以了解到,声明一个类会导出所有成员,并且需要为所有成员定义。我怀疑编译器会生成所有非 d 函数的定义以符合此规则。dllexportdelete

正如你在这里读到的,实际上,我希望假定的机制(在你的案例中定义复制构造函数以用于导出目的)检查这个特征的值(或使用类似的机制),而不是实际检查它是否会编译。这可以解释为什么当你使用而不是 .std::is_copy_constructible<std::vector<std::unique_ptr<int>>>::valuetrueunique_ptr<T>vector<unique_ptr<T>>

因此,问题在于,即使它不会编译,它实际上也定义了复制构造函数。std::vector

恕我直言,检查就足够了,因为在您发生的时候,您无法知道隐式函数是否会在您使用的地方(甚至可能是另一个项目)使用 odr。因此,我不会将其视为编译器中的错误。is_copy_constructibledllexportdllimportB

评论

0赞 R_Kapp 6/22/2018
但有时,编译器会从不同项目中的标头中内联某些内容 - 这不是一个完美的用例吗?即,如果我不声明复制构造函数或在我的模块中使用 odr-use,请不要在编译时包含它 - 而是将其内联到任何执行 odr 使用它的模块中。对我来说,这是一个完全有效的修复程序(特别是因为隐式定义的复制构造函数应该相当简单)——这会导致任何其他问题吗?
0赞 Pixelchemist 6/22/2018
@R_Kapp:另一个项目如何知道您(或您的编译器)是否在您的中定义了复制构造函数,或者当且仅当存在 odr use时,它是否应该生成一个“内联”?如果定义在头文件中,则可以内联它,但如果函数是隐式的,则无法在项目的编译器时判断 dll 是否提供定义。由于 s copy 构造函数不会隐式删除,因为 of 也不会删除 of,因此应生成隐式定义。dllYXAstd::vector
0赞 R_Kapp 6/22/2018
这是一个公平的观点......我想一个更简单的解决方法是始终内联(而不是导出)隐式定义的复制构造函数。这样,您就不会违反 ODR(从未导出任何内容),并且它仍然仅在实际使用 odr 时才被定义。
0赞 R_Kapp 6/22/2018
(如果你愿意,请随时告诉我,我应该把这个问题作为一个单独的问题来问)另外,这在带有 GNU 的 Linux 上是如何工作的?该类也在那里被列为“用于导出”,我没有隐式生成该函数。如果我在另一个模块/项目中使用它,那将如何工作?我的猜测是它会做我上面建议的事情(即,如果它是隐式的,则始终内联它),但两个操作系统之间可能存在一些我不知道的特定差异。