15

I need a container with run-time known size with no need to resizing. std::unique_ptr<T[]> would be a useful, but there is no encapsulated size member. In the same time std::array is for compile type size only. Hence I need some combination of these classes with no/minimal overhead.

Is there a standard class for my needs, maybe something in upcoming C++20?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
αλεχολυτ
  • 4,792
  • 1
  • 35
  • 71
  • 2
    Are you referring to type bounds, or size bounds? For the latter, you could just pre-allocate a `vector`. And should that say `unique`? – Carcigenicate Mar 27 '19 at 11:49
  • @Carcigenicate what do you mean by "type bounds"? `vector` has excessive interface, all I would like to have is `std::array` with run-time size. E.g. `operator[]` and `size()` at the first sight. – αλεχολυτ Mar 27 '19 at 11:57
  • I couldn't remember if C++ allows specifying bounds on the type like in Java. If you just want an array with a size defined at runtime, use a, `std::vector`, and use a call to `reserve`. If add a answer, but I'm getting ready to go out, and I suspect this is a dupe. – Carcigenicate Mar 27 '19 at 12:01
  • @Carcigenicate `vector` will force initialize items on resize. As far as I know it is not allowed to use `operator[]` for indexes >= size (despite of calling `reserve`). – αλεχολυτ Mar 27 '19 at 12:15
  • 1
    What's the problem? If you want to initialize the elements in addition to reserving, just call `std::vector::resize`. – Fureeish Mar 27 '19 at 14:08
  • 1
    @αλεχολυτ no container allows you to access elements that don't exist. calling `new T[n]` initialises `n` `T`s – Caleth Mar 27 '19 at 14:48
  • 1
    Whenever you have no plans to resize, then the capacity number stored in a `std::vector` appears to be wasteful. But its cost is negligible compared to the time and space cost of dynamic allocation. As such, using a non-resizable dynamic array class doesn't really gain you anything tangible over just using `std::vector`. (I learned this the hard way). – hegel5000 Mar 27 '19 at 15:51

5 Answers5

11

Use std::vector. This is the class for runtime sized array in the STL.

It let you resize it or pushing elements into it:

auto vec = std::vector<int>{};

vec.resize(10); // now vector has 10 ints 0 initialized
vec.push_back(1); // now 11 ints

Some problems stated in the comments:

vector has an excessive interface

So is std::array. You have more than 20 function in std::array including operators.

Just don't use what you don't need. You don't pay for the function you won't use. It won't even increase your binary size.

vector will force initialize items on resize. As far as I know, it is not allowed to use operator[] for indexes >= size (despite calling reserve).

This is not how it is meant to be used. When reserving you should then resize the vector with resize or by pushing elements into it. You say vector will force initialize elements into it, but the problem is that you cannot call operator= on unconstructed objects, including ints.

Here's an example using reserve:

auto vec = std::vector<int>{};

vec.reserve(10); // capacity of at least 10
vec.resize(3); // Contains 3 zero initialized ints.

// If you don't want to `force` initialize elements
// you should push or emplace element into it:

vec.emplace_back(1); // no reallocation for the three operations.
vec.emplace_back(2); // no default initialize either.
vec.emplace_back(3); // ints constructed with arguments in emplace_back

Keep in mind that there is a high chance for such allocation and use case, the compiler may completely elide construction of elements in the vector. There may be no overhead in your code.

I would suggest to measure and profile if your code is subject to very precise performance specification. If you do not have such specification, most likely this is premature optimization. The cost of memory allocation completely out measure the time it takes to initialize elements one by one.

Other parts of your program may be refactored to gain much more performance than trivial initialization can offer you. In fact, getting in the way of it may hinder optimization and make your program slower.

