【问题标题】:C++11 rvalue reference calling copy constructor tooC++11 右值引用也调用复制构造函数
【发布时间】:2013-08-07 18:24:21
【问题描述】:

我一直在测试一些 C++11 的一些特性。 我遇到了右值引用和移动构造函数。

我实现了我的第一个移动构造函数,这里是:

#include <iostream>
#include <vector>
using namespace std;

class TestClass{

public:
    TestClass(int s):
        size(s), arr(new int[s]){
    }
    ~TestClass(){
        if (arr)
            delete arr;
    }
    // copy constructor
    TestClass(const TestClass& other):
            size(other.size), arr(new int[other.size]){
        std::copy(other.arr, other.arr + other.size, arr);
    }

    // move constructor
    TestClass(TestClass&& other){
        arr=other.arr;
        size=other.size;

        other.arr=nullptr;
        other.size=0;
    }

private:
    int size;
    int * arr;
};

int main(){
    vector<TestClass> vec;

    clock_t start=clock();
    for(int i=0;i<500000;i++){
        vec.push_back(TestClass(1000));
    }
    clock_t stop=clock();
    cout<<stop-start<<endl;

    return 0;
}

代码运行良好。无论如何在复制构造函数中放置一个 std::cout 我注意到它被调用了!而且很多次..(移动构造函数 500000 次,复制构造函数 524287 次)。

更让我吃惊的是,如果我将代码中的复制构造函数注释掉,整个程序会快很多,而这一次移动构造函数被调用了 1024287 次。

有什么线索吗?

【问题讨论】:

  • 你使用的是哪个编译器?
  • @pinoscotto 附带说明:你有未定义的行为,你应该使用delete[],并且你不需要在析构函数中使用if (arr) ...。所以析构函数应该是简单的~TestClass(){ delete[] arr; }
  • @pinoscotto 在没有 if 的情况下调用 delete[] 总是安全的:(1)如果 new 在初始化中抛出,析构函数将不会被调用(2)否则你要么点到有效地址 (3) 或拥有nullptr,删除nullptr 始终是安全的(由标准保证)。

标签: c++ c++11 rvalue-reference move-constructor


【解决方案1】:

noexcept 放在移动构造函数中:

TestClass(TestClass&& other) noexcept {

详细说明:我打算给这个 Pierre,但不幸的是 cppreference 来源只是大致正确。

在 C++03 中

vector<T>::push_back(T)

具有“强异常保证”。这意味着如果 push_back 抛出异常,向量将保持在调用 push_back 之前的相同状态。

如果移动构造函数抛出异常,这个保证是有问题的。

vector 重新分配时,它会喜欢 将元素从旧缓冲区移动 到新缓冲区。但是,如果这些移动中的任何一个引发异常(除了第一个),那么它就会处于旧缓冲区已被修改的状态,并且新缓冲区还没有包含它应该包含的所有内容。 vector 无法将旧缓冲区恢复到其原始状态,因为它必须将元素移回原处,那些 移动也可能会失败。

因此为 C++11 制定了一条规则:

  1. 如果T 有一个noexcept 移动构造函数,则可用于将元素从旧缓冲区移动到新缓冲区。

  2. 否则,如果T 有一个复制构造函数,则会使用它来代替。

  3. 否则(如果没有可访问的复制构造函数),那么最终将使用移动构造函数,但是在这种情况下,不再给出强大的异常安全保证。

澄清:规则 2 中的“复制构造函数”是指采用 const T&amp; 的构造函数,而不是那些所谓的 T&amp; 复制构造函数。 :-)

【讨论】:

  • 您能否详细说明您的答案,以及 noexcept 的作用是什么?
  • Sod all on Visual Studio :-( 除了说“错误 C3646: 'noexcept' : unknown override specifier”
  • @Alon 有一个相关的,尽管是 C++11 之前的批准,讨论 here。基本上,它是关于在标准库容器中维护某些异常保证。
  • @doctorlove:在 Visual Studio 中,throws()可能可以工作,尽管这意味着一些稍微不同的东西,可能会导致它自己的问题。
【解决方案2】:

在你的移动构造函数中使用noexcept

TestClass(TestClass&& other) noexcept { ... }

noexcept 没有像这样的常量表达式等价于noexcept(true)

编译器可以使用此信息来启用对非抛出函数的某些优化以及启用 noexcept 运算符,该运算符可以在编译时检查特定表达式是否被声明为抛出任何异常。

例如,std::vector 等容器如果元素的移动构造函数为 noexcept,则将移动其元素,否则复制。

来源:http://en.cppreference.com/w/cpp/language/noexcept_spec

注意:这是一个 C++11 功能。某些编译器可能还没有实现它......(例如:Visual Studio 2012

【讨论】:

    【解决方案3】:

    std::vector 中的所有保留内存都被使用时,将调用复制构造函数。添加元素前需要调用std::vector::reserve()方法。

    vector<TestClass> vec;
    vec.reserve(500000);
    

    【讨论】:

      【解决方案4】:

      另一个问题。在移动构造函数中,

      // move constructor
      TestClass(TestClass&& other){
          arr=other.arr;
          size=other.size;
      
          other.arr=nullptr;
          other.size=0;
      }
      

      不应该

      arr=std:move(other.arr);

      size=std:move(other.size);

      因为

      所有命名值(例如函数参数)总是评估为左值(即使是那些声明为右值引用的值)

      ?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-08
        • 1970-01-01
        相关资源
        最近更新 更多