选择什么标准容器(如果有的话)?

时间:2014-04-12 11:58:59

标签: c++ memory-management containers

我需要一些内存管理,并希望我可以将它建立在某个std容器上。我的要求是:

  1. 元素只有一个默认构造函数(没有副本,没有移动,没有别的)
  2. 容器可以通过一小块连续的元素块扩展(在后面)
  3. 我甚至大致知道我总共需要多少元素,甚至更好,在任何时候我最终需要多少元素。不过这些都是估算值。
  4. 我真的不需要迭代器,但是获取元素运行数的方法会很方便。
  5. 所以,我需要通过添加块来扩展,例如std::deque。但是对于std::deque,我无法保证通过8个元素进行扩展会给我一个连续的块。并且std::deque没有capacity所以我无法“适应”std::deque

    这意味着我必须自己写,对吗? (注意:我不想知道如何自己编写,但只有当我必须时)。

    编辑以澄清:只有在每次扩展时获得的元素块必须是连续的,而不是整个容器 - 这显然与其他要求相矛盾。

    编辑jalf 那么这是什么:用于“排序”3D点的空间八度树。树节点指的是立方体单元,并形成与使用指针链接的父母和女儿的链接结构。同级节点未链接,但在内存中相邻。预先不知道节点的总数(因为每个最终节点的点数> 1),但是可以获得估计。在树构建期间,当划分非最终节点时,必须获得最多8个新节点的连续块,然后将其链接到树中。移动或复制这些节点会使任何现有链接(指针)失效。

    另一个编辑只是为了澄清一些讨论。任何基于std::vector<T>的设计都不得使用resize()和/或reserve()。在某些条件下,两者都需要复制或移动T的构造函数。即使从未在这些条件下调用过,代码也无法编译。

5 个答案:

答案 0 :(得分:5)

只有一个问题,std::vector适合您。

它完全是连续的,不仅仅是块状的,可以扩展,保持连续(第2点)。扩展可能意味着重新分配(因此先前获得的指针/迭代器失效,移动),但如果您事先知道总大小(第3点),则可以reserve()以便不重新分配发生了。

给定向量i的迭代器v,您可以通过i - v.begin()获得正在运行的数字(位置);类似地,给定一个元素的指针pp - &v[0](第4点)。

捕获是你的观点1.有emplace_back(),但出于与异常安全相关的原因,std::vector仍尝试在某处暂时构建元素,然后移动到他们的永久职位。

假设你有这门课程

struct A
{
    A() { }
    A(A&&) = delete;
    A(const A&) = delete;
};

我可以看到两个解决方法:

  1. 派生另一个类B,默认构造而不是复制/移动构造:

    struct B : A
    {
        B() : A() { }
        B(B&&) : A() { }
        B(const B&) : A() { }
    };
    
  2. 如果你不能这样做,那就制作一个为你做这个的分配器对象:

    template<typename T>
    struct allocator : std::allocator<T>
    {
        using std::allocator<T>::allocator;
        using std::allocator<T>::construct;
    
        template<typename U>
        void construct(U* p, U&&) { construct(p); }
    
        template<typename U>
        void construct(U* p, const U&) { construct(p); }
    
        template<typename U>
        struct rebind { using other = allocator<U>; };
    };
    
    template<>
    struct allocator<void> : std::allocator<void> { };
    
  3. 两种情况的使用如下所示(live example):

    template<typename C, size_t N = 100>
    void test()
    {
        C c;
        c.reserve(N);
        for (size_t i = 0; i < N; ++i)
            c.emplace_back();
    }
    
    int main ()
    {
        test<std::vector<B> >();
        test<std::vector<A, allocator <A> > >();
    }
    

    请记住,这样仍有A的实例被构造然后被丢弃。这是使用std::vector的一个不幸后果。如果A足够小并且其默认构造没有任何奇怪的副作用,那么这应该不是问题。

    如果你仍然需要扩展超过最初的reserve(),那么我建议使用这种向量的容器作为块。如果您仍然希望将此元容器视为具有自己的迭代器的单个容器,那么相关的是我自己的join view及其iterator仅仅是为了一个想法,但这仍然是非常实验性的。我打赌在Boost中还有为此目的的东西,但我并不那么熟悉。

答案 1 :(得分:1)

我建议只使用std::deque<std::vector<T>>作为自定义类的私有数据成员,以确保:

  • 新元素仅在后面添加
  • 元素以块的形式添加

答案 2 :(得分:1)

这是一个简单的模板化C ++ 11类,它使用指向矢量的指针向量来包含元素,它以块的形式在后面扩展。以前分配的元素的指针在扩展后仍然有效。它支持基于范围的循环和随机访问。我使用std :: vector有两个目的,一个顶层向量保持指向第二级向量,顶层向量用普通push_back扩展,所以指向第二级向量的指针每次到达时都会被复制(非常快)扩展数量为2的幂 - 指定为非常少且不常见。第二级向量分配有在构造Chunky实例时或在扩展期间给出的初始大小,并且大小永远不会改变,它们包含元素(模板的参数),因此指向元素的指针不会被无效。调用extend()。