αλεχολυτ
  • 4,792
  • 1
  • 35
  • 71
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • 1
    How would you suggest to initialize only say 5th element in 10 elements vector? Now about overhead, storing the only size and pointer to the data will occupy `2*sizeof(size_t)` = 16 bytes on stack for x64. In the same time most popular compilers use 24 bytes for `std::vector`. 8 bytes are not too much, but I don't need them. – αλεχολυτ Mar 27 '19 at 14:56
  • @αλεχολυτ a vector is a size, a pointer to data and a *capacity*. The capacity is the thing that let you reseve before actually resizing. You need those 8 bytes. – Guillaume Racicot Mar 27 '19 at 14:58
  • 3
    @αλεχολυτ: Define "initialize only say 5th element". Objects have to undergo *some* form of building, even if that ultimately boils down to being in an uninitialized state. The memory in the reserved portion doesn't have objects in it, initialized or not. And if the space before the 5th element doesn't have objects in it, then it is not an array of 10 elements. – Nicol Bolas Mar 27 '19 at 14:59
  • @NicolBolas I mean something like this in raw-arrays: `int a[10]; a[5] = 42;` only `a[5]` is initialized. I can do same with `unique_ptr`. – αλεχολυτ Mar 27 '19 at 15:00
  • @αλεχολυτ if you want to only initialize like 5th element, you can create a vector of raw memory, use placement new to initialize only some element and reintepret that memory to another type. Unless you have specific performance specification, this is pure premature optimization and you should use a normal vector. And keep in mind that you'll have to store somewhere a list of which elements have been constructed. That in itself require memory. – Guillaume Racicot Mar 27 '19 at 15:02
  • @αλεχολυτ and the caveat with the "allocate and placement new" strategy is that you don't get the implementation defined magic of `std::vector` that allows it to have an underlying array without creating one, i.e. you lose `data()` – Caleth Mar 27 '19 at 15:05
  • 1
    @αλεχολυτ also, keep in mind that in an optimized build, the compiler may elide initialization completely. – Guillaume Racicot Mar 27 '19 at 15:09
  • 2
    @αλεχολυτ Are you sure that what you want is something sensible? You wrote `int a[10]; a[5] = 42;`; how will you keep track of the fact that only `a[5]` is initialized, and avoid reading from other indices (which is undefined behaviour)? – Brian Bi Mar 27 '19 at 15:35
  • 1
    @GuillaumeRacicot Unfortunately, no compiler I tested seems to be able to optimize out the unnecessary initialization (see example in my answer below). Even given a simple `vector` that get's completely overwritten right after construction, every major compiler will effectively emit an unnecessary call to `memset`, even at maxium optimization level… – Michael Kenzel Mar 27 '19 at 17:09
  • `std::vector` is incompatible with non copy/movable types – OwnageIsMagic Nov 11 '20 at 21:55
  • @OwnageIsMagic not completely. The only fixed requirement is Erasable – Caleth Apr 10 '23 at 10:38
10

Allocate the memory using an std::unique_ptr<T[]> like you suggested, but to use it - construct an std::span (in C++20; gsl::span before C++20) from the raw pointer and the number of elements, and pass the span around (by value; spans are reference-types, sort of). The span will give you all the bells and whistles of a container: size, iterators, ranged-for, the works.

#include <span>
// or:
// #include <gsl/span>

int main() {

    // ... etc. ...

    {
        size_t size = 10e5;
        auto uptr { std::make_unique<double[]>(size) };
        std::span<int> my_span { uptr.get(), size };
        do_stuff_with_the_doubles(my_span);
    }

    // ... etc. ...
}

For more information about spans, see:

