Избегание стандартных конструкций элементов в стандартных контейнерах

Мне интересно создать контейнер uninitialized_vector, который будет семантически идентичен std::vector с оговоркой, что новые элементы, которые иначе были бы созданы с конструктором без аргументов, будут создаваться без инициализации. Я в первую очередь заинтересован в том, чтобы избежать инициализации POD до 0. Насколько я могу судить, не существует способа сделать это, объединив std::vector со специальным типом распределителя.

Я хотел бы создать свой контейнер в том же духе, что и std::stack, который адаптирует контейнер, предоставленный пользователем (в моем случае std::vector). Другими словами, я бы хотел избежать переопределения всего std::vector и вместо этого предоставить "фасад" вокруг него.

Есть ли простой способ управления конструкцией по умолчанию из "внешнего" std::vector?


Вот решение, к которому я пришел, который был вдохновлен Керреком:

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <cassert>

// uninitialized_allocator adapts a given base allocator
// uninitialized_allocator behavior is equivalent to the base
// except for its no-argument construct function, which is a no-op
template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_allocator
    : BaseAllocator::template rebind<T>::other
{
  typedef typename BaseAllocator::template rebind<T>::other super_t;

  template<typename U>
    struct rebind
  {
    typedef uninitialized_allocator<U, BaseAllocator> other;
  };

  // XXX for testing purposes
  typename super_t::pointer allocate(typename super_t::size_type n)
  {
    auto result = super_t::allocate(n);

    // fill result with 13 so we can check afterwards that
    // the result was not default-constructed
    std::fill(result, result + n, 13);
    return result;
  }

  // catch default-construction
  void construct(T *p)
  {
    // no-op
  }

  // forward everything else with at least one argument to the base
  template<typename Arg1, typename... Args>
    void construct(T* p, Arg1 &&arg1, Args&&... args)
  {
    super_t::construct(p, std::forward<Arg1>(arg1), std::forward<Args>(args)...);
  }
};

namespace std
{

// XXX specialize allocator_traits
//     this shouldn't be necessary, but clang++ 2.7 + libc++ has trouble
//     recognizing that uninitialized_allocator<T> has a well-formed
//     construct function
template<typename T>
  struct allocator_traits<uninitialized_allocator<T> >
    : std::allocator_traits<std::allocator<T>>
{
  typedef uninitialized_allocator<T> allocator_type;

  // for testing purposes, forward allocate through
  static typename allocator_type::pointer allocate(allocator_type &a, typename allocator_type::size_type n)
  {
    return a.allocate(n);
  }

  template<typename... Args>
    static void construct(allocator_type &a, T* ptr, Args&&... args)
  {
    a.construct(ptr, std::forward<Args>(args)...);
  };
};

}

// uninitialized_vector is implemented by adapting an allocator and
// inheriting from std::vector
// a template alias would be another possiblity

// XXX does not compile with clang++ 2.9
//template<typename T, typename BaseAllocator>
//using uninitialized_vector = std::vector<T, uninitialized_allocator<T,BaseAllocator>>;

template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_vector
    : std::vector<T, uninitialized_allocator<T,BaseAllocator>>
{};

int main()
{
  uninitialized_vector<int> vec;
  vec.resize(10);

  // everything should be 13
  assert(std::count(vec.begin(), vec.end(), 13) == vec.size());

  // copy construction should be preserved
  vec.push_back(7);
  assert(7 == vec.back());

  return 0;
}

Это решение будет работать в зависимости от того, насколько точно конкретный компилятор поставщика и реализация STL std::vector соответствуют С++ 11.

Ответ 1

Я думаю, что проблема сводится к типу инициализации, которую контейнер выполняет над элементами. Для сравнения:

T * p1 = new T;   // default-initalization
T * p2 = new T(); // value-initialization

Проблема со стандартными контейнерами заключается в том, что они принимают аргумент по умолчанию для инициализации значения, как в resize(size_t, T = T()). Это означает, что нет элегантного способа избежать инициализации или копирования значений. (Аналогично для конструктора.)

Даже использование стандартных распределителей не работает, потому что их центральная функция construct() принимает аргумент, который становится инициализированным значением. Вам будет нужно использовать construct(), который использует инициализацию по умолчанию:

template <typename T>
void definit_construct(void * addr)
{
  new (addr) T;  // default-initialization
}

Такая вещь уже не будет подходящим стандартным распределителем, но вы можете создать свой собственный контейнер вокруг этой идеи.

Ответ 2

Вместо использования обертки вокруг контейнера подумайте об использовании обертки вокруг типа элемента:

template <typename T>
struct uninitialized
{
    uninitialized() { }
    T value;
};

Ответ 3

Я не считаю, что это возможно, обертывая вектор (который работает с каждым типом), если вы не измените размер вектора при каждой операции добавления и удаления.

Если вы можете отказаться от упаковки контейнеров STL, вы можете сделать это, сохранив массив кучи char в куче и используя размещение new для каждого из объектов, которые вы хотите построить. Таким образом, вы могли бы точно контролировать, когда были вызваны конструкторы и деструкторы объектов, один за другим.