【问题标题】:Should I make my local variables const or movable?我应该使我的局部变量为 const 还是可移动的?
【发布时间】:2020-09-11 05:14:50
【问题描述】:

对于本地范围内的任何对象,我的默认行为是将其设为 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,因为它 (correctly) 不会将 Cake const& 转换为 Cake&&。即使对象的生命周期非常接近尾声。我们不能从我们承诺不会改变的东西中窃取资源。但这会削弱 const 正确性。

那么,我怎样才能把蛋糕也吃掉呢?

(即我如何才能拥有 const 正确性并从移动语义中受益。)

【问题讨论】:

  • 如果你真的想,你可以写一个包装类,里面有一个非常量实例,它公开一个对它的常量引用和一个move_from方法。在调试版本中,您可以添加一个断言以防止对象在被移出后被使用。但老实说,我会简单地放弃 const 并完成它。
  • 我唯一想到的就是使用 PIMPL 成语,并将 unique_ptr 转换为 impl mutable,或者移动构造 impl,作为元素 unique_ptr 点to 不会“继承”常量。这两件事都有些笨拙。
  • @HolyBlackCat 是的,我考虑过这一点,但我发现它存在三个问题。首先,它不会直观地告诉您发生了什么,我仍然必须写 W<Cake> cake 而不是 Cake const cake(这可以通过将持有的实例设置为 mutable 来缓解)。其次,auto 在窗外。第三,它剥夺了编译器的优化机会,因为持有的实例不再是 const(它只是穿上了 const 外观的衣服)。
  • @DanielLangr:const 的优化也丢失了。 (通过 const 引用进行变异仍然是可能的,而 mutate const 对象是 UB)。
  • @DanielLangr:我们可以节省一些load。在foo(const int&); const int i = 42; foo(i); return i; 我们知道我们返回 42;在int i = 42; foo(i); return i; 中,我们必须重新加载i,这可能在foo 中发生了变化。

标签: c++ c++17 move-semantics const-correctness


【解决方案1】:

我相信不可能从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 返回的对象已经是一个非常量右值,它可能会被移出(如果适用,它可能再次被省略)。

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

【讨论】:

  • 有趣。我想如果你有几个cakes 想要传递给serve_dish,这也可以。除非这些需要在同一个范围内交互。
  • @bitmask 是的,这就是这种方法的缺点——将单个作用域分成两个。
  • 嗯,如果 helper 是一个 lambda,那么 cake-interaction 将再次成为可能(有一些小警告)并且编译器应该能够内联本地 lambda。我忽略了什么吗?
  • @bitmask 我相信这是正确的。 (此类代码的读者可能会很困惑为什么会有 lambda,所以我建议添加一些解释性评论,例如指向此问题的链接:)。
  • 考虑一下,我不确定如何从 lambda 外部访问该符号(周围范围内的 Cake const* 可能是个坏主意)。所以我认为我的建议毕竟不会有什么收获。
【解决方案2】:

您确实应该继续使用变量,因为这是一种很好的做法(称为),它在推理代码时也很有帮助——即使在创建代码时也是如此。 对象不能被移动——这是一件好事——如果你从一个对象移动,你几乎总是在很大程度上修改它,或者至少是暗示的(因为基本上移动意味着窃取由原始对象)!

来自core guidelines

你不能在常量上设置竞争条件。更容易推理 当许多对象不能改变它们的值时,关于一个程序。 承诺“不改变”作为参数传递的对象的接口 大大提高可读性。

尤其是this guideline

Con.4:使用 const 定义具有不变值的对象 施工后


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

Is there a solution that does not exploit NRVO?

如果 NRVO 你认为包括保证复制省略,那么不是真的,或者是和否。这有点复杂。试图将返回值从按值返回函数中移出并不一定会按照您的想法或希望进行。此外,“无副本”总是比移动性能更好。因此,您应该尝试让编译器发挥作用,并特别依赖保证复制省略(因为您使用)。如果您有我所说的无法进行省略的复杂场景:您可以使用move 结合保证复制省略/NRVO,以避免完整复制。

所以这个问题的答案是这样的:如果你的对象已经被声明为 const,那么你几乎总是可以直接依赖复制省略/按值返回,所以使用它。否则,您还有其他情况,然后自行决定最佳方法 - 在极少数情况下,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: "));

【讨论】:

  • 你已经知道我的感受了,但这个答案仍然有用; +1。
【解决方案3】:

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

是时候改变你的“默认行为”了,因为它已经过时了。

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

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

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

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

【讨论】:

  • 这个答案是基于意见的,我不同意。在推理代码时(甚至在编程时),函数变量范围内的 const 正确性非常有帮助!
  • @darune:出于兴趣,您对没有const 的语言有什么看法? (例如 Java、Python、早期 C)。
  • 好吧,我们在这里讨论 c++。与其他语言相比,其他语言有很多其他的优点和缺点。例如。例如,我通常不会将 c++ 用于我使用 python 的东西,因为它们是两个完全不同的野兽(即使“python”也是不可变的,但工作方式有所不同)。 Java 可能更容易比较,但使用其他“范式”来获得类似的东西(即没有 setter 的 getter)并且还有 final 关键字。
  • 我不认为这是基于意见的,const 很好并且有时有助于提高可读性,但是当您知道 const 实际上会变得悲观(在某些情况下)时,最好只是做最简单的事情,即删除 const。
  • @Waqar 我同意这一点,没有理由悲观,如果它使您的生活和代码更简单,那么只需删除“const”(由于生产力,我自己经常这样做)-据说,对于库级代码和广泛的功能,它可以提供很大帮助。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    有限的解决方法是 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 容器不兼容(提供非 const 版本将在非 const 使用中利用复制)
    • 应该考虑移动后的使用(我们应该处于有效状态,通常)。要么提供owning getter,要么使用owning 检查“保护”适当的方法。

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

    【讨论】:

    • 并且可能会带回与auto_ptr 相关的问题...例如在某些极端情况下无法按预期工作。编写与预期做事方式背道而驰的代码确实是个坏主意。不确定给出答案是否是个好主意,因为 OP 可能会想编写可分割的代码!
    • @Phil1970 没关系,我已经很想去做有争议和有争议的事情了;)
    猜你喜欢
    • 2022-11-27
    • 1970-01-01
    • 1970-01-01
    • 2010-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-17
    • 2013-01-02
    相关资源
    最近更新 更多