3

Imagine I want to construct a fixed-size std::vector of objects without move or copy constructors, such as std::atomic<int>. In this case the underlying std::atomic class has a 1-arg constructor which takes an int, as well as a default constructor (which initializes the value to 0).

Using the initializer_list syntax like std::vector<std::atomic<int>> v{1,2,3} doesn't work, because the arguments are first converted to the element type T of the vector as part of the creation of the initializer_list and so the copy or move constructor will be invoked.

In the particular case of std::atomic<int> I can default-construct the vector and then mutate the elements after:

std::vector<std::atomic<int>> v(3);
v[0] = 1;
v[1] = 2;
v[2] = 3;

However, in addition to being ugly and inefficient, it isn't a general solution since many objects may not offer post-construction mutation equivalent to what you could get by calling the appropriate constructor.

Is there any way to get the "emplace-like" behavior that I want at vector construction?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • 2
    Seriously, I would just use `std::deque`. But if you can't, the only way to do what you want is through a custom allocator. – Brian Bi Oct 13 '17 at 19:09
  • @Brian - does `std::deque` allow this construction idiom? – BeeOnRope Oct 13 '17 at 19:09
  • 1
    With `std::deque` you'll have to emplace the elements one by one, but it'll work because adding elements to the beginning or end doesn't move any of the other elements. – Brian Bi Oct 13 '17 at 19:10
  • @Brian - right, it's at least better in `std::vector` in that respect, unfortunately sometimes I need the guarantee of contiguous storage that `vector` offers. I am also interested in the custom allocator approach: it isn't obvious to me how a custom allocator gets around the `vector` constructor interface. – BeeOnRope Oct 13 '17 at 19:11
  • Possible duplicate of [How to emplace elements while constructing std::vector?](https://stackoverflow.com/questions/46686258/how-to-emplace-elements-while-constructing-stdvector) – Chris Drew Oct 13 '17 at 21:33
  • It seems to me this is the same question as https://stackoverflow.com/questions/13193484/how-to-declare-a-vector-of-atomic-in-c , with the words shuffled around a bit – M.M Oct 13 '17 at 22:19
  • @M.M - it's a fair bit different. The OP there wanted to `push_back` elements into a vector, which isn't ever going to be allowed for non-movable types since there is a potential resizing which requires moveability. The idea of emplacing objects directly into a newly constructed vector, however, can work in theory and in practice, as the last part of the answer below shows using the constructor that takes two iterators. – BeeOnRope Oct 13 '17 at 23:12

1 Answers1

2

A general solution is to make your vector take a custom allocator whose construct method performs the appropriate initialization. In the code below, v uses the MyAllocator<NonMovable> allocator rather than std::allocator<NonMovable>. When the construct method is called with no arguments, it actually calls the constructor with the appropriate argument. In this way, the default constructor can initialize the elements properly.

(For simplicitly, I have made next_value static in this example, but it could just as well be a non-static member variable that's initialized when MyAllocator is constructed.)

#include <stdio.h>
#include <memory>
#include <new>
#include <vector>

struct NonMovable {
    NonMovable(int x) : x(x) {}
    const int x;
};

template <class T>
struct MyAllocator {
    typedef T value_type;
    static int next_value;
    T* allocate(size_t n) {
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    void deallocate(T* p, size_t n) {
        ::operator delete(p);
    }
    template <class U>
    void construct(U* p) {
        new (p) U(++next_value);
    }
};

template <class T> int MyAllocator<T>::next_value = 0;

int main() {
    std::vector<NonMovable, MyAllocator<NonMovable>> v(10);
    for (int i = 0; i < 10; i++) {
        printf("%d\n", v[i].x);
    }
}

http://coliru.stacked-crooked.com/a/1a89fddd325514bf

This is the only possible solution when you're not allowed to touch the NonMovable class and its constructor may require multiple arguments. In the case where you only need to pass one argument to each constructor, there is a much simpler solution that uses the range constructor of std::vector, like so:

std::vector<int> ints(10);
std::iota(ints.begin(), ints.end(), 1);
std::vector<NonMovable> v(ints.begin(), ints.end());

(Though if you can't afford the extra memory, then you'll have to write a custom iterator, which will be a lot more code.)

Brian Bi
  • 111,498
  • 10
  • 176
  • 312