我为每个100万个元素进行了10个扩展,并且每个元素都可以访问它们,它的运行时间为106毫秒(每个访问平均值为10.6纳秒)。

coliru link

然后,我将每个10,000个元素的1000个扩展和每个元素的访问权限定时,它运行115毫秒(每个访问平均值为11.5纳秒)。

coliru link

chunky.h

#ifndef CHUNKY_H
#define CHUNKY_H
#include <vector>
#include <cstddef>
#include <stdexcept>

template<class T> class Chunky {
    std::vector< std::vector<T> * > _chunk;
    size_t _size   = 0;
    T*     _active = nullptr;
    size_t _begin  = 0;
    size_t _end    = 0;
 public:
    Chunky() = default;
    Chunky( size_t guess ) { extend( guess ); }
    ~Chunky() { for ( auto & it : _chunk ) delete it; }
    void extend( size_t x ) {
        _chunk.push_back( new std::vector<T>( x ) );
        _size += x;
    }
    size_t size() const { return _size; }
    T & operator[]( size_t z ) { return at( z ); }
    T & at( size_t z ) {
        if ( z < _begin || z >= _end ) {
            size_t x = 0;
            for( _end = 0; z >= _end && x < _chunk.size(); ++x)
            {
                _begin = _end;
                _end += _chunk[x]->size();
                _active = _chunk[x]->data();
            }
            if (z >= _end) throw std::out_of_range("Chunky at "+std::to_string(z));
        }
        return _active[z - _begin];
    }
    class iterator
    : public std::iterator<std::forward_iterator_tag, int>
    {
        Chunky<T> * _tp = nullptr;
        size_t _x = 0;
    public:
        iterator() = default;
        iterator(Chunky<T> * tp) : _tp(tp) {}
        bool operator!= (const iterator& other) const {
            return _tp != other._tp || _x != other._x;
        }
        T & operator* () const { return _tp->at(_x); }
        T * operator->() const { return &_tp->at(_x); }
        iterator& operator++ () {
            if (++_x >= _tp->size()) {
                _x = 0;
                _tp = nullptr;
            }
            return *this;
        }
        iterator& operator+= ( size_t x ) { while(x--) *this++; return *this; }
        friend inline iterator& operator+ ( iterator & lhs, size_t x ) { while(x--) ++lhs; return lhs; }
        friend inline iterator operator+ ( iterator lhs, size_t x ) { while(x--) ++lhs; return lhs; }
    };
    inline iterator begin() { return iterator(this); }
    static inline const iterator end() { return iterator(); }

};

#endif

一个简单的测试

#include "chunky.h"
#include <iostream>
#include <chrono>

using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::chrono::steady_clock;

struct Element
{
    int _value = 0;
    Element() = default;
    Element(const Element&) = delete;
    Element(Element&&) = delete;
    Element& operator=(Element const&) = delete;
};

int main( int argc, char *argv[] )
{
    Chunky<Element> c( 5 ); // 5 default constructed Elements
    int i = 0;    

    // Chunky allows range based for loop
    for( auto & it : c ) it._value = 100 + i++;

    // Pointers to elements are valid for the lifetime of the container
    Element * element_ptr = & c[4]; // Take address of one of the elements
    std::cout << "The fifth element has a value of " << element_ptr->_value << std::endl;

    size_t previous_size = c.size();

    c.extend( 10 ); // 10 more default constructed Elements

    std::cout << "Dereferencing pointer to fifth element after extend, value is still " << element_ptr->_value << std::endl;

    for( size_t k=0; k < 10; ++k )
        c[previous_size + k]._value = 1100 + i++;

    // random access to initial elements and newly extended elements
    c[3]._value = -3;
    c[13]._value = -13;

    std::cout << "\nThe Elements contain values of: " << std::endl;

    // range based for loop
    for( auto & it : c ) std::cout << it._value << std::endl;

    steady_clock::time_point start = steady_clock::now();

    size_t extend_size = 1e6;

    for( size_t x = 0; x < 10; ++x ) {
        size_t previous_size = c.size();
        c.extend( extend_size ); // 1 million more default constructed Elements
        for( size_t k=0; k < extend_size; ++k )
            c[previous_size + k]._value = previous_size + k;
    }

    steady_clock::time_point end = steady_clock::now();

    std::cout << "\nExtending 10 times by " << extend_size << " and initializing the Elements took "
        << duration_cast<milliseconds>(end - start).count()
        << " msec.\n";

    return 0;
}

运行示例

g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
The fifth element has a value of 104
Dereferencing pointer to fifth element after extend, value is still 104

The Elements contain values of: 
100
101
102
-3
104
1105
1106
1107
1108
1109
1110
1111
1112
-13
1114

Extending 10 times by 1000000 and initializing the Elements took 106 msec.

答案 3 :(得分:1)

所有复杂的解决方案有什么用?不会std::vector<std::unique_ptr<T>>解决问题,或者我错过了什么?

答案 4 :(得分:1)

