复制初始化的奇怪行为,为什么不调用复制构造函数?

8

我正在阅读直接初始化和复制初始化之间的区别(§8.5/12):

T x(a);  //direct-initialization
T y = a; //copy-initialization

我从阅读有关复制初始化的内容中了解到,它需要可访问且非显式复制构造函数,否则程序将无法编译。我通过编写以下代码进行了验证:

struct A
{
   int i;
       A(int i) : i(i) { std::cout << " A(int i)" << std::endl; }
   private:
       A(const A &a)  {  std::cout << " A(const A &)" << std::endl; }
};

int main() {
        A a = 10; //error - copy-ctor is private!
}

GCC出现错误(ideone),提示:

prog.cpp:8: error: ‘A::A(const A&)’ is private

到目前为止一切顺利,再次确认了Herb Sutter的说法

复制初始化意味着在使用复制构造函数进行初始化之前,首先调用必要的用户定义转换,并等价于形式"T t = u;":


接下来,我通过注释“private”关键字使copy-ctor可访问。现在,自然而然地期望将打印以下内容:

A(const A&)

但令我惊讶的是,它实际上打印了以下内容(ideone):

A(int i)

为什么?
好吧,我明白了,首先使用`A(int i)`从`10`(int类型)创建了一个临时对象A,按照需要应用转换规则(§8.5/14),然后它应该调用复制构造函数来初始化a。但是它没有。为什么?
如果实现允许消除调用复制构造函数的需要(§8.5/14),那么为什么当复制构造函数被声明为private时它不接受代码?毕竟,它没有调用它。这就像一个调皮捣蛋的孩子先是烦人地要求一个“特定”的玩具,当你给他一个“特定”的玩具时,他在你背后把它扔掉了。:|
这种行为可能危险吗?我的意思是,我可能会在复制构造函数中做一些其他“有用”的事情,但如果它不调用它,它是否不改变程序的行为?

请看我在这里的问题:https://dev59.com/mW445IYBdhLWcg3ws8Zp,我相信这是一个类似的情况。 - davka
顺便提一下,“根据我对拷贝初始化的了解,它需要有可访问且非显式的拷贝构造函数,否则程序将无法编译。” - 实际上比这更复杂。在你的例子中,如果你把你的拷贝构造函数设为public但显式,它仍然可以工作。请参阅http://llvm.org/bugs/show_bug.cgi?id=8342 - Johannes Schaub - litb
@Johannes:GCC 也不接受这个:http://www.ideone.com/KiOTV - Nawaz
@Nawaz:非常好的问题!!! - Destructor
6个回答

10
你是在问为什么编译器要进行访问检查吗?C++03中的12.8/14规定:

如果一个对象的复制构造函数或复制赋值运算符被隐式使用,但特殊成员函数不可访问,则程序是非法的。

当实现“省略复制构造”(12.8/15允许)时,我认为这并不意味着复制构造函数不再“隐式使用”,而只是不会执行。
或者您是在问为什么标准这样说?如果复制省略是访问检查规则的例外,那么在成功执行省略的实现中,您的程序将是良好的,但在未执行省略的实现中将是非法的。
我相信作者会认为这是一件坏事。以这种方式编写便携式代码更容易 - 即使在您的实现中省略了复制,编译器也会告诉您是否编写了尝试复制不可复制对象的代码。我怀疑,在检查访问权限之前弄清楚优化是否成功,或者在尝试优化之后推迟访问检查,也可能会给实现者带来麻烦,尽管我不知道这是否值得考虑。

这种行为可能危险吗?我的意思是,我可能会在复制构造函数中执行其他有用的操作,但如果它不调用它,那么它是否不会改变程序的行为?

当然可能会有危险 - 复制构造函数中的副作用仅在实际复制对象时发生,并且您应该相应地设计它们:标准说可以省略复制,因此不要在复制构造函数中放置代码,除非您希望在12.8/15中定义的条件下省略它。
MyObject(const MyObject &other) {
    std::cout << "copy " << (void*)(&other) << " to " << (void*)this << "\n"; // OK
    std::cout << "object returned from function\n"; // dangerous: if the copy is
      // elided then an object will be returned but you won't see the message.
}

