【问题标题】:How to allocate memory for std::vector and then call constructor for some elements later?如何为 std::vector 分配内存,然后稍后为某些元素调用构造函数?
【发布时间】:2020-10-07 03:10:16
【问题描述】:

我正在“现代化”一个(相当老的)C++ 项目,但在这部分遇到了挫折:

旧代码为动态数组分配内存,然后在需要时为元素调用构造函数。我猜想为所有元素调用构造函数代价高昂,所以作者选择了这种方式(性能对这个项目至关重要)。旧代码如下所示(简化):

struct my_struct {
    my_struct(int x, int y, int z) { /* expensive ctor */ }
};

struct other_class {
    my_struct* arr;
    other_class(int n) {        
        arr = (my_struct*) malloc(n * sizeof(arr[0]);
    }

    void foo(int idx, int a, int b, int c) {
        new (&arr[idx]) my_struct(a, b, c);
    }
};

我将arr 更改为std::vector<my_struct>,并使用std::reserve 来“保留”内存。代码工作正常,通过了所有当前测试,但我知道这不行,因为 std::reserve 不会增加该向量的大小,所以调用 arr.size() 仍将返回 0。这是我的代码:

struct other_class {
    std::vector<my_struct> arr;
    other_class(int n) {        
        arr.reserve(n);
    }

    void foo(int idx, int a, int b, int c) {
        new (&arr[idx]) my_struct(a, b, c);
    }
};

如何让这段代码既快速又安全(假设我不能给my_struct添加一个默认的ctor)?谢谢。


编辑:这是示例代码。它按预期编译和运行,没有任何警告:http://cpp.sh/8ytwf

【问题讨论】:

  • 为向量分配多少内存(其容量)无关紧要,只要大小为零,所有索引都将超出范围。
  • std::vector 不支持将向量中的构造对象放在向量中的任意位置;第一个 size() 元素必须全部处于活动状态(即必须已构造),其余元素(直到 capacity())不得处于活动状态(即必须未构造/必须已销毁)。原始代码如何跟踪活着的和不活着的,以便仅在非活着的对象上适当地调用析构函数?
  • 您有一些选择:使用std::option。将默认 ctor 添加到 my_struct。添加一个新的自定义数据结构(在原始内存上操作),其中包含“活动”元素列表(可能作为位数组)并正确管理生命周期。
  • 或者代码的结构可能使得在 other_class 被销毁时,对于 [0, n) 中的每个 idxfoo 已经被调用一次。如果是这样,则无需显式跟踪数组中的活动和不活动。这种不安全的代码可能应该严格限制在一小部分用途中。

标签: c++ vector memory-management constructor stl


【解决方案1】:

但我知道这不好,因为 std::reserve 不会增加该向量的大小

std::reserve 确实增加了向量的容量,当您只想分配内存并稍后推送元素时,这正是您想要的。

向量管理的底层数组的大小不等于向量size()size() 返回容器中元素的数量,只要没有 size() == 0

当你稍后推送元素时,你不需要使用placement new,但你应该使用push_backemplace_back。更具体地说,这是错误的:

 new (&arr[idx]) my_struct(a, b, c);

因为您正在访问超出范围的向量(请记住:大小是元素的数量,容量可以更大,但您不能访问大于向量大小的索引)。而是:

 arr.emplace_back(a,b,c);

【讨论】:

  • 请注意,arr.emplace_back(a,b,c); 不考虑 idx,而原始代码考虑,因此它不是直接替换。您需要进行额外的调整以使代码与std::vector 一起使用。
  • @Mankarse 没错,但向量没有空槽,它们不是稀疏的。当索引到41 处没有元素时,您不能在索引42 处添加元素。如果真的需要,有解决方案,但我不想在答案中投入太多
  • 我相信这个new (&amp;arr[idx]) my_struct(a, b, c);没有错。我的示例代码已编译并运行良好:cpp.sh/8ytwfAFAIK、vector::[] 运算符不进行范围检查。只有vector::at() 可以
  • @Scorta 这是未定义的行为。毫无疑问。大小仍然是0,你可以做出各种奇怪的效果。例如尝试复制向量
  • @idclev 463035818,啊,是的,没错。我的意思是它可以按我的需要工作,但它并不安全。
【解决方案2】:

虽然我更喜欢emplace_back 解决方案,但在这种情况下您可以使用std::optional。这将允许您使用idx 来确定哪个元素填充了有效对象。

struct other_class 
{
    std::vector<std::optional<my_struct>> arr;
    other_class( int n ) : arr( n, std::nullopt )
    { }

    void foo( int idx, int a, int b, int c ) 
    {
        arr.at( idx ).emplace( a, b, c );
    }
};

【讨论】:

  • 这个解决方案最接近我的需要,但是我的公司没有采用 C++17,所以我现在不能使用它:( -- 我知道你为什么更喜欢emplace_back吗?它无法在随机索引处构造元素。
  • 我更喜欢emplace_back,因为它很简单。话虽这么说,如果您不使用C++17,我只能想到几个选项。首先,使用boost::optional。如果您已经在使用 boost 库,这是您的最佳选择。其次,您可以编写自己的optional 类型。
猜你喜欢
  • 2020-04-10
  • 2016-09-22
  • 2023-02-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-18
  • 2012-04-20
  • 1970-01-01
相关资源
最近更新 更多