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

Should I make my local variables const or movable?

本文关键字:可移动 常量 我的 局部变量 我应该      更新时间:2023-10-16

对于本地作用域中的任何对象,我的默认行为都是将其设为const。例如:

auto const cake = bake_cake(arguments);

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

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

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

根据我对副本省略规则的理解,不能保证cake副本会被省略(但我对此不确定)。

因此,我必须将cake移出:

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

但是std::move不会有任何作用,因为它(正确地)不会将Cake const&强制转换为Cake&&。即使物体的寿命已经接近尾声。我们不能从我们承诺不会改变的事情中窃取资源。但这会削弱const的正确性。

那么,我怎么能既吃蛋糕又吃蛋糕呢

(即,我如何既有常量正确性,又能从移动语义中获益。)

我认为不可能从const对象中移动,至少使用标准的移动构造函数和非mutable成员是不可能的。然而,可以有一个const自动本地对象,并为其应用复制省略(即NRVO)。在您的情况下,您可以重写原始函数如下:

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

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

return serve_dish(helper(arguments));

由于helper返回的对象已经是一个非常值,因此可以将其从中移动(如果适用,也可以将其删除)。

下面是一个演示这种方法的现场演示。请注意,在生成的程序集中没有调用复制/移动构造函数。

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

您确实应该继续将变量设为常量,因为这是一种很好的做法(称为常量正确性),而且在对代码进行推理时也会有所帮助,甚至在创建代码时也是如此。const对象不能从中移动-这是一件好事-如果你从一个对象中移动,你几乎总是在很大程度上修改它,或者至少这是隐含的(因为基本上移动意味着窃取原始对象拥有的资源)!

从核心指导方针来看:

你不能在常数上有竞争条件。推理更容易关于程序,当许多对象无法更改其值时。承诺"不更改"作为参数传递的对象的接口大大提高了可读性。

,尤其是本指南:

Con.4:使用const定义具有在构造后不会更改的值的对象


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

Is there a solution that does not exploit NRVO?

如果通过NRVO,您认为包含保证的副本省略,那么不是真的,或者同时包含是和否。这有点复杂。试图将返回值从逐值返回函数中移出并不一定能达到你所想或希望的效果;无副本";总是比移动性能好。因此,您应该尝试让编译器发挥它的魔力,尤其是依赖保证的副本省略(因为您使用的是c++17)。如果你有一个我称之为不可能省略的复杂场景:那么你可以使用move保证拷贝省略/NRVO相结合,以避免完整拷贝。

因此,这个问题的答案是:如果您的对象已经声明为const,那么您几乎总是可以直接依赖于copy-elision/return-by-value,所以请使用它。否则,您会遇到其他情况,然后自行决定最佳方法——在极少数情况下,move可能是有序的(这意味着它与副本省略相结合)

"复杂"场景示例:

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.
}

"修复"的高级方法是使用复制省略,即:

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.

在这个例子中,"修复"的较差方式是;

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

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

是时候改变你的";默认行为";因为这是不合时宜的。

如果移动语义从一开始就被构建到语言中,那么生成自动变量const将很快成为糟糕的编程实践。

const从未用于微观优化。微观优化最好留给编译器。const主要用于成员变量和成员函数。这也有助于清理语言:例如,"foo"const char[4]类型,而在C中,它是char[4]类型,并且奇怪地理解不允许修改内容。

现在(由于C++11)自动变量的const实际上可能是有害的,正如您所观察到的,现在是停止这种做法的时候了。const参数的值类型也是如此。您的代码也不会那么冗长。

就我个人而言,比起const对象,我更喜欢不可变的对象。

一个有限的解决方法是const-move构造函数:

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;
};
  • 需要额外的可变成员
  • 与将执行复制而非移动的std容器不兼容(提供非常量版本将在非常量使用中利用复制)
  • 应该考虑移动后的使用情况(通常情况下,我们应该处于有效状态)。要么提供owninggetter,要么用owning检查"保护"适当的方法

当使用移动时,我个人只会放下const