【问题标题】:returning constant object and assigning it to non-constant object返回常量对象并将其分配给非常量对象
【发布时间】:2015-09-03 08:18:07
【问题描述】:

我发现了一个明显忽略 const-ness 的代码的奇怪行为:

#include <iostream>

using std::cerr;

class A
{
public:
    A() { cerr << "A::A()\n"; }
    A(const A &a) { cerr << "A::A(const A&)\n"; }
    A(A &&) { cerr << "A::A(A&&)\n"; }
    A & operator = (const A &a) { cerr << "A::operator=(const A&)\n"; return *this; }
    A & operator = (A &&a) { cerr << "A::operator(A&&)\n"; return *this; }
    ~A() { cerr << "A::~A()\n"; }

    const A get() const { cerr << "const A A::get() const\n"; return A(); }
    A get() { cerr << "A A::get()\n"; return A(); }
};

int main()
{
    const A a;
    A b = a.get();
}

首先,我在这里所期望的:a 是一个常量,因此调用了 get() 的常量版本。接下来,常量对象被返回,但左侧是非常量对象b,所以应该调用复制构造函数。哪个不是:

A::A()
const A A::get() const
A::A()
A::~A()
A::~A()

这种行为是 c++ 标准所期望的吗? 那么,RVO 可以简单地忽略临时对象的常量性吗?以及如何在此处强制执行复制?

禁用复制省略的输出 (-fno-elide-constructors) 进行了额外的移动和预期的复制构造函数调用:

A::A()
const A A::light_copy() const
A::A()
A::A(A&&)
A::~A()
A::A(const A&)
A::~A()
A::~A()
A::~A()

如果a对象不是常量,那么就是两步不复制,这也是意料之中的。

PS。这种行为对我来说很重要,因为我看到的是打破浅拷贝 const-strictness:对于 get() 的 const 版本(实际上是 shallow_copy()),我需要确保不会修改返回的对象,因为返回的对象是浅拷贝,对浅拷贝的修改会影响“父”对象(可能是常量)。

【问题讨论】:

  • 实际返回值优化 (RVO)。
  • “light_copy”是指“浅拷贝”吗?
  • 您确定要在get 方法中返回新创建的对象A() 而不是*this?如果是这样,这些方法可以是static。但是在这种情况下,编译器只接受其中一个。
  • T.C. 的回答告诉您,您看到的行为是标准规定的。我怀疑你在混淆概念。对浅拷贝的更改将始终“传播”到浅拷贝的“源”。现在听起来你想要一个浅拷贝,直到浅拷贝被改变。那将是写时复制或句柄习语。请注意,写时复制很难正确处理,并且在多线程世界中表现不佳(这就是标准不再允许使用 CoW 实现 std::string 对象的原因)。

标签: c++ c++11 constants copy-constructor


【解决方案1】:

那么,RVO 可以简单地忽略临时对象的常量性吗?

是的。 [class.copy]/p31(引用 N4527,其中包含一些澄清意图的 DR;强调我的):

这种复制/移动操作的省略,称为复制省略,是 在以下情况下允许(可以结合到 消除多个副本):

  • 在具有类返回类型的函数中的 return 语句中,当 表达式 是非易失性自动对象的名称(其他 比函数参数或由 handler (15.3) 的 exception-declaration 具有与函数返回类型相同的类型 (忽略 cv-qualification),复制 /移动 直接构造自动对象可以省略操作 进入函数的返回值
  • [...]
  • 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同类型的类对象时 (忽略 cv-qualification),可以省略复制/移动操作 将临时对象直接构造到 省略复制/移动
  • [...]

第三个项目符号在这里适用;请注意,类似的规则也适用于 NRVO(第一个项目符号)。

【讨论】:

  • 感谢您的澄清;有什么想法可以在这里强制执行复制吗?
  • @AlexanderSergeev 您希望通过复制这里实现什么目标?您的函数返回 new 对象;如果您强制复制,您会将一个新对象复制到另一个新对象中,然后丢弃第一个对象。标准允许复制省略的原因是为了避免创建和丢弃多余的对象;它应该对代码的实际行为没有影响。
  • @KyleStrand 我实现shallow_copy实际上需要所有这些:如果父对象是常量,我需要确保shallow_copy的const-overload将返回常量对象。复制构造函数创建一个深层副本。所以,关键是const A parent; const A copy = parent.shallow_copy(); 按预期工作,const A parent; A copy = parent.shallow_copy(); 使用额外的复制构造函数(因为返回类型是常量而左侧不是),因此进行了深层复制。但它不起作用,因为 NVRO 绕过了 const'ness 限制。
  • @AlexanderSergeev 如果您有一些必须由析构函数清理的数据的句柄,那么const 将无济于事,因为析构函数不能是 CV 限定的。我认为您在这里可能需要的是返回一个右值限定的实例,而不是一个 const 限定的实例。
  • @AlexanderSergeev 使用语言,而不是反对它。如果您想要 A 的常量视图,请创建一个仅提供常量视图的 A_view 类。
【解决方案2】:

如果你想禁止从 const 临时构造/赋值,你可以将这些方法标记为已删除:

A(const A &&) = delete;
A& operator = (const A &&) = delete;

Live Demo

【讨论】:

  • 我收到一个错误:use of deleted function ‘A::A(A&amp;&amp;)’ 当移动构造函数/赋值运算符被删除时(gcc-4.9.3)
  • 我添加了一个const,它对非 const 对象按预期工作:Demo
  • 好的,但我也希望能够使用常量对象
  • 你可以使用 const 引用。我的意思是const A&amp; b = a.get();
  • 嗯,是的,这是一种方法;但我更喜欢隐含的。
猜你喜欢
  • 1970-01-01
  • 2019-02-05
  • 2017-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-15
  • 2019-07-07
相关资源
最近更新 更多