简短的回答似乎是没有单个标准容器完成这项工作,你必须自己编写

这被证明比我想象的更困难,因为生成对象的指针必须在内存管理器的生命周期内保持有效(在原始问题中通过请求无法移动或复制的请求实现)。这不包括使用std::vector::resize()std::vector::reserve()。仍然可以设计sequence_container<std::vector<T>>,但要么为每个新对象块创建另一个vector,要么预先构造一大块对象,然后将它们放出,直到块耗尽为止。 / p>

为避免这种情况,似乎必须编写一些chunk类(使用而不是vector)并处理allocator问题。这是一个实现:

#include <list>
#include <memory>

template<typename type, typename allocator=std::allocator<type>,
         template<typename,typename> class sequence_container = std::list>
class chunk_allocator
{
public:
  using object = type;
  using pointer = object*;
  using size_type = std::size_t;
  using alloc_traits = std::allocator_traits<allocator>;
private:
  struct chunk
  {
    allocator alloc;
    const pointer beg_data, end_capacity;
    pointer end_data;
    chunk(size_type cap, const allocator&all)
      : alloc(all)
      , beg_data(alloc_traits::allocate(alloc,cap))
      , end_capacity(beg_data+cap)
      , end_data(beg_data) {}
    ~chunk()
    {
      if(beg_data==nullptr) return;
      for(; --end_data>=beg_data; --end_data)
        alloc_traits::destroy(alloc,end_data);
      alloc_traits::deallocate(alloc,beg_data,capacity());
    }
    size_type size() const noexcept { return end_data - beg_data; }
    size_type capacity() const noexcept { return end_capacity - beg_data; }
    pointer make(size_type n)
    {
      if(end_data + n > end_capacity)
        return nullptr;
      auto ptr = end_data;
      for(; n; --n,++end_data)
        alloc_traits::construct(alloc,end_data);
      return ptr;
    }
  };
  using chunk_alloc = typename alloc_traits::template rebind_alloc<chunk>;
  using chunk_container = sequence_container<chunk,chunk_alloc>;
  using chunk_iterator = typename chunk_container::iterator;
  chunk_container chunks;
  chunk_iterator last_chunk;
  /// no default constructor
  chunk_allocator() = delete;
  /// no copy
  chunk_allocator(chunk_allocator const&) = delete;
  chunk_allocator&operator=(chunk_allocator const&) = delete;
public:
  /// allow move
  chunk_allocator(chunk_allocator&&) = default;
  chunk_allocator&operator=(chunk_allocator&&) = default;
  /// constructor
  explicit
  chunk_allocator(size_type initial_capacity, allocator const&alloc=allocator())
    : chunks(alloc)
    , last_chunk(chunks.emplace(chunks.end(),initial_capacity,alloc)) {}
  /// invalid index
  static constexpr size_type invalid = ~size_type(0);
  /// find index for element, return invalid if not ours
  size_type index(const object*ptr) const noexcept
  {
    size_type n=0;
    for(auto c=chunks.begin(); c!=chunks.end(); ++c)
      if(c->beg_data <= ptr && ptr < c->end_data)
        return n + size_type(ptr-c->beg_data);
      else
        n += c->size();
    return invalid;
  }
  /// obtain contiguous chunks of objects
  /// \param[in] n          \# objects in returned chunk
  /// \param[in] chunk_size \# objects to allocate should we not have enough
  /// \return pointer to first of n contiguous objects
  object*create(const size_type n, size_type chunk_size=0)
  {
    if(n==0)
      return nullptr;
    if(last_chunk->end_data + n > last_chunk->end_capacity) {
      if(chunk_size==0) chunk_size = last_chunk->capacity();
      if(chunk_size< n) chunk_size = n;
      last_chunk = chunks.emplace(chunks.end(),chunk_size,last_chunk->alloc);
    }
    return last_chunk->make(n);
  }
};

// test
#include <iostream>
struct foo
{
  int X;
  static int C;
  foo() : X(C++) { std::cout<<"foo::foo(): X="<<X<<std::endl; }
  foo(foo const&) = delete;
  foo&operator=(foo const&) = delete;
  foo(foo &&) = delete;
  foo&operator=(foo &&) = delete;
};

int foo::C=0;
int main()
{
  std::cout<<" chunk_allocator<foo> C(3);"<<std::endl;
  chunk_allocator<foo> C(3);
  auto a = C.create(1);
  std::cout<<" auto a=C.create(1)="<<a<<std::endl;
  auto b = C.create(4);
  std::cout<<" auto b=C.create(4)="<<b<<std::endl;
  auto c = C.create(3);
  std::cout<<" auto c=C.create(3)="<<c<<std::endl;
  std::cout<<" a="<<a<<" a->X="<<a->X<<" index(a)="<<C.index(a)<<'\n'
       <<" b="<<b<<" b->X="<<b->X<<" index(b)="<<C.index(b)<<'\n'
       <<" c="<<c<<" c->X="<<c->X<<" index(c)="<<C.index(c)<<'\n';
}