【问题标题】:Why can we use `std::move` on a `const` object?为什么我们可以在 `const` 对象上使用 `std::move`?
【发布时间】:2015-04-20 03:11:45
【问题描述】:

在 C++11 中,我们可以编写如下代码:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

当我调用std::move 时,表示我要移动对象,即我将更改对象。移动const 对象是不合理的,那么为什么std::move 不限制这种行为呢?以后会是个陷阱吧?

这里的陷阱意味着布兰登在评论中提到:

”我认为他的意思是“陷阱”他鬼鬼祟祟,因为如果他不 意识到,他最终得到的副本并不是他想要的。”

在 Scott Meyers 的《Effective Modern C++》一书中,他举了一个例子:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

如果std::move 被禁止对const 对象进行操作,我们可以很容易地找出错误,对吧?

【问题讨论】:

  • 但是试着移动它。尝试改变它的状态。 std::move 本身不会对对象做任何事情。有人可能会说 std::move 名字不好。
  • 它实际上并没有移动任何东西。它所做的只是转换为右值引用。尝试CAT cat2 = std::move(cat);,假设CAT 支持常规移动分配。
  • std::move 只是一个演员表,它实际上并没有移动任何东西
  • @WhozCraig:小心,因为您发布的代码在没有警告的情况下编译和执行,这有点误导。
  • @MooingDuck 从来没有说过它不会编译。它只是因为启用了默认的 copy-ctor 才起作用。静一下,轮子就会掉下来。

标签: c++ c++11


【解决方案1】:

你忽略了一个技巧,即std::move(cat) 实际上并没有移动任何东西。它只是告诉编译器 try 移动。但是,由于您的类没有接受const CAT&& 的构造函数,它将改为使用隐式const CAT& 复制构造函数,并安全地复制。没有危险,没有陷阱。如果复制构造函数因任何原因被禁用,您将收到编译器错误。

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

打印COPY,而不是MOVE

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

请注意,您提到的代码中的错误是性能问题,而不是稳定性问题,因此此类错误永远不会导致崩溃。它只会使用较慢的副本。此外,没有移动构造函数的非常量对象也会出现这样的错误,因此仅添加const 重载不会捕获所有这些。我们可以检查从参数类型移动构造或移动赋值的能力,但这会干扰假定依赖复制构造函数的通用模板代码。 哎呀,也许有人希望能够从 const CAT&amp;&amp; 构建,我能说他不能吗?

【讨论】:

  • 上升了。值得注意的是,根据用户定义的常规移动构造函数或赋值运算符的定义隐式删除复制函数也将通过损坏的编译来证明这一点。很好的答案。
  • 另外值得一提的是,需要非const 左值的复制构造函数也无济于事。 [class.copy] §8:“否则,隐式声明的复制构造函数将具有X::X(X&amp;) 的形式”
  • 我认为他的意思不是计算机/装配术语中的“陷阱”。我认为他的意思是它偷偷摸摸地“诱捕”他,因为如果他没有意识到,他最终会得到一个不是他想要的副本。我猜..
  • 让我们在包含所有mutable 成员的CAT 对象上编写一个const CAT&amp;&amp; 构造函数:)
  • std::move 不是试图移动某物。它所做的只是将类型T 更改为T&amp;&amp;(或将const T 更改为const T&amp;&amp;)。这可以在任何地方完成,包括“移动”没有意义的地方,例如 int f(int const &amp;&amp;i) { return 2; } 这样的函数。
【解决方案2】:
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

这里我们看到std::moveT const 上的使用。它返回一个T const&amp;&amp;。我们有一个 strange 的移动构造函数,它完全采用这种类型。

它被调用了。

现在,这种奇怪的类型确实比您的提案要修复的错误更罕见。

但是,另一方面,现有的std::move 在泛型代码中效果更好,因为您不知道您正在使用的类型是T 还是T const

【讨论】:

  • +1 是第一个真正尝试解释您为什么想要const 对象上调用std::move 的答案。
  • +1 用于显示采用const T&amp;&amp; 的函数。这表达了“我将采用右值引用但我保证不会修改它”的“API 协议”。我想,除了使用 mutable 时,它​​并不常见。也许另一个用例是能够在几乎任何东西上使用forward_as_tuple,然后再使用它。
【解决方案3】:

到目前为止,其他答案被忽略的一个原因是 通用 代码在面对移动时具有弹性的能力。例如,假设我想编写一个通用函数,将所有元素从一种容器中移出,以创建另一种具有相同值的容器:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

酷,现在我可以从deque&lt;string&gt; 相对有效地创建vector&lt;string&gt;,每个string 都将在此过程中移动。

但是,如果我想从 map 迁移怎么办?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

如果std::move 坚持使用非const 参数,则move_each 的上述实例将无法编译,因为它试图移动const intmapkey_type)。但是这段代码不在乎如果它不能移动key_type。出于性能原因,它想移动mapped_type (std::string)。

对于这个示例,以及在通用编码中类似的无数其他示例,std::move移动请求,而不是移动请求。

【讨论】:

    【解决方案4】:

    我和 OP 有同样的担忧。

    std::move 不移动对象,也不保证对象是可移动的。那为什么叫搬家呢?

    我认为不可移动可能是以下两种情况之一:

    1.移动类型为 const。

    我们在语言中使用 const 关键字的原因是我们希望编译器防止对定义为 const 的对象进行任何更改。以 Scott Meyers 书中的例子为例:

        class Annotation {
        public:
         explicit Annotation(const std::string text)
         : value(std::move(text)) // "move" text into value; this code
         { … } // doesn't do what it seems to!    
         …
        private:
         std::string value;
        };
    

    字面意思是什么?将 const 字符串移动到 value 成员 - 至少,这是我在阅读解释之前的理解。

    如果语言打算在调用 std::move() 时不执行 move 或不保证 move 适用,那么在使用单词 move 时会产生误导。

    如果该语言鼓励人们使用 std::move 以获得更好的效率,它必须尽早防止这样的陷阱,尤其是对于这种明显的字面矛盾。

    我同意人们应该意识到移动一个常量是不可能的,但是这个义务不应该意味着当明显的矛盾发生时编译器可以保持沉默。

    2。该对象没有移动构造函数

    就我个人而言,正如 Chris Drew 所说,我认为这与 OP 的关注点不同

    @hvd 这对我来说似乎有点不合时宜。仅仅因为 OP 的建议并不能解决世界上所有的错误并不一定意味着它是一个坏主意(它可能是,但不是因为你给出的原因)。 ——克里斯·德鲁

    【讨论】:

      【解决方案5】:

      我很惊讶没有人提到它的向后兼容性方面。我相信,std::move 是专门设计用于在 C++11 中执行此操作的。想象一下,您正在使用严重依赖 C++98 库的遗留代码库,因此如果没有复制分配的后备,移动会破坏事情。

      【讨论】:

        【解决方案6】:

        还好你可以使用 clang-tidy 的检查来发现此类问题:https://clang.llvm.org/extra/clang-tidy/checks/performance-move-const-arg.html

        【讨论】:

          猜你喜欢
          • 2022-01-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-23
          • 1970-01-01
          • 2014-02-16
          • 2020-09-01
          相关资源
          最近更新 更多