我应该使局部变量为常量还是可移动的?

Should I make my local variables const or movable?

对于本地范围内的任何对象,我的默认行为是使其成为 const。例如:

auto const cake = bake_cake(arguments);

我尽量减少非功能性代码,因为这会增加可读性(并为编译器提供一些优化机会)。所以在类型系统中也反映这一点是合乎逻辑的。

然而,使用移动语义,这会产生问题:如果我的 cake 很难或不可能复制,而我想在完成后将其传递出去怎么办?例如:

if (tastes_fine(cake)) {
  return serve_dish(cake);
}

据我了解 copy elision rules 不能保证 cake 副本会被删除(但我不确定这一点)。

所以,我不得不将 cake 移出:

return serve_dish(std::move(cake)); // this will not work as intended

但是 std::movedo nothing useful, as it (correctly) 不会将 Cake const& 转换为 Cake&&。即使对象的生命周期非常接近尾声。我们不能从我们承诺不会改变的东西中窃取资源。但是这样会削弱const-correctness.

那么,我的蛋糕怎么吃也吃不完?

(即我怎样才能拥有常量正确性并从移动语义中受益。)

在我看来,如果您想要 move,那么 "const correct" 不声明它 const 会更好,因为您将(!)更改它。 这是意识形态的矛盾。您不能同时移动某物并留在原地。 您的意思是,该对象在某些范围内的一段时间内将是 const。在这种情况下,您可以声明对它的 const 引用,但在我看来,这会使代码复杂化并且不会增加安全性。 反之亦然,如果您在 std::move() 之后不小心使用了对对象的 const 引用,则会出现问题,尽管它看起来像是在使用 const 对象。

一个有限的解决方法是 const move constructor:

class Cake
{
public:
    Cake(/**/) : resource(acquire_resource()) {}
    ~Cake() { if (owning) release_resource(resource); }

    Cake(const Cake& rhs) : resource(rhs.owning ? copy_resource(rhs.resource) : nullptr) {}
    // Cake(Cake&& rhs) // not needed, but same as const version should be ok.
    Cake(const Cake&& rhs) : resource(rhs.resource) { rhs.owning = false; }

    Cake& operator=(const Cake& rhs) {
        if (this == &rhs) return *this;
        if (owning) release_resource(resource);
        resource = rhs.owning ? copy_resource(rhs.resource) : nullptr;
        owning = rhs.owning;
    }
    // Cake& operator=(Cake&& rhs) // not needed, but same as const version should be ok.
    Cake& operator=(const Cake&& rhs) {
        if (this == &rhs) return *this;
        if (owning) release_resource(resource);
        resource = rhs.resource;
        owning = rhs.owning;
        rhs.owning = false;
    }
    // ...

private:
    Resource* resource = nullptr;
    // ...
    mutable bool owning = true;
};
  • 需要额外的可变成员。
  • 与将执行复制而不是移动的标准容器不兼容(提供非 const 版本将在非 const 用法中利用复制)
  • 应考虑移动后的用法(通常我们应该处于有效状态)。要么提供 owning getter,要么 "protect" 适当的方法 owning 检查。

我个人会在使用 move 时删除 const

我认为不可能从 const 对象移动,至少对于标准移动构造函数和非 mutable 成员是不可能的。但是,它可以有一个 const 自动本地对象和 apply copy elision(即 NRVO)。在您的情况下,您可以按如下方式重写原始函数:

Cake helper(arguments)
{
   const auto cake = bake_cake(arguments);
   ...  // original code with const cake
   return cake;  // NRVO 
}

然后,在你原来的函数中,你可以调用:

return serve_dish(helper(arguments));

由于 helper 返回的对象已经是一个非 const 右值,它可能被移出(如果适用,它可能再次被省略)。

Here 是演示此方法的现场演示。请注意,在生成的程序集中没有调用 copy/move 构造函数。

如果可以的话,让它们可以移动。

是时候改变你的“默认行为”了,因为它不合时宜。

如果从一开始就将移动语义内置到语言中,那么制作自动变量 const 很快就会成为糟糕的编程习惯。

const 从未打算用于 micro-optimisations。 Micro-optimisations 最好留给编译器。 const 主要为成员变量和成员函数而存在。它还有助于稍微清理语言:例如"foo" 是一个 const char[4] 类型,而在 C 中它是一个 char[4] 类型,奇怪的是你不能修改内容。

现在(C++11 起)const 如您所见,自动变量实际上是有害的,是时候停止这种做法了。 const 参数 by-value 类型也是如此。您的代码也不会那么冗长。

我个人更喜欢不可变对象而不是const对象。

你确实应该继续让你的变量 as that is good practice (called ) and it also helps when reasoning about code - even while creating it. A 对象不能被移动 ​​- 这是一件好事 - 如果你从一个对象移动你几乎总是在很大程度上修改它或者至少是隐含的(因为基本上移动意味着窃取原始对象拥有的资源)!

来自core guidelines

You can’t have a race condition on a constant. It is easier to reason about a program when many of the objects cannot change their values. Interfaces that promises “no change” of objects passed as arguments greatly increase readability.

特别是 this guideline :

Con.4: Use const to define objects with values that do not change after construction


继续下一个问题的主要部分:

Is there a solution that does not exploit NRVO?

如果 NRVO 要求包含 保证复制省略,则不是,或者同时是和否。这有点复杂。尝试通过值函数将 return 值移出 return 并不一定会按照您的想法或希望的那样进行。此外,“不复制”总是比移动更好 performance-wise。因此,您应该尝试让编译器发挥它的魔力,并特别依赖 保证复制省略(因为您使用 )。如果你有我称之为不可能省略的复杂场景:然后你可以使用 move 结合 保证副本 elision/NRVO,以避免完整副本。

所以这个问题的答案是这样的:如果你的对象已经被声明为 const,那么你几乎总是可以直接依赖 copy-elision/return 的值,所以使用它。否则,您会遇到其他一些情况,然后谨慎选择最佳方法 - 在极少数情况下,move 可能是有序的(意味着它与 copy-elision 组合)。

'complex' 场景示例:

std::string f() {
  std::string res("res");
  return res.insert(0, "more: ");//'complex scenario': a reference gets returned here will usually mean a copy is invoked here.
}

'fix' 的更好方法是使用 copy-elision 即:

return res;//just return res as we already had that thus avoiding copy altogether - it's possible that we can't use this solution for more *hairy/complex* scenarios.

在此示例中 'fix' 的劣势方法是;

return std::move(res.insert(0, "more: "));