为什么不调用移动构造函数和赋值来初始化带有 initialize_list 的向量

Why move constructor and assignement not being called for initializing a vector with initialize_list

我有下面的 class 并尝试添加复制和移动构造函数和赋值运算符。我的目标是尽可能少的复制和优化。

我希望向量被填充到位,即在创建向量时不会调用复制构造函数。我做错了什么以及如何强制它使用移动构造函数或赋值?

#include <iostream>
#include <concepts>
#include <vector>

template<typename T>
requires std::is_arithmetic_v<T>
class Data {
    private :
    T mA = 0;
    T mB = 0;
    
    public: 
    Data(const T& data) : mA{data}{ // from single T
        std::cout << "Constructed Data with: " << mA << ", " << mB << std::endl;
    }

    Data(const Data<T>& other)  : mA{other.mA}, mB{other.mB} {
        std::cout << "COPY Constructed Data with: " << mA << ", " << mB << std::endl;
    }
     
    Data(Data<T>&& other)  : mA{other.mA}, mB{other.mB} {
        std::cout << "MOVE Constructed Data with: " << mA << ", " << mB << std::endl;
    }    

    Data(const std::initializer_list<T>& list) {
        std::cout << "Constructed Data with list: "; 
        if(list.size() >= 2) {
            mA = *list.begin();
            mB = *(list.begin() + 1);
            std:: cout << mA << ", " << mB << std::endl;
        }
    }

    ~Data() {
        std::cout << "Destructed: " << mA << ", " << mB << std::endl;
    }

    const Data operator=(const Data& other) {
        mA = other.mA;
        mB = other.mB;
        return *this;
    }

    Data operator=(Data&& other) {
        mA = other.mA;
        mB = other.mB;
        return *this;
    }
};

int main() {

    std::cout << "** With initilizer_list **" << std::endl;
    {
        auto vec = std::vector<Data<int>>{{1,1}, {2,2}};
    }


    std::cout << "\n**With element**" << std::endl;
    {
        auto vec = std::vector<Data<int>>{1,2};
    }

   std::cout << "\n**With push**" << std::endl;
   {
       auto vec = std::vector<Data<int>>();
       vec.push_back(1);
       vec.push_back(2);
   }

}

输出:

** With initilizer_list **
Constructed Data with list: 1, 1
Constructed Data with list: 2, 2
COPY Constructed Data with: 1, 1
COPY Constructed Data with: 2, 2
Destructed: 2, 2
Destructed: 1, 1
Destructed: 1, 1
Destructed: 2, 2

**With element**
Constructed Data with: 1, 0
Constructed Data with: 2, 0
COPY Constructed Data with: 1, 0
COPY Constructed Data with: 2, 0
Destructed: 2, 0
Destructed: 1, 0
Destructed: 1, 0
Destructed: 2, 0

**With push**
Constructed Data with: 1, 0
MOVE Constructed Data with: 1, 0
Destructed: 1, 0
Constructed Data with: 2, 0
MOVE Constructed Data with: 2, 0
COPY Constructed Data with: 1, 0
Destructed: 1, 0
Destructed: 2, 0
Destructed: 1, 0
Destructed: 2, 0

CompilerExplorer Link

I expect the vectors to be filled in place

push_back 和初始化列表构造函数都期望元素已经构造并通过参数传递。因此,元素必须首先通过转换构建,然后 moved/copied 进入向量的存储。如果您不希望这样,请改用 emplace_back,它采用元素构造函数的构造函数参数并就地创建元素。

这并不能解决所有复制和移动的情况,因为如果旧存储变得太小而无法容纳更多元素,向量必须将对象移动到新存储。为了完全避免这种情况,首先调用 vec.reserve(...),其中 ... 至少与向量的最大大小一样大。

在重新分配的情况下使用复制而不是移动的原因是因为您没有标记移动构造函数noexcept。如果移动构造函数不是 noexceptstd::vector 更喜欢复制构造函数,因为如果移动构造函数在将对象移动到新存储时抛出异常,那么 std::vector 不能保证它能够滚动-回到以前的状态,就像通常那样。


在 class 中声明 copy/move constructor/assignment 或析构函数总是有点冒险。首先,因为很容易导致未定义的行为,请参阅 rule of three/five,其次,如果不仔细编写,它通常会导致比隐式生成的版本更糟糕的结果。

如果您没有具体原因,例如因为您在 class 中管理原始资源,所以不要声明任何这些特殊成员。 (仅)然后隐含的将自动执行正确且最有可能最好的事情(“零规则”)。