【问题标题】:How to avoid object copy at vector list initialization and how to extend the lifetime of a temporary如何在向量列表初始化时避免对象复制以及如何延长临时对象的生命周期
【发布时间】:2020-09-05 00:20:39
【问题描述】:

考虑下面的代码,其中一些“MyType”类的对象存储在一个向量中,然后这个向量被传递给“VectorProcessor”类的一个对象:

// to compile, run: g++ -Wall --std=c++17 test.cpp -o test
#include <vector>
#include <iostream>

using namespace std;

class MyType {
private:
    int x;
public:
    MyType(int a_x) {
        this->x = a_x;
        cout << "MyType constructor; x=" << this->x << endl;
    }
    ~MyType() {
        cout << "MyType destructor; x=" << this->x << endl;
        this->x = 0;
    }
    MyType(const MyType &o)  {
        this->x = o.x +10;
        cout << "MyType copy constructor; x=x'+10=" << this->x << endl;
    }
    MyType& operator=(const MyType& o)  {
        this->x = o.x +20;
        cout << "MyType =copy constructor; x=x'+20=" << this->x << endl;
        return *this;
    }
    MyType(MyType &&o) {
        this->x = o.x + 30;
        cout << "MyType move constructor; x=x'+30=" << this->x << endl;
    }
    MyType & operator=(MyType &&o) {
        this->x = o.x + 40;
        cout << "MyType =move constructor; x=x'+40=" << this->x << endl;
        return *this;
    }
    friend ostream& operator<<(ostream& os, const MyType& s) {
        return os << s.x;
    }
};

template <typename T>
class VectorProcessor {
    private:
        const vector<T> &v;
    public:
        VectorProcessor(const vector<T> &a_lista): v{a_lista} {
            cout << "VectorProcessor constructor" << endl;
        }
        VectorProcessor(const vector<T> &&a_lista): v(a_lista) {
            cout << "VectorProcessor rvalue constructor" << endl;
        }
        const T& getLast() {
            return this->v[this->v.size()-1];
        }
};

int main() {
#if 1
    // test (A)
    vector<MyType> v{1,2,3,4,5};
    VectorProcessor<MyType> r(v);
#else
    // test (B)
    VectorProcessor<MyType> r(vector<MyType>{1,2,3,4,5});
#endif
    cout << "r.getLast(): " << r.getLast() << endl;

    return 0;
}

如果我启用“测试A”,程序输出是:

MyType constructor; x=1
MyType constructor; x=2
MyType constructor; x=3
MyType constructor; x=4
MyType constructor; x=5
MyType copy constructor; x=x'+10=11
MyType copy constructor; x=x'+10=12
MyType copy constructor; x=x'+10=13
MyType copy constructor; x=x'+10=14
MyType copy constructor; x=x'+10=15
MyType destructor; x=5
MyType destructor; x=4
MyType destructor; x=3
MyType destructor; x=2
MyType destructor; x=1
VectorProcessor constructor
r.getLast(): 15
MyType destructor; x=11
MyType destructor; x=12
MyType destructor; x=13
MyType destructor; x=14
MyType destructor; x=15

如果我启用“测试 B”,程序输出是:

MyType constructor; x=1
MyType constructor; x=2
MyType constructor; x=3
MyType constructor; x=4
MyType constructor; x=5
MyType copy constructor; x=x'+10=11
MyType copy constructor; x=x'+10=12
MyType copy constructor; x=x'+10=13
MyType copy constructor; x=x'+10=14
MyType copy constructor; x=x'+10=15
VectorProcessor rvalue constructor
MyType destructor; x=11
MyType destructor; x=12
MyType destructor; x=13
MyType destructor; x=14
MyType destructor; x=15
MyType destructor; x=5
MyType destructor; x=4
MyType destructor; x=3
MyType destructor; x=2
MyType destructor; x=1
r.getLast(): 0

然后我问:不使用指针,或者智能指针,

1) 如何避免在 MyType 类的对象中发生额外的复制(对于测试 A 和 B)? 2)在“测试B”中,为什么在r.getLast()之前调用了MyType析构函数?也就是说,如何将临时vector&lt;MyType&gt;{1,2,3,4,5}的生命周期延长到块的末尾?

【问题讨论】:

  • 你写=copy constructor的地方,实际上是复制赋值运算符(它不是构造函数)。与移动赋值运算符类似
  • 来自更自以为是的 POV:如果不是您故意在各种构造函数中注入副作用,编译器很可能能够优化您调用移动构造函数所产生的任何成本额外的时间,所以区别可能并不像您期望的那么重要。
  • 这个问题有点落后——一个更相关的问题通常是如何避免创建临时文件(或允许/强制编译器忽略它们)。一般来说,在语句中创建的未命名临时(例如,在您的情况 B 中,用于构造 r 的对象)仅在语句结束之前存在。根据上下文,延长临时对象寿命的一种方法是给它一个名称(例如,初始化一个 const 引用以引用它)以便对象在引用在范围内时持续存在。但是一个未命名的临时对象的生命不能延长到封闭范围的末尾

标签: c++ c++11 constructor


【解决方案1】:

std::initializer_list的设计方式,初始化器相当于:

vector<MyType> v{ MyType(1), MyType(2), MyType(3), MyType(4), MyType(5) };

即它创建临时的MyTypes 来构造初始化列表,然后向量构造函数从初始化列表中复制其元素。有些人认为这是一个错误,但我们现在坚持下去。

为了避免多余的对象,您可以就地构造元素,例如:

vector<MyType> v;
v.reserve(5);
for (int i = 1; i <= 5; ++i)
    v.emplace_back(i);

这段代码有多种包装方式,但这是基本思想。


对于您的第二个问题,除了少数例外情况外,临时对象仅存在于创建它们的完整表达式的末尾。绑定到构造函数引用参数不是例外之一。您在test (A) 中的代码是执行此操作的正确方法。

【讨论】:

  • “绑定到构造函数引用参数不是例外之一”:您能再解释一下吗?有哪些例外(如何延长临时的生命周期?
猜你喜欢
  • 2013-11-20
  • 2016-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-15
  • 1970-01-01
相关资源
最近更新 更多