What is a "span" and when should I use one?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 3
    note that ownership of the data and information about its span are still kept separately in this approach… – Michael Kenzel Mar 27 '19 at 16:25
  • I assume it needs to be `uptr.get()` to make this compile!? Apart from that, this is good when you just need a local buffer. But then you can also just store the size in a local variable, use std:: algorithms in case you need that functionality, and be mostly happy. I would assume that @αλεχολυτ is looking for a solution that allows the container to be passed around as a whole!? – Michael Kenzel Mar 27 '19 at 17:17
  • I do agree though that using a `span` like this will be a nice solution for the case of a local buffer once we get C++20. – Michael Kenzel Mar 27 '19 at 17:18
  • @MichaelKenzel: Spans have been quite usable since around 2015 with the "Core Guidelines Support Library", GSL.. no need to wait for 2020. – einpoklum Mar 27 '19 at 20:20
  • An alternative is to make a small struct of the smart pointer and span object, or a wrapper to the smart pointer and size that generates the span on the fly. This enables passing around the array. – patatahooligan Mar 28 '19 at 13:53
  • I've used your technique, it's useful. – Alen Wesker Mar 01 '22 at 09:38
  • `std::span` is useless if you want dynamically sized arrays that are also empty. Ie you want fixed size vectors. Span is just wrapped c array without actually doing any work, it will dumb iterate over empty memory etc – Enerccio Aug 31 '22 at 09:36
  • @Enerccio: Indeed, span _is_ just a reference type, it doesn't "do work". If someone were to give you some pointer `p` and a size `sz` which is 0, and you would write `for(auto iter = p; iter < p + sz; iter++)` - then you would also "dumb-iterate"; I don't see the problem. – einpoklum Aug 31 '22 at 10:26
  • @einpoklum what I am searching for is something that can create `vector<>` like object but with static memory behind (up to certain size). I don't want to write boilerplate code that vector already has like `push_back` I would only guard methods that can go over "reserved" limit. If it would go above I would allocate completely new structure twice the size and copy elements. I am experimenting with custom allocator. – Enerccio Aug 31 '22 at 10:34
  • @Enerccio: `std::span` is not what you want. It doesn't own anything, nor allocate anything - it's like a pointer with an associated size. Perhaps you want to look at my [question regarding vector-like objects](https://stackoverflow.com/q/67409337/1593077). – einpoklum Aug 31 '22 at 11:03
6

Use std::vector. If you want to remove the possibility of changing it's size, wrap it.

template <typename T>
single_allocation_vector : private std::vector<T>, public gsl::span<T>
{
    single_allocation_vector(size_t n, T t = {}) : vector(n, t), span(vector::data(), n) {}
    // other constructors to taste
};
Caleth
  • 52,200
  • 2
  • 44
  • 75
3

Something called std::dynarray was proposed for C++14:

std::dynarray is a sequence container that encapsulates arrays with a size that is fixed at construction and does not change throughout the lifetime of the object.

But there were too many issues and it didn't become part of the standard.

So there exists no such container currently in the STL. You can keep using vectors with an initial size.

w-m
  • 10,772
  • 1
  • 42
  • 49
  • 2
    I highly doubt OP want a stack allocated array. He wants a vector that `operator[]` works on unconstructed elements. Still your answer has good info mentionning alternatives though. – Guillaume Racicot Mar 27 '19 at 14:18
  • Thanks for the clarification. I read the question as the need for a `std::array` with runtime size, but you may be right. I think I can safely leave the answer up though as the other answers go into detail how to work with vector. – w-m Mar 27 '19 at 15:15
  • Please consider linking to [my SO answer comparing various vector-like types including `dynarray`](https://stackoverflow.com/a/67409339/1593077). – einpoklum Aug 07 '23 at 20:24
1

Unfortunately, no new containers were added in C++ 20 (at least none that I'd be aware of). I would agree, however, that such a container would be very useful. While just using std::vector<T> with reserve() and emplace_back() will usually do OK, it does often generate inferior code compared to using a plain new T[] as the use of emplace_back() seems to inhibit vectorization. If we use an std::vector<T> with an initial size instead, compilers seem to have trouble optimizing away the value initialization of elements, even if the entire vector is going to be overwritten right afterwards. Play with an example here.

You could use, for example, a wrapper like

template <typename T>
struct default_init_wrapper
{
    T t;

public:
    default_init_wrapper() {}
    template <typename... Args>
    default_init_wrapper(Args&&... args) : t(std::forward<Args>(args)...) {}

    operator const T&() const { return t; }
    operator T&() { return t; }
};

and

std::vector<no_init_wrapper<T>> buffer(N);

to avoid the useless initialization for trivial types. Doing so seems to lead to code similarly good as the plain std::unique_ptr version. I wouldn't recommend this though, as it's quite ugly and cubmersome to use, since you then have to work with a vector of wrapped elements.

I guess the best option for now is to just roll your own container. This may serve as a starting point (beware of bugs):

template <typename T>
class dynamic_array
{
public:
    using value_type = T;
    using reference = T&;
    using const_reference = T&;
    using pointer = T*;
    using const_pointer = const T*;
    using iterator = T*;
    using const_iterator = const T*;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;

private:
    std::unique_ptr<T[]> elements;
    size_type num_elements = 0U;

    friend void swap(dynamic_array& a, dynamic_array& b)
    {
        using std::swap;
        swap(a.elements, b.elements);
        swap(a.num_elements, b.num_elements);
    }

    static auto alloc(size_type size)
    {
        return std::unique_ptr<T[]> { new T[size] };
    }

    void checkRange(size_type i) const
    {
        if (!(i < num_elements))
            throw std::out_of_range("dynamic_array index out of range");
    }

public:
    const_pointer data() const { return &elements[0]; }
    pointer data() { return &elements[0]; }

    const_iterator begin() const { return data(); }
    iterator begin() { return data(); }

    const_iterator end() const { return data() + num_elements; }
    iterator end() { return data() + num_elements; }

    const_reverse_iterator rbegin() const { return std::make_reverse_iterator(end()); }
    reverse_iterator rbegin() { return std::make_reverse_iterator(end()); }

    const_reverse_iterator rend() const { return std::make_reverse_iterator(begin()); }
    reverse_iterator rend() { return std::make_reverse_iterator(begin()); }

    const_reference operator [](size_type i) const { return elements[i]; }
    reference operator [](size_type i) { return elements[i]; }

    const_reference at(size_type i) const { return checkRange(i), elements[i]; }
    reference at(size_type i) { return checkRange(i), elements[i]; }

    size_type size() const { return num_elements; }

    constexpr size_type max_size() const { return std::numeric_limits<size_type>::max(); }

    bool empty() const { return std::size(*this) == 0U; }

    dynamic_array() = default;

    dynamic_array(size_type size)
        : elements(alloc(size)), num_elements(size)
    {
    }

    dynamic_array(std::initializer_list<T> elements)
        : elements(alloc(std::size(elements))), num_elements(std::size(elements))
    {
        std::copy(std::begin(elements), std::end(elements), std::begin(*this));
    }

    dynamic_array(const dynamic_array& arr)
    {
        auto new_elements = alloc(std::size(arr));
        std::copy(std::begin(arr), std::end(arr), &new_elements[0]);
        elements = std::move(new_elements);
        num_elements = std::size(arr);
    }

    dynamic_array(dynamic_array&&) = default;

    dynamic_array& operator =(const dynamic_array& arr)
    {
        return *this = dynamic_array(arr);
    }

    dynamic_array& operator =(dynamic_array&&) = default;

    void swap(dynamic_array& arr)
    {
        void swap(dynamic_array& a, dynamic_array& b);
        swap(*this, arr);
    }

    friend bool operator ==(const dynamic_array& a, const dynamic_array& b)
    {
        return std::equal(std::begin(a), std::end(a), std::begin(b));
    }

    friend bool operator !=(const dynamic_array& a, const dynamic_array& b)
    {
        return !(a == b);
    }
};
Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • Be very careful. Your no init wrapper will still initialize construct any non trivial types – Guillaume Racicot Mar 27 '19 at 14:36
  • @GuillaumeRacicot yes, I think that's kinda the best you can do. It will at least leave trivial types uninitialized. With non-trivial types, even using a plain `new T[N]` must initialize the elements. The wrapper would just be one way to bring an `std::vector` on par with the plain dynamic array in terms of codegen, in cases where that would be desired. Of course the wrapper is generally not that great… – Michael Kenzel Mar 27 '19 at 14:40
  • I renamed the wrapper type to better reflect what it actually does. – Michael Kenzel Mar 27 '19 at 17:36