vector::push_back and std::move

4
我尝试了以下代码:
#include <iostream>

struct test{
    test(){}
    test(test const &){
        std::cout<<__LINE__<<"\n";
    }
    test(test&&){
        std::cout<<__LINE__<<"\n";
    }
};

#include <vector>
#include <utility> // std::move
int main(){
    auto&& tmp = test();
    std::vector<test> v;
    v.push_back(tmp);
    std::cout<<__LINE__<<"\n";
    v.push_back(std::move(tmp));

    return 0;
}

vs2013编译器输出:

6 // 复制

18

9 // 移动

9 // 移动

g++和clang++输出:

6 // 复制

18

9 // 移动

6 // 复制

我的问题是:

  1. tmp的类型是test&&吗?tmp是右值吗?

  2. 如果tmp的类型是test&&,为什么第一个push_back没有使用移动构造函数?

  3. 最后一个输出来自哪里?为什么vs2013和g++输出结果不同?

谢谢。

第三个问题的答案:

正如andrew.punnett所评论的那样,它来自重新分配。


我已经修改了我的答案,以扩展安德鲁的评论。 - Potatoswatter
最有效的解决方案是 v.emplace_back();,它既不需要复制也不需要移动。 - fredoverflow
1
注意:如果在调用v.emplace_back()之前已经构造了对象,则该操作会移动该对象。 - DarkWanderer
@DarkWanderer v.emplace_back(); 在向量的内存中直接构造对象,调用之前没有创建任何对象。 - fredoverflow
1
@FredOverflow:不对。MyClass instance; v.emplace_back(instance);。虽然我关于移动的想法是错误的,但这将通过复制构造函数进行复制。 - DarkWanderer
@DarkWanderer,你不应该传递一个实例给emplace_back。只需直接写v.emplace_back();,括号内不需要任何内容。 - fredoverflow
2个回答

4

tmp 的类型是 test&& 吗?

是和不是。 tmp 是一个类型为 test&& 的右值引用变量,但作为表达式的标识符 tmp 具有类型 test 和值类别 左值。& 永远不是表达式类型的一部分。

tmp 是右值吗?

不是。标识符的任何使用都是左值表达式,即使是右值引用的名称。访问右值引用变量与访问左值引用变量基本相同;只有 decltype(tmp) 可以区分它们。通常会使用 decltype((tmp)) 来避免区分它们。

如果 tmp 的类型是 test&&,为什么第一个 push_back 没有使用移动构造函数?

因为右值引用的名称仍然是左值。要获取右值表达式,请使用 move(tmp)

最后一条输出语句来自哪里?为什么 vs2013 和 g++ 输出了不同的结果?

Clang 和 GCC 默认只为 vector 分配了一个对象的空间。当您添加第二个对象时,向量存储被重新分配,从而导致对象被复制。为什么没有移动它们?因为移动构造函数不是 noexcept 的,所以如果它引发异常,则无法撤消重新分配。

至于 MSVC 的两次移动,有两种可能性,可以通过实验来区分——我手头没有副本。

  1. MSVC 默认保留了足够的空间来存储两个对象,第二次移动是来自内部局部变量。
  2. MSVC 忽略了移动构造函数必须是 noexcept 的要求,并调用它执行重定位。这将是一个错误,但在这种情况下,它掩盖了一个常见的错误。

如果您将 vector 替换为 deque,则不会再看到任何副本,因为 deque 不允许假定可复制性。


3
大部分正确,但额外调用构造函数是因为向量正在被重新定位而不是使用“临时内部变量”。例如,在创建向量后尝试使用v.reserve(2)。 - andypea
这里难道不是使用了一些引用折叠的巧妙技巧吗,即 tmp 是 RHS 返回的内容吗?这与整个“通用引用”的概念是相同的。 - juanchopanza
那么 test&& 对 test& 没有优势吗? - cqdjyy01234
@andrew.punnett 谢谢,我只是假设GCC和Clang已经保留了足够的空间。(test应该只有一个字节,动态分配少于几个字的空间很少有优势。)嗯,看起来像是MSVC的一个bug。典型。 - Potatoswatter
@user1535111 取决于你对“优越性”的理解。rvalue引用和lvalue引用之间的主要区别在于它们的初始化方式。尝试使用auto &进行替换,看看会发生什么。 - Potatoswatter
显示剩余3条评论

2

Visual Studio目前还不支持noexcept关键字,因此可能不符合push_back的异常安全性要求。另外,额外的输出是grow函数计算容量差异的结果。

#include <iostream>
#include <vector>
#include <utility> // std::move

struct Except{
    Except(){}
    Except(Except const &) {
        std::cout<< "COPY\n";
    }
    Except(Except&&)  {
        std::cout<< "MOVE\n";
    }
};

struct NoExcept{
    NoExcept(){}
    NoExcept(NoExcept const &) noexcept {
        std::cout<< "COPY\n";
    }
    NoExcept(NoExcept&&) noexcept {
        std::cout<< "MOVE\n";
    }
};

template <typename T> void Test( char const *title,int reserve = 0) {
    auto&& tmp = T();
    std::cout<< title <<"\n";
    std::vector<T> v;
    v.reserve(reserve);

    std::cout<< "LVALUE REF ";
    v.push_back(tmp);
    std::cout<< "RVALUE REF ";
    v.push_back(std::move(tmp));
    std::cout<< "---\n\n";
}
int main(){
    Test<Except>( "Except class without reserve" );
    Test<Except>( "Except class with reserve", 10 );
    Test<NoExcept>( "NoExcept class without reserve" );
    Test<NoExcept>( "NoExcept class with reserve", 10 );
}

在clang中的结果如下:

Except class without reserve
LVALUE REF COPY
RVALUE REF MOVE
COPY
---

Except class with reserve
LVALUE REF COPY
RVALUE REF MOVE
---

NoExcept class without reserve
LVALUE REF COPY
RVALUE REF MOVE
MOVE
---

NoExcept class with reserve
LVALUE REF COPY
RVALUE REF MOVE
---

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接