STL算法和back_inserter可以预分配空间吗?

时间:2018-09-25 14:15:52

标签: c++ memory stl stdvector stl-algorithm

如果我有类似的东西:

vector<int> longVector = { ... };
vector<int> newVector;
transform(longVector.begin(), longVector.end(), back_inserter(newVector),
          [] (int i) { return i * i; });

在处理和添加新元素之前,STL是否能够在newVector中预分配空间?我知道这不是算法的要求,但是“好的”实现可以优化它吗?或者,对于这种情况,我是否应该在之前添加newVector.reserve(longVector.size());?我并不一定要问那里的每个stdlib实现是否都可以(尽管有人知道特定的示例会很棒),但要问的是,在给定算法的接口和要求的情况下,它是否有可能(并且是预期的)。 / p>

这个问题适用于多种STL算法,transformcopymovefill_n,...不仅适用于back_inserter,而且适用于我猜是front_inserterinserter

编辑:为清楚起见,对于输出迭代器是{{1}的transform的情况,stdlib是否可以提供例如back_inserter的特定实现。 },在这种情况下,它将在实际运行转换之前访问向量对象并保留足够的空间以在给定的一对迭代器之间存储vector

4 个答案:

答案 0 :(得分:4)

它会在库中需要很多特殊包装,而带来的好处很小。

从集合中分离算法的全部要点是,它们既不需要了解彼此,也可以添加自己的与标准集合一起使用的算法,或者添加与现有算法一起使用的新集合。

由于唯一的好处就是奖励那些懒得叫reserve()的程序员,所以我觉得任何实现者都不大可能实现这种东西。尤其是因为它可能需要输入迭代器上的std::​distance()才能正常工作,从而进一步限制了它的使用。

还要注意,这种实现将需要在其迭代器中保留对拥有向量的引用,并且将无法使用std::​vector<T>::​iterator的最常见表示形式,即T*。无论是否使用此新功能,这都是所有用户必须承担的费用。

技术上可行吗?也许在某些情况下。可以吗我认同。良好的努力价值?不。

答案 1 :(得分:3)

使用boost::transform_iterator而不是std::transformstd::back_inserter几乎可以达到1内存分配的预期效果。

问题是,由于boost::transform_iterator无法返回对元素的引用,因此将其标记为std::input_iterator_tag。输入迭代器是单遍迭代器,与其他迭代器类别不同,当传递给std::vector范围构造函数时,它使用push_back来填充向量。

您可以强制恢复原始的迭代器类别,并需要注意的是,这样的迭代器违反了双向或随机访问迭代器必须返回对元素的引用的标准要求:

#include <boost/iterator/transform_iterator.hpp>
#include <algorithm>
#include <vector>

template<class I>
struct original_category_iterator : I {
    using iterator_category = typename std::iterator_traits<typename I::base_type>::iterator_category;
    using I::I;
};

template<class I>
inline original_category_iterator<I> original_category(I i) {
    return {i};
}

int main() {
    std::vector<int> longVector = {1,2,3};
    auto f = [](auto i) { return i * i; };
    std::vector<int> newVector(original_category(boost::make_transform_iterator(longVector.begin(), f)),
                               original_category(boost::make_transform_iterator(longVector.end(), f)));
}

答案 2 :(得分:1)

好消息是范围库does为随机访问迭代器容器保留了空间,因此您可以使用它。

现在回到问题所在:

循环保存是有问题的

不读code很难解释,但是如果STL算法在循环中被调用并且正在做保留,则可能触发二次复杂度。问题是某些STL容器会保留所请求的确切内存量(对于小尺寸内存是可以理解的,但是对于较大的IMAO,这是错误的行为),因此,例如,如果当前容量为1000,并且您调用reserve(1005),reserve(1010) ),reserve(1010)会导致3个重新分配(这意味着您每次复制〜1000个元素才能获得5个额外的空间)。 这是代码,它有点长,但是我希望你能明白:

#include<vector>
    #include<iostream>
    #include<chrono>
    int main(){
         std::vector<float> vec(10000,1.0f);
         std::vector<std::vector<float>> small_vecs(5000, std::vector<float>(50,2.0f));
         const auto t_start = std::chrono::high_resolution_clock::now();
         for(size_t i = 0; i < small_vecs.size(); i++) {
             // uncomment next line for quadratic complexity
             //vec.reserve(vec.size()+small_vecs[i].size());
             for (size_t j=0; j< small_vecs[i].size(); ++j){
                 vec.push_back(small_vecs[i][j]);
             }
         }
         const auto t_end = std::chrono::high_resolution_clock::now();
         std::cout << "runtime:" <<
             std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count()
             << "ms\n";
    }

奖金:

我上次对基准back_iterator进行基准测试,即使保留时间可悲地慢(以x衡量的减速,而不是%),因此,如果您关心性能,请确保在使用back_inserter代码时,将其针对手动循环进行基准测试。 >

答案 3 :(得分:0)

我认为不可能。与用于迭代器类别的容器和算法之间存在着明显的区别。

像clear()和擦除()一样,reserve()修改容器。引入reserve()可使算法知道容器,这与清晰分离的清晰设计背道而驰。

还可以拥有

deque<int> longDeque = { ... };
deque<int> newDeque;
transform(longDeque.begin(), longDeque.end(), back_inserter(newDeque),
          [] (int i) { return i * i; });

list<int> longList = { ... };
list<int> newList;
transform(longList.begin(), longList.end(), back_inserter(newList),
          [] (int i) { return i * i; });

和std :: deque和std :: list不支持reserve(),但是代码相同。

最后一点: 向量没有push_front(),因此不必支持front_inserter()。