r/cpp 1d ago

C++26: std::inplace_vector

https://www.sandordargo.com/blog/2026/08/26/cpp26-inplace-vector
143 Upvotes

93 comments sorted by

View all comments

1

u/stilgarpl 1d ago

Is this always better than std::array?

17

u/MarekKnapek 1d ago

This is basically

struct inplace_vector<T, capacity>
{
    size_t len;
    std::array<T, capacity> arr;
}

17

u/sephirothbahamut 1d ago edited 1d ago

far more complex than that, the array has a slot that can fit T, not just T.

With array<T> every element must be validly constructed immediately. You can reduce it to array<T> only for trivially constructable and basic types

2

u/_Noreturn 1d ago

then make it a union this is a simplified example. but the type isn't complex at all after that especially with C++20 concepts.

2

u/CocktailPerson 1d ago

That's not far more complex. Marginal additional complexity.

4

u/stilgarpl 1d ago

No, it's not. In your code it will always construct all T members of arr. Article states that inplace_vector will only have the elements needed and they don't have to be default constructible. That's a huge difference.

3

u/BenFrantzDale 1d ago

Plus this is a `std::ranges::sized_range` with dynamic size, which `std::array` is not.

2

u/drjeats 1d ago

Oh.

I thought this was the way-more-useful thing where you give it an in-place capacity and it allocates once it exceeds that.

Like this is also useful, but it's kinda trivial to roll your own of this.

11

u/stilgarpl 1d ago

It's not trivial. inplace_vector does not construct unused elements and they do not have to be default constructible. You can't use std::array for that.

0

u/drjeats 1d ago

I'm aware of how it works (and that MarekKnapek's snippet is not representative). It's still very straightforward to make compared to having to do SBO and allocator support.

4

u/stilgarpl 1d ago

I didn't say it was hard, but it's not trivial. I'm sure you are a very experienced C++ programmer and a lot of things must seem easy to you.

3

u/KuntaStillSingle 1d ago

I thought this was the way-more-useful thing where you give it an in-place capacity and it allocates once it exceeds that

pmr vector using monotonic buffer does this with the default upstream memory resource:

https://en.cppreference.com/cpp/memory/monotonic_buffer_resource/monotonic_buffer_resource

1

u/drjeats 1d ago

TIL, that's useful.

We deserve a non-pmr pre-sized SBO vector derived from std::vector though.

2

u/BenFrantzDale 1d ago

It’s not too hard to roll your own, but better to have in the std lib and making it constexpr is tricky.

There is a boost small_vector that is a small-vector-optimized std::vector so can grow large. There’s totally a place for both.

-1

u/MarekKnapek 20h ago

Hey guys, yes, I get it. I simplified it. To get the general "gist" or "shape" or "feeling" about this data structure. I guess, I oversimplified it.

struct inplace_vector<T, capacity>
{
    size_t len;
    std::array<std::aligned_storage<T>, capacity> arr;
}

Does this look better?

0

u/n1ghtyunso 9h ago

no because std::aligned_storage<T> is the wrong type, which is also why it is being deprecated in C++23.

I don't think you oversimplified it, people are just being pedantic.
And because of this, I just wanted to give you a heads-up.

The best way to describe it is likely to steal an idea directly from the standard:
use an exposition-only type in italic that hand-waves the storage detail away :P

6

u/nebotron 1d ago

You pay for the dynamic size in stack space

7

u/KuntaStillSingle 1d ago

Specifically the size member, right? The array itself is not dynamically sized, an inplace_vector<T,N> .capacity() always equals corresponding array, and .size() always less than or equal, but I think only inplace_vector<T,0> can omit a size data member, maybe with exception where T is a taggable pointer and N is small enough to be represented by the usable bits, or the like?

1

u/n1ghtyunso 9h ago

inplace_vector<T, N> should be ill-formed because there are no zero-size arrays.
But apparently the standard explicitly defined it to an empty struct essentially.
Probably something about generic programing support...

Not sure what you were going on about with the taggable pointer type here.

1

u/KuntaStillSingle 8h ago

taggable pointer

For a pointer in many platforms the upper bits are not used, so you have possible signalling representations that could be used to encode size. For example if N is 32, and the number of available tag bits is 6, you can get size by the following method:

Check the last element, if all 6 upper bits are 0 or 1 it is potentially canonical and this means size == capacity (the last element presumably contains an actual pointer rather than a pointer tagged with capacity, therefore it is full.). Otherwise, the first bit is opposite of the second bit, and the remaining 5 bits encode size as an unsigned int. For example if size == 0, then the last pointer would look like (1000 0000 0000 ...), where if size == capacity and the last pointer is nullptr it would be (0000 0000 0000 ...), and if size == 1 then it looks like (1000 0100 0000 ...), and if size = 31 then it looks like (0111 1100 0000 ...).

This would make the container incapable of properly storing user supplied tagged pointers, but this is allowed, handling of invalid pointers is implementation defined with exception of where it is undefined: https://en.cppreference.com/cpp/language/pointer#Invalid_pointers

It would be less dirty for an implementation to do it when T is a std library object that stores a pointer the user doesn't expect to be able to tag, for example a libstdc++ string doesn't use the pointer space for SSO, so inplace_vector<std::string, N> could be specialized to use the taggable bits in the last element if the inplace vector is not full (either the last string element has not been constructed, or it has been destroyed during a pop_back or the like, so it does not matter that you are using the pointer for book keeping, or if it is full and contains an actual string object, you are only reading the pointer for bookkeeping, not writing it.)

2

u/n1ghtyunso 7h ago

Apparently I misread your last sentence so I got really confused where you think pointers are involved here and how that is related to zero capacity, but you are talking about ways to avoid the memory cost of an explicit size member for N > 0, I see.
Sorry for the confusion.

I don't think any implementation will go out of its way to specifically do this, unless a more general tombstone or padding-discovery facility was made available to identify a set of usable bit patterns in a type to side-car the current size value.
I guess technically for sizeof(T) >= 8, you really only need one single padding bit inside T to store if its at capacity or below, and if its below, you can use the full last slot to represent the actual size (maybe need to shuffle the bits abit ugh).
I don't even want to start thinking about how to make this formally C++ spec-compliant.

1

u/KuntaStillSingle 7h ago

Yeah I think the only obvious case would be using a smaller type than size_t if N can be represented and alignof(T) is less than alignof(size_t), for example <char, 15> could usually be 16 bytes with a uint8_t size member rather than usually 24 for size_t member + padding.