【问题标题】:Why can I std::move elements from a const vector?为什么我可以从 const 向量中 std::move 元素?
【发布时间】:2022-01-14 12:14:52
【问题描述】:

为什么下面的代码会编译?

#include <vector>
#include <iostream>

struct Foo {
  std::vector<int> bar = {1, 2, 3};
};

int main()
{
    Foo foo1;
    const Foo& foo2 = foo1;
    
    std::vector<int> target;
    
    std::move(foo2.bar.begin(), foo2.bar.end(), std::back_inserter(target));

    return 0;
}

std::move 的文档说

此操作后,移动范围内的元素仍将 包含适当类型的有效值,但不一定是 与移动前相同的值。

所以这实际上可以改变对象 foo2,即使它被声明为 const。为什么会这样?

【问题讨论】:

  • 您正在为 Foo 结构分配内存,然后将其标记为 const,因此您正在阻止更改该变量的引用。由于vector&lt;&gt; 指向另一块内存,您可以自行调整它。如果您想阻止该向量进行更改,请将其标记为 const:const vector&lt;int&gt; nums {1, 2, 3}
  • @yemo 不确定这是否解释了整个故事。我可以简化示例代码further。即使barconst std::vector&lt;int&gt;std::move 也是允许的。
  • “不一定”是一个笼统的说法,但可能不适用于特定的场景。例如,“您收到的亲笔签名的物品可能不一定与展示的物品相同。”但是你说“啊哈,但在我的情况下,我知道该项目是独一无二的,唯一存在的项目是展示项目。'不一定'意味着商店有权力复活,可以让名人起死回生,让他们再签一件,然后卖给我吗?”
  • @nemo,您不应该使用对容器的 const 引用来改变容器元素(除非使用 mutable)。类似地,作为 const 的结构将该属性传输到其内容。所以确实,这个问题在我看来是非常有道理的!

标签: c++ language-lawyer move-semantics


【解决方案1】:

所以这实际上可以改变对象 foo2,即使它被声明为 const。为什么会这样?

std::move 算法允许移动输入元素,如果可以的话

对于每个输入元素,它执行*dest = std::move(*from),其中destfrom 是输出和输入迭代器。由于from 取消对常量对象的引用,std::move(*from) 创建了一个右值引用const int&amp;&amp;。由于ints 没有用户定义的构造函数,因此对*dest 的赋值实际上会导致该语言定义的复制构造。

如果您的元素属于类类型 T 并带有用户定义的复制和移动构造函数,则重载决策必须选择复制构造函数 (T(const T&amp;)) 而不是移动构造函数 (T(T&amp;&amp;)),因为 @987654333 @左值引用可以绑定到const右值,而非const右值引用不能(因为这需要丢弃const)。

底线是std::move(带有迭代器的算法)正在执行移动操作,它可能会或可能不会调用移动构造函数或赋值。如果调用了移动构造函数或赋值,并且移动对源具有破坏性,则算法将修改源元素。在其他情况下,它只会执行复制。

【讨论】:

    【解决方案2】:

    要通过示例演示 Andrey Semashev 的 answer,请考虑以下内容:

    #include <vector>
    
    struct movable
    {
        movable() = default;
        
        movable(const movable&) = delete;
        movable& operator=(const movable&) = delete;
    
        movable(movable&&) = default;
        movable& operator=(movable&&) = default;
    };
    
    struct copyable
    {
        copyable() = default;
        
        copyable(const copyable&) = default;
        copyable& operator=(const copyable&) = default;
    
        copyable(copyable&&) = delete;
        copyable& operator=(copyable&&) = delete;
    };
    
    int main()
    {
        // original example
        const std::vector<int> si;
        std::vector<int> ti;
        
        std::move(si.begin(), si.end(), std::back_inserter(ti)); // OK
    
        // example 2
        const std::vector<copyable> sc;
        std::vector<copyable> tc;
        
        std::move(sc.begin(), sc.end(), std::back_inserter(tc)); // OK
    
        // example 3
        const std::vector<movable> sv;
        std::vector<movable> tv;
        
        std::move(sv.begin(), sv.end(), std::back_inserter(tv)); // ERROR - tries to use copy ctor
    
        return 0;
    }
    

    尽管copyable 没有移动构造函数,但示例 2 编译时没有错误,因为std::move 在这里选择了复制构造函数。

    另一方面,示例 3 无法编译,因为 movable 的移动构造函数被 sv 的 constness 否定(更好的词?)。你得到的错误是:

    error: use of deleted function 'movable::movable(const movable&)'
    

    这是一个完整的example


    更新

    分步说明:

    1. 由于我们的类型是const std::vector&lt;T&gt;,所以它的vector::begin()函数返回const_iterator

    2. const_iteratorstd::move 算法中取消引用时,返回const T&amp;

    3. std::move算法内部使用std::move函数,例如:

      // taken from cppreference.com
      while (first != last) *d_first++ = std::move(*first++);
      
    4. std::move 函数依次返回:

      // taken from cppreference.com
      static_cast<typename std::remove_reference<T>::type&&>(t)`
      

      所以,对于const T&amp;,它返回const T&amp;&amp;

    5. 由于我们没有使用const T&amp;&amp; 参数定义的构造函数或operator=,因此重载解析会选择采用const T&amp; 的那个。

    6. 瞧。

    【讨论】:

    • 这对我来说有点出乎意料,但是当我使用 msvc 19.29 时,第二个示例的编译失败,并且说我试图引用已删除的函数 copyable::copyable(copyable &&)。其他编译器并非如此。
    • @deephace MSVC 在这种情况下会出错,因为此代码中未使用 copyable 的移动构造函数。在重载解析期间必须将其丢弃。
    • @deephace 重载解析发生在函数使用之前。只要删除的函数没有被实际选择,它在重载候选集中就不是错误。见here。例如,这就是允许移动类型(如std::unique_ptr)工作的原因。对于此类类型,复制构造函数被定义为已删除,并且当您执行移动操作时,复制和移动 ctor 都被视为候选,并选择了移动 ctor。丢弃的已删除副本 ctor 不会导致错误。
    • @deephace 这可能很有趣:stackoverflow.com/a/28595207/4358570
    • @deephace 因为它有问题 :P 我相信 Herb Sutter 是说他们会放弃它以支持 Clang。不过不要引用我的话:)
    猜你喜欢
    • 1970-01-01
    • 2015-04-20
    • 1970-01-01
    • 1970-01-01
    • 2014-02-16
    • 1970-01-01
    • 2020-09-01
    相关资源
    最近更新 更多