1
尝试回答我的问题“这种行为是否危险?”并给予+1。 - Nawaz
1
这里的基本问题不就是可访问性吗? - user2100815
@Neil:是的,但我理解“如果不调用它,是否会改变程序的行为”部分是关于普通情况下的拷贝构造函数省略,而不仅仅是关于可访问性检查的需要。通过点赞,我认为提问者也支持我的观点 :-) 我认为可访问性检查并不危险。 - Steve Jessop
2
@Nawaz:所谓“相应设计”,我基本上是指,“了解标准,不要依赖于代码被执行,即使标准表明它可能不会被执行”。在我的代码片段中,第一行表示发生了复制,并且如果发生复制,则会打印出来。第二行表示对象返回,但即使发生对象返回,它也可能不会被打印出来,因此如果您依赖于它出现,那么您将误解程序的输出。 - Steve Jessop
1
然而,这个例子有一个弱点——第二行即使在没有对象返回的情况下也会发生,特别是当对象由于任何其他原因而被复制时。我希望这不会让问题变得混乱,如果是的话,那么请想象在添加该行之前,我已经知道,在我的程序中除了(可能)与对象返回相关的副本之外,没有其他副本。 - Steve Jessop
显示剩余3条评论

5
C++明确允许多种涉及拷贝构造函数的优化,这些优化实际上会改变程序的语义。(这与大多数优化不同,因为它们不影响程序的语义)。特别地,如果编译器知道现有对象将变得不可达,它就可以重复使用该对象而不是进行拷贝。在这种情况下(拷贝构造函数),编译器允许这样做;另一个类似的情况是“返回值优化”(RVO),即如果您声明保存函数返回值的变量,则C++可以选择在调用者的框架上分配该变量,这样当函数完成时就不需要将其拷贝回调用者。
总之,在C ++中,如果定义的拷贝构造函数具有副作用或执行除拷贝以外的任何操作,那么就要小心处理。

+1。好帖子。特别是这部分内容:在编译器知道现有的对象将变得不可达时,有几种情况允许编译器重用现有对象,而不是复制一个新对象。拷贝构造就是其中之一;另一个类似的情况是“返回值优化”(RVO),如果您声明保存函数返回值的变量,则C++可以选择将其分配在调用者的框架上,这样当函数完成时,它不需要将其复制回调用者。 - Nawaz

4
在任何编译器中,在代码优化过程之前,都需要进行语法[和语义]分析过程。
如果代码不符合语法要求,则无法编译。只有在后续阶段(即代码优化)中,编译器才决定省略它创建的临时对象。
因此,您需要一个可以访问的副本构造函数。

1
这是非常明显的,Prasoon,语法分析是在优化阶段之前完成的,这就是为什么需要公开声明copy-ctor。我的问题是,为什么要这样做? - Nawaz
1
但是在语义分析时,C++编译器通常允许在大多数情况下省略复制构造函数。这不是一种常见意义上的优化。 - user2100815
2
@Prasoon 这样做是不被允许的。具体来说,在某些情况下,不构造对象是不允许的,否则整个 RAII(资源获取即初始化)机制就会崩溃。 - user2100815
1
也许现在指出“省略”并不等于“优化”是很好的。实际上,“NRO”应该是“NRE”。 - user2100815
2
@Neil:那只是“好像”规则,不是吗?这是唯一允许优化掉“未使用”对象的事情,但如果创建和销毁它具有可观察的副作用,则不允许进行该优化。任何明智的实现都定义锁操作为可观察的,即使它没有显式修改volatile对象。 - Steve Jessop
显示剩余7条评论

1

这里你可以找到这个(带上你的评论 ;)):

[标准]还说临时副本可以省略,但复制构造函数的语义约束(例如可访问性)仍然必须进行检查。


0
RVO和NRVO,伙计。这是一个完美的复制省略案例。

除非他的复制构造函数是私有的 - 我不确定是否允许省略。 - user2100815
它并没有回答我的问题。而且我知道RVO和NRVO的相关知识。 - Nawaz
实际上,仔细想想,它几乎肯定是这样的 - 我不明白为什么隐私应该有任何区别。但是在找到标准参考之前,我感到有些胆怯 :-) - user2100815

0

这是编译器的一种优化。

在评估:A a = 10; 而不是:

  1. 首先通过 A(int) 构造一个临时对象;

  2. 通过复制构造函数构造 a 并传递临时对象;

编译器将直接使用 A(int) 构造 a


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接