12

I'm interested in building an uninitialized_vector container, which will be semantically identical to std::vector with the caveat that new elements which otherwise would be created with a no-argument constructor will instead be created without initialization. I'm primarily interested in avoiding initializing POD to 0. As far as I can tell, there's no way to accomplish this by combining std::vector with a special kind of allocator.

I'd like to build my container in the same vein as std::stack, which adapts a user-provided container (in my case, std::vector). In other words, I'd like to avoid reimplementing the entirety of std::vector and instead provide a "facade" around it.

Is there a simple way to control default construction from the "outside" of std::vector?


Here's the solution I arrived at, which was inspired Kerrek's answer:

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

// uninitialized_allocator adapts a given base allocator
// uninitialized_allocator's 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;
}

This solution will work depending on how closely a particular vendor's compiler & STL's std::vector implementation conforms to c++11.

Jared Hoberock
  • 11,118
  • 3
  • 40
  • 76
  • Can you just define a default constructor that does nothing? I bet the compiler may inline or omit it esp with optimization turned on. – Doug T. Aug 28 '11 at 02:00
  • @Doug T. - That may solve the case of the default constructor, but the interesting stuff seems to happen in ```resize``` -- it's unclear how to call functions like ```resize``` without invoking constructors. – Jared Hoberock Aug 28 '11 at 02:02
  • 2
    @Doug: That has the caveat that you're no longer POD with a user-defined constructor... – Kerrek SB Aug 28 '11 at 02:03

3 Answers3

7

Instead of using a wrapper around the container, consider using a wrapper around the element type:

template <typename T>
struct uninitialized
{
    uninitialized() { }
    T value;
};
James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • 1
    Doesn't that still default construct `value`? – Seth Carnegie Aug 28 '11 at 02:05
  • Ah, didn't realise he was using only POD types. – Seth Carnegie Aug 28 '11 at 02:09
  • I think something clever like this may work, but I have the additional requirements of preserving traits like ```std::vector::value_type```, ```std::vector::reference```, etc. These shouldn't expose ```unintialized```. – Jared Hoberock Aug 28 '11 at 02:10
  • 1
    Writing an `uninitialized_vector` in terms of `vector>` should be straightforward; each member function just needs to defer to the corresponding member function of the underlying `vector`, encapsulating access to `value` if required. – James McNellis Aug 28 '11 at 02:14
5

I think the problem boils down to the type of initialization that the container performs on elements. Compare:

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

The problem with the standard containers is that they take the default argument to be value initialized, as in resize(size_t, T = T()). This means that there's no elegant way to avoid value-initialization or copying. (Similarly for the constructor.)

Even using the standard allocators doesn't work, because their central construct() function takes an argument that becomes value-initialized. What you would rather need is a construct() that uses default-initialization:

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

Such a thing wouldn't be a conforming standard allocator any more, but you could build your own container around that idea.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • Thanks for this answer, but I don't understand your comment about ```construct()``` taking an argument that becomes value-initialized. Why can't it be a no-op? – Jared Hoberock Aug 28 '11 at 02:43
  • @Jared: I just mean the way that standard allocators are used in standard containers - their `construct()` function is usually something like `construct(void *, const T &)`, so there's at least one copy during construction, but I imagine that this function would be called with a value-initialized object anyway, like `construct(p, T())`, so you can't escape value initialization inside the standard containers. (Even the C++11 emplacing allocators can't help you because you cannot "move" something more than once...) – Kerrek SB Aug 28 '11 at 02:53
  • So basically I'm saying that you could rig up a container that behaves very much like `vector`, but instead of calling the standard allocator, you'd just make it say `new (addr) T;` instead of calling `construct()`. You can keep the other aspects of the allocator (i.e. memory allocation). – Kerrek SB Aug 28 '11 at 02:54
  • Interesting -- it looks like your ```resize(size_t, T = T())``` example actually isn't standard. ```std::vector``` is required to distinguish between ```resize(size_t)``` and ```resize(size_t, const T &)``` (similarly for its constructors). So it appears that a standard allocator may work, depending on how closely ```std::vector``` conforms. – Jared Hoberock Aug 28 '11 at 03:36
  • By "standard" I meant of course [taken from a reliable source](http://programmers.stackexchange.com/questions/88241/whats-wrong-with-cplusplus-com) :-) Indeed, N3290 says that two distinct signatures `resize(size_t)` and `resize(size_t, const T &)` exist. So it's possible that `resize(n)` already behaves as you want. – Kerrek SB Aug 28 '11 at 03:41
  • The problem is that there are [three types of initialization](https://stackoverflow.com/questions/29765961/default-value-and-zero-initialization-mess), but `std::allocator` only has two: zero and value. There is no method for default initialization. If there was, the containers could optionally use that instead. – Mooing Duck May 06 '20 at 17:53
1

I don't believe this is possible by wrapping a vector (that works with every type), unless you resize the vector on every add and remove operation.

If you could give up wrapping STL containers, you could do this by keeping an array of char on the heap and using placement new for each of the objects you want to construct. This way you could control exactly when the constructors and destructors of objects were called, one by one.

Seth Carnegie
  • 73,875
  • 22
  • 181
  • 249