【问题标题】:push_back to std::vector creates many temp objects compared to std::list与 std::list 相比, push_back 到 std::vector 创建了许多临时对象
【发布时间】:2019-06-27 10:50:10
【问题描述】:

我创建了一个类来跟踪具有唯一 ID 的实例。 构造此类的新实例(或复制构造它)提供了一个唯一 ID,该 ID 取自 ID 池。销毁将 ID 放回池中。打印语句观察实例何时被创建和销毁。

#include <iostream>
#include <vector>
#include <list>


class IdPool {
public:
    IdPool()  {
        m_id = allocateID();
        std::cout << "c'tor id: " << m_id << std::endl;
    }

    ~IdPool() {
        freeID(m_id);
        std::cout << "d'tor free id: " << m_id << std::endl;
    }
    IdPool(const IdPool& obj) {
        m_id = allocateID();
        std::cout << "copy c'tor id: " << m_id << std::endl;
    }

    class Init {
    public:
        Init(const int maxIDs) {
            for (int i=maxIDs; i>=1; --i) {
                s_idArray.push_back(i);
            };
        }
    };

    int id() { return m_id; }

private:
    int allocateID() {
        if (s_idArray.empty()) 
            return 0;
        else {
            int id = s_idArray.back();
            s_idArray.pop_back();
            return id;
        }
    }

    bool freeID(int id) {
        if ( (id > 0 ) && (s_idArray.size() < s_maxIdCount) ) {
            s_idArray.push_back(id);
            return true;
        } else {
            return false;
        }
    }

    static std::vector<int> s_idArray;
    static const size_t     s_maxIdCount;
    static Init             s_setIdCount;
    int                     m_id;
};


const size_t IdPool::s_maxIdCount = 10;
std::vector<int> IdPool::s_idArray;
IdPool::Init IdPool::s_setIdCount(IdPool::s_maxIdCount);


int main(int argc, char* argv[]) {
    using namespace std;

    cout << endl << "-- push 2 IDs to list --" << endl;
    list<IdPool> listId;
    for (int i = 0; i < 2; ++i) {
        listId.push_back(IdPool());
        cout << "push_back to list id: " << listId.back().id() << endl << endl;
    }

    cout << endl << "-- push 2 IDs to vector --" << endl;
    vector<IdPool> vecId;
    for (int i = 0; i < 2; ++i) {
        vecId.push_back(IdPool());
        cout << "push_back to vector id: " << vecId.back().id() << endl << endl;
    }

    cout << endl << "-- push 2 IDs to preallocated vector --" << endl;
    vector<IdPool> vecIdReserved;
    vecIdReserved.reserve(5);
    for (int i = 0; i < 2; ++i) {
        vecIdReserved.push_back(IdPool());
        cout << "push_back to reserved vector id: " << vecIdReserved.back().id() << endl << endl;
    }

    return 0;
}

在测试 ID 生成器类时,我观察到以下行为:

列表:将新 ID 推送到列表中按预期工作(为临时对象提取新 ID,在临时对象被销毁后将其放回,参见输出。

向量:将新 ID 推送到向量会创建许多临时对象,对应于向量的大小。在每个 push_back 操作之后,向量在尾部包含相同的 ID,请参见下面的输出。如果我想使用 IdPool 类来识别派生类的实例,这不是所需的行为。对于列表,我希望每个 push_back 操作只有一个 temp。我在这里错过了什么?

编辑: 推送到预先分配的向量与列表一样。在这种情况下,这可能是要走的路。我只需要记住在使用矢量之前进行预订。

输出

-- push 2 IDs to list --
c'tor id: 1
copy c'tor id: 2
d'tor free id: 1
push_back to list id: 2

c'tor id: 1
copy c'tor id: 3
d'tor free id: 1
push_back to list id: 3


-- push 2 IDs to vector --
c'tor id: 1
copy c'tor id: 4
d'tor free id: 1
push_back to vector id: 4

c'tor id: 1
copy c'tor id: 5
d'tor free id: 4
copy c'tor id: 4
d'tor free id: 1
push_back to vector id: 4


-- push 2 IDs to preallocated vector --
c'tor id: 1
copy c'tor id: 6
d'tor free id: 1
push_back to reserved vector id: 6

c'tor id: 1
copy c'tor id: 7
d'tor free id: 1
push_back to reserved vector id: 7

【问题讨论】:

标签: c++ stl


【解决方案1】:

接受的答案是对您看到这么多副本的原因的正确解释,但有一种方法可以消除更多副本:emplace_back。

如你所见,当你写作时:

vecId.push_back(IdPool());

您调用 IdPool 的默认构造函数,然后调用复制构造函数将其复制到向量的存储中。 (而且,正如公认的答案所解释的,如果它必须增加可用的存储量,它可能还必须复制已经在向量存储中的元素。)

但如果你要写:

vecId.emplace_back();

新的IdPool会直接在vector的存储区构建,保存一份。

记住vecId的类型是std::vector&lt;IdPool&gt;,所以emplace_back知道它需要构造一个IdPool。由于我们没有向 emplace_back 传递任何参数,它知道调用 IdPool 的默认构造函数。

假设 IdPool 有一个额外的构造函数,其名称为 std::string

explicit IdPool(std::string name) { blah, blah, blah }

您也可以使用 emplace_back 调用此构造函数,如下所示:

vecId.emplace_back("pool party!");

对于元素类型的每个公共构造函数,emplace_back 都有一个有效的重载。

vector<IdPool> vecId;
for (int i = 0; i < 2; ++i) {
    vecId.emplace_back();
    cout << "push_back to vector id: " << vecId.back().id() << endl << endl;
}

【讨论】:

    【解决方案2】:

    因为push_back 无法按预期工作。 当vector的capacity等于vector的size时,会导致重新分配。但确切的行为是实现定义的

    出于效率的考虑,push_back 不仅开辟了一个小房间正好适合另一个元素(这违反了 C++ 标准的时间复杂度要求。),但它适用于大尺寸(通常是原始尺寸的两倍) ,这就是为什么你说尺寸是对应于原始尺寸的。)

    为了减少重新分配的时间并避免额外的重新分配(但实际上,使用保留并不总是可以避免),请使用reserve

    要缩小向量,请使用 shrink_to_fit(但也不能保证成功。)

    似乎需要example

    【讨论】:

    • 据我所知,线性增加容量会违反push_back 被 O(1) 摊销的标准要求。
    • @HolyBlackCat 否,因为它已摊销。以下 push_back 操作不需要分配。所以摊销时间是常数。
    • 线性容量增长是指维持size == capacity,因此每次插入都会发生重新分配。我不认为将大小增加一个常数因子会给你 O(1) 摊销插入。我的意思是没有编译器“打开一个适合另一个元素的小房间”,因为这将不再是 O(1) 摊销,因此违反了要求。
    • @HolyBlackCat 所以这是对我答案的补充,对吧?
    • 在我看来,这一点也不粗鲁。
    猜你喜欢
    • 2010-09-19
    • 1970-01-01
    • 2017-09-21
    • 1970-01-01
    • 2019-05-22
    • 2012-05-07
    • 1970-01-01
    • 2015-01-15
    • 1970-01-01
    相关资源
    最近更新 更多