21

一个类必须有一个有效的复制或移动构造函数,这样的任何语法都是合法的:

C x = factory();
C y( factory() );
C z{ factory() };

在 C++03 中,依靠复制省略来防止编译器接触复制构造函数是相当普遍的。无论定义是否存在,每个类都有一个有效的复制构造函数签名。

在 C++11 中,一个不可复制的类型应该定义C( C const & ) = delete;,无论使用如何,都会使对该函数的任何引用无效(对于不可移动的类型也是如此)。(C++11 §8.4.3/2)。一方面,GCC 在尝试按值返回这样的对象时会抱怨。复制省略不再有帮助。

幸运的是,我们还有新的语法来表达意图,而不是依赖漏洞。该factory函数可以返回一个花括号初始化列表来临时就地构造结果:

C factory() {
    return { arg1, 2, "arg3" }; // calls C::C( whatever ), no copy
}

编辑:如果有任何疑问,此return语句解析如下:

  1. 6.6.3/2:“带有花括号初始化列表的返回语句通过指定初始化列表中的复制列表初始化 (8.5.4) 初始化要从函数返回的对象或引用。”
  2. 8.5.4/1:“复制初始化上下文中的列表初始化称为复制列表初始化。” ¶3:“如果 T 是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议(13.3、13.3.1.7)选择最佳构造函数。”

不要被名称copy-list-initialization误导。8.5:

13:初始化的形式(使用括号或=)通常是无关紧要的,但当初始化器或被初始化的实体具有类类型时,它就很重要;见下文。如果被初始化的实体没有类类型,则带括号的初始化器中的表达式列表应为单个表达式。

14:在表单 T x = a; 以及参数传递、函数返回、抛出异常(15.1)、处理异常(15.3)和聚合成员初始化(8.5.1)中发生的初始化称为复制初始化。

当初始化器是花括号初始化列表时,复制初始化及其替代方法直接初始化始终遵循列表初始化。添加 没有语义影响=,这是列表初始化被非正式地称为统一初始化的原因之一。

有区别:与复制初始化不同,直接初始化可能会调用显式构造函数。复制初始化初始化一个临时对象,并在转换时将其复制以初始化对象。

语句的复制列表初始化规范return { list }仅指定精确等效语法为temp T = { list };,其中=表示复制初始化。它并不立即暗示调用了复制构造函数。

-- 结束编辑。


然后可以将函数结果接收到右值引用中,以防止将临时复制到本地:

C && x = factory(); // applies to other initialization syntax

问题是,如何从返回不可复制、不可移动类型的工厂函数初始化非静态成员?引用技巧不起作用,因为引用成员不会延长临时成员的生命周期。

请注意,我不考虑聚合初始化。这是关于定义一个构造函数。

4

2 回答 2

2

关于你的主要问题:

问题是,如何从返回不可复制、不可移动类型的工厂函数初始化非静态成员?

你没有。

您的问题是您试图将两件事混为一谈:如何生成返回值以及如何在调用站点使用返回值。这两件事没有相互联系。请记住:函数的定义不会影响它的使用方式(就语言而言),因为该定义不一定对编译器可用。因此,C++ 不允许生成返回值的方式影响任何事情(除了省略,这是一种优化,而不是语言要求)。

换句话说,这个:

C c = {...};

与此不同:

C c = [&]() -> C {return {...};}()

您有一个按值返回类型的函数。它返回一个 prvalue 类型的表达式C。如果你想存储这个值,从而给它一个名字,你有两个选择:

  1. 将其存储为const&or &&。这会将临时的生命周期延长到控制块的生命周期。你不能用成员变量来做到这一点;它只能通过函数中的自动变量来完成。

  2. 将其复制/移动到一个值中。您可以使用成员变量来执行此操作,但显然它要求类型是可复制或可移动的。

如果您想存储纯右值表达式,这些是 C++ 提供给您的唯一选项。因此,您可以使类型可移动或返回一个新分配的指向内存的指针并存储它而不是一个值。

这种限制是最初创建移动的很大一部分原因:能够按价值传递事物并避免昂贵的副本。无法更改语言以强制省略返回值。因此,他们在许多情况下降低了成本。

于 2012-06-17T17:42:38.203 回答
1

此类问题是C++17 更改以允许这些初始化(并从语言中排除副本,而不仅仅是作为优化)的主要动机之一。

于 2017-09-18T01:48:24.183 回答