What is the difference between std::initializer_list and std::span? Both are contiguous sequences of values of some type. Both are non-owning.
So when do we use the first and when do we use the latter?
The short answer is that std::initializer_list<T> is used to create a new range, for the purposes of initialization. While std::span<T> is used to refer to existing ranges, for better APIs.
std::initializer_list<T> is a language feature that actually constructs a new array and owns it. It solves the problem of how to conveniently initialize containers:
template <typename T>
struct vector {
vector(std::initializer_list<T>);
};
vector<int> v = {1, 2, 3, 4};
That creates a std::initializer_list<int> on the fly containing the four integers there, and passes it into vector so that it can construct itself properly.
This is really the only place std::initializer_list<T> should be used: either constructors or function parameters to pass a range in on the fly, for convenience (unit tests are a common place that desires such convenience).
std::span<T> on the other hand is used to refer to an existing range. Its job is to replace functions of the form:
void do_something(int*, size_t);
with
void do_something(std::span<int>);
Which makes such functions generally easier to use and safer. std::span is constructible from any appropriate contiguous range, so taking our example from earlier:
std::vector<int> v = {1, 2, 3, 4};
do_something(v); // ok
It can also be used to replace functions of the form:
void do_something_else(std::vector<unsigned char> const&);
Which can only be called with, specifically, a vector, with the more general:
void do_something_else(std::span<unsigned char const>);
Which can be called with any backing contiguous storage over unsigned char.
With span you have to be careful, since it's basically a reference that just doesn't get spelled with a &, but it is an extremely useful type.
The principle differences between the two are how the language treats them. Or more specifically, how it doesn't in the case of a span.
You cannot create an initializer_list from an existing container object or array. They can only be created by copying from other initializer_lists or from a braced-init-list (ie: { stuff }). That is, the compiler governs the creation of an initializer_list and the array it points into.
Similarly, a constructor that takes only an initializer_list has special meaning to the compiler. When performing list initialization on that type, such constructors are given priority in overload resolution. span is given no special meaning by the compiler.
initializer_list also has lifetime rules that are different from span. The lifetime of the array pointed to by an initializer_list is the lifetime of the initializer_list object that was created from a braced-init-list. This is not true of span; the lifetime of a span created from a container is whatever you say it is based on how you use it in code.
Broadly speaking, you should only use initializer_list when you're initializing an object.
One difference is that std::span can be used dynamically, unlike std::initializer_list.
Related
This question already has answers here:
Range-based for with brace-initializer over non-const values?
(5 answers)
Closed 5 months ago.
This is a follow-up question to:
Innocent range based for loop not working
In this question, the syntax that is used is:
int a{},b{},c{},d{};
for (auto& i : {a, b, c, d}) {
i = 1;
}
That is not legal c++. (In order to make it a work a new container type has to be invented that stores pointers or references internally)
Is this just a sort of a side-effect of two different concepts or why was it not allowed ?
At first this look like a miss in regard to driving the language forward, but knowing much work goes into these things Im guessing it was problematic or perhaps just not considered very much.
Im speculating answers might be something like:
It was problematic in some way, given how initializer_list/arrays/brace-init lists/etc. were designed (and 'fixing' that would either be too hard for compiler writers or would perform less optimal in the generel case).
It would require a special rule or it could potentially be allowed with some fundamental language change (eg. to initializer_list).
It would be ambiguous / unclear to read.
I think the most important part about why this is invalid is reason given at cppreference (since C++14, emphasis mine):
The underlying array is a temporary array of type const T[N], in which
each element is copy-initialized (except that narrowing conversions
are invalid) from the corresponding element of the original
initializer list. The lifetime of the underlying array is the same as
any other temporary object, except that initializing an
initializer_list object from the array extends the lifetime of the
array exactly like binding a reference to a temporary (with the same
exceptions, such as for initializing a non-static class member). The
underlying array may be allocated in read-only memory.
So giving reference to non-const in for loop is invalid. Also as noted in comment, array of references is not valid C++ construct
Relevant cpp standard read: http://eel.is/c++draft/support.initlist.access
It would require a special rule or it could potentially be allowed with some fundamental language change (eg. to initializer_list).
The change would indeed need to be fundamental. The problem is not with how initializer_list is implemented though.
According to [dcl.ref]:
There shall be no references to references, no arrays of references, and no pointers to references.
This makes sense since a reference is not an object in the strict sense. A reference is not required to have storage.
Since arrays of references are not legal, these are a few workarounds:
Use an array of pointers
for (auto* i : {&a, &b, &c, &d}) {
*i = 1;
}
Flavours using std::reference_wrapper. Note that I've used int& instead of auto& to not have to use i.get() = 1:
for (int& i : {std::ref(a), std::ref(b), std::ref(c), std::ref(d)}) {
i = 1;
}
for (int& i : std::initializer_list<std::reference_wrapper<int>>{a,b,c,d}) {
i = 1;
}
If you use it a lot, make a helper:
template<typename T>
using refwrap = std::initializer_list<std::reference_wrapper<T>>;
for (int& i : refwrap<int>{a,b,c,d}) {
i = 1;
}
It does not work because it isn't supposed to work. That's the design of the feature. That feature being list initialization, which as the name suggests is about initializing something.
When C++11 introduced initializer_list, it was done for precisely one purpose: to allow the system to generate an array of values from a braced-init-list and pass them to a properly-tagged constructor (possibly indirectly) so that the object could initialize itself with that sequence of values. The "proper tag" in this case being that the constructor took the newly-minted std::initializer_list type as its first/only parameter. That's the purpose of initializer_list as a type.
Initialization, broadly speaking, should not modify the values it is given. The fact that the array backing the list is a temporary also doubles-down on the idea that the input values should logically be non-modifiable. If you have:
std::vector<int> v = {1, 2, 3, 4, 5};
We want to give the compiler the freedom to make that array of 5 elements a static array in the compiled binary, rather than a stack array that bloats the stack size of this function. More to the point, we logically want to think of the braced-init-list like a literal.
And we don't allow literals to be modified.
Your attempt to make {a, b, c, d} into a range of modifiable references is essentially trying to take a construct that already exists for one purpose and turn it into a construct for a different purpose. You're not initializing anything; you're just using a seemingly convenient syntax that happens to make iterable lists of values. But that syntax is called a "braced-init-list", and it generates an initializer list; the terminology is not an accident.
If you take a tool intended for X and try to hijack it do Y, you're likely to encounter rough edges somewhere down the road. So the reason why it doesn't work is that this it's not meant to; these are not what braced-init-lists and initializer_lists are for.
You might next say "if that's the case, why does for(auto i: {1, 2, 3, 4, 5}) work at all, if braced-init-lists are only for initialization?"
Once upon a time, it didn't; in C++11, that would be il-formed. It was only in C++14 when auto l = {1, 2, 3, 4}; became legal syntax; an auto variable was allowed to automatically deduce a braced-init-list as an initializer_list.
Range-based for uses auto deduction for the range type, so it inherited this ability. This naturally led people to believe that braced-init-lists are about building ranges of values, not initializing things.
In short, someone's convenience feature led you to believe that a construct meant to initialize an objects is just a quick way to create an array. It never was.
Having established that braced-init-lists aren't supposed to do the thing you want them to do, what would it take to make them do what you want?
Well, there are basically two ways to do it: small scale and large scale. The large-scale version would be to change how auto i = {a, b, c, d}; works, so that it could (based on something) create a modifiable range of references to expressions. Range-based for would just use its existing auto-deduction machinery to pick up on it.
This is of course a non-starter. That definition already has a well-defined meaning: it creates a non-modifiable list of copies of those expressions, not references to their results. Changing it would be a breaking change.
A small-scale change would be to hack range-based for to do some fancy deduction based on whether the range expression is a braced-init-list or not. Now, because such ranges and their iterators are buried by the compiler-generated code for range-based for, you won't have as many backwards compatibility problems. So you could do make a rule where, if your range-statement defines a non-const reference variable, and the range-expression is a braced-init-list, then you do some different deduction mechanisms.
The biggest problem here is that it's a complete and total hack. If it's useful to do for(auto &i : {a, b, c d}), then it's probably useful to be able to get the same kind of range outside of a range-based for loop. As it currently stands, the rules about auto-deduction of braced-init-lists are consistent everywhere. Range-based for gains its capabilities simply because it uses auto deduction.
The last thing C++ needs is more inconsistency.
This is doubly important in light of C++20 adding an init-statement to range-for. These two things ought to be equivalent:
for(auto &i : {a, b, c, d})
for(auto &&rng = {a, b, c, d}; auto &i: rng)
But if you change the rules only based on the range-expression and range-statement, they wouldn't be. rng would be deduced according to existing auto rules, thus making the auto &i non-functional. And having a super-special-case rule that changes how the init-statement of a range-for behaves, different from the same statement in other locations, would be even more inconsistent.
Besides, it's not too difficult to write a generic reference_range function that takes non-const variadic reference parameters (of the same type) and returns some kind of random-access range over them. That will work everywhere equally and without compatibility problems.
So let's just avoid trying to make a syntactic construct intended for initializing objects into a generic "list of stuff" tool.
It appears that in C++20, we're getting some additional utility functions for smart pointers, including:
template<class T> unique_ptr<T> make_unique_for_overwrite();
template<class T> unique_ptr<T> make_unique_for_overwrite(size_t n);
and the same for std::make_shared with std::shared_ptr. Why aren't the existing functions:
template<class T, class... Args> unique_ptr<T> make_unique(Args&&... args); // with empty Args
template<class T> unique_ptr<T> make_unique(size_t n);
enough? Don't the existing ones use the default constructor for the object?
Note: In earlier proposals of these functions, the name was make_unique_default_init().
These new functions are different:
Original make_XYZ: Always initializes the pointed-to value ("explicit initialization", see § class.expl.init in the standard).
New make_XYZ_for_overwrite: Performs "default initialization" of the pointed-to value (see § dcl.init, paragraph 7 in the standard); on typical machines, this means effectively no initialization for non-class, non-array types. (Yes, the term is a bit confusing; please read the paragraph at the link.)
This is a feature of plain vanilla pointers which was not available with the smart pointer utility functions: With regular pointers you can just allocate without actually initializing the pointed-to value:
new int
For unique/shared pointers you could only achieve this by wrapping an existing pointer, as in:
std::unique_ptr<int[]>(new int[n])
now we have a wrapper function for that.
Note: See the relevant ISO C++ WG21 proposal as well as this SO answer
allocate_shared, make_shared, and make_unique all initialize the underlying object by performning something equivalent to new T(args...). In the zero-argument case, that reduces to new T() - which is to say, it performs value initialization. Value initialization in many cases (including scalar types like int and char, arrays of them, and aggregates of them) performs zero initialization - which is to say, that is actual work being done to zero out a bunch of data.
Maybe you want that and that is important to your application, maybe you don't. From P1020R1, the paper that introduced the functions originally named make_unique_default_init, make_shared_default_init, and allocate_shared_default_init (these were renamed from meow_default_init to meow_for_overwrite during the national ballot commenting process for C++20):
It is not uncommon for arrays of built-in types such as unsigned char or double to be immediately initialized by the user in their entirety after allocation. In these cases, the value initialization performed by allocate_shared, make_shared, and make_unique is redundant and hurts performance, and a way to choose default initialization is needed.
That is, if you were writing code like:
auto buffer = std::make_unique<char[]>(100);
read_data_into(buffer.get());
The value initialization performed by make_unique, which would zero out those 100 bytes, is completely unnecessary since you're immediately overwriting it anyway.
The new meow_for_overwrite functions instead perform default initialization since the memory used will be immediately overwritten anyway (hence the name) - which is to say the equivalent of doing new T (without any parentheses or braces). Default initialization in those cases I mentioned earlier (like int and char, arrays of them, and aggregates of them) performs no initialization, which saves time.
For class types that have a user-provided default constructor, there is no difference between value initialization and default initialization: both would just invoke the default constructor. But for many other types, there can be a large difference.
By forwarded in-place construction, I take to mean std::allocator::construct and the various emplace methods, e.g., std::vector::emplace_back. I just find that forwarded in-place construction in C++ does not (unable to?) take advantage of the list-initialization syntax. As a result, it seems one can never forward in-place construct an aggregate. I just want to make sure whether forwarded in-place construction does not support list-initialization and hence aggregate types. Is this due to the limitation of the language? Could someone provide reference to the standard concerning this issue? Following is an illustration:
While we can do in-place construction directly like
int(*p)[3] = ...;
new(p) int[3]{1, 2, 3};
we cannot do forwarded in-place construction like
std::allocator<int[3]> allo;
allo.construct(p, 1, 2, 3);
While {} has been called uniform initialization syntax, it is far from universal.
take these two examples:
size_t a = 3;
size_t b = 1;
std::vector<size_t> v1{a,b};
std::vector<size_t> v2(a,b);
in the first case, we construct a vector containing two elements, 3 and 1.
In the second case, we create a vector containing 1,1,1 -- 3 copies of 1.
live example.
So {} based construction can cause a different behavior than () based construction in some cases. What more, in the above case, there is no way to reach the "3 copies of 1" syntax using {} construction (that I know of). But the {3,2} case can be handled by simply explicitly creating an initializer list and passing it to ().
As most types that take initializer lists can be emplace constructed by explicitly passing in an initializer list, and the C++ standard library was designed for types with constructors more than types without them, the C++ standard library nearly uniformly emplace constructs using () and not {}.
The downside is that types that want to be list-initialized cannot be emplaced via this mechanism.
In theory, list_emplace methods that construct using {} instead could be added to each interface. I encourage you to propose that!
std::allocator's construct() (and also the default implementation provided by std::allocator_traits) are specified to use (): ::new((void *)p) U(std::forward<Args>(args)...) (see [allocator.members]/p12, [allocator.traits.members]/p5).
It is impractical to change it to {} at this point, because it would silently break existing code:
std::vector<std::vector<int>> foo;
foo.emplace_back(10, 10); // add a vector of ten 10s with (); two 10s with {}
There is an LWG issue to make it fall back to using {} if () doesn't work. We'll have to see whether the committee agrees with that direction.
#Yakk points out the potential drawbacks with this approach:
foo.emplace_back(10); // ten 0s
foo.emplace_back(10, 10); // ten 10s
foo.emplace_back(10, 10, 10); // three(!) 10s
A similar issue (see Appendix B of N2215) led to the decision that list-initialization will always prefer initializer_list constructors.
Reading the answer to this question, I was surprised to find that std::min(std::initializer_list<T>) takes its arguments by value.
If you use std::initializer_list in the way implied by its name, i.e. as an initializer to some object, I understand that we don't care about copying its elements since they will be copied anyway to initialize the object. However, in this case here we most likely don't need any copy, so it would seem much more reasonable to take the arguments as std::initializer_list<const T&> if only it were possible.
What's the best practice for this situation? Should you not call the initializer_list version of std::min if you care about not making unnecessary copies, or is there some other trick to avoid the copy?
There is no such thing as std::initializer_list<const T&>. Initializer lists can only hold objects by value.
See [dcl.init.list]/5:
An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation allocated a temporary array of N elements of type const E, where N is the number of elements in the initializer list.
There are no arrays of references, so there cannot be an initializer_list of references either.
My suggestion in this circumstance would be to write a replacement for std::min which takes a variadic template of forwarding references. This works (although I'm sure it can be improved):
template<typename T, typename... Args>
T vmin( T arg1, Args&&... args )
{
T *p[] = { &arg1, &args... };
return **std::min_element( begin(p), end(p),
[](T *a, T *b) { return *a < *b; } );
}
See it working - using min or mmin, two extra copies are made (for a and b).
To sum up the comments:
An std::initializer_list is supposed to be a lightweight proxy as described on cppreference.com.
Therefore a copy of an initializer list should be very fast since the underlying elements are not copied:
C++11 §18.9/2
An object of type initializer_list<E> provides access to an array of objects of type const E.
[ Note:
A pair of pointers or a pointer plus a length would be obvious representations for initializer_list.
initializer_list is used to implement initializer lists as specified in 8.5.4. Copying an initializer list does not copy the underlying elements. — end note ]
Using a reference though would boil down to using a pointer and therefore an additional indirection.
The actual problem of std::min therefore is not that it takes the initializer_list by value, but rather, that the arguments to initializer_list have to be copied if they are computed at runtime. [1]
That the benchmark at [1] was broken as found out later is unfortunate.
auto min_var = std::min({1, 2, 3, 4, 5}); // fast
auto vec = std::vector<int>{1, 2, 3, 4, 5};
min_var = std::min({vec[0], vec[1], vec[2], vec[3], vec[4]}); // slow
[1]: N2722 p. 2
My comments on this answer got me thinking about the issues of constness and sorting. I played around a bit and reduced my issues to the fact that this code:
#include <vector>
int main() {
std::vector <const int> v;
}
will not compile - you can't create a vector of const ints. Obviously, I should have known this (and intellectually I did), but I've never needed to create such a thing before. However, it seems like a useful construct to me, and I wonder if there is any way round this problem - I want to add things to a vector (or whatever), but they should not be changed once added.
There's probably some embarrassingly simple solution to this, but it's something I'd never considered before.
I probably should not have mentioned sorting (I may ask another question about that, see this for the difficulties of asking questions). My real base use case is something like this:
vector <const int> v; // ok (i.e. I want it to be OK)
v.push_back( 42 ); // ok
int n = v[0]; // ok
v[0] = 1; // not allowed
Well, in C++0x you can...
In C++03, there is a paragraph 23.1[lib.containers.requirements]/3, which says
The type of objects stored in these components must meet the requirements of CopyConstructible types (20.1.3), and the additional requirements of Assignable types.
This is what's currently preventing you from using const int as a type argument to std::vector.
However, in C++0x, this paragraph is missing, instead, T is required to be Destructible and additional requirements on T are specified per-expression, e.g. v = u on std::vector is only valid if T is MoveConstructible and MoveAssignable.
If I interpret those requirements correctly, it should be possible to instantiate std::vector<const int>, you'll just be missing some of its functionality (which I guess is what you wanted). You can fill it by passing a pair of iterators to the constructor. I think emplace_back() should work as well, though I failed to find explicit requirements on T for it.
You still won't be able to sort the vector in-place though.
Types that you put in a standard container have to be copyable and assignable. The reason that auto_ptr causes so much trouble is precisely because it doesn't follow normal copy and assignment semantics. Naturally, anything that's const is not going to be assignable. So, you can't stick const anything in a standard container. And if the element isn't const, then you are going to be able to change it.
The closest solution that I believe is possible would be to use an indirection of some kind. So, you could have a pointer to const or you could have an object which holds the value that you want but the value can't be changed within the object (like you'd get with Integer in Java).
Having the element at a particular index be unchangeable goes against how the standard containers work. You might be able to construct your own which work that way, but the standard ones don't. And none which are based on arrays will work regardless unless you can manage to fit their initialization into the {a, b, c} initialization syntax since once an array of const has been created, you can't change it. So, a vector class isn't likely to work with const elements no matter what you do.
Having const in a container without some sort of indirection just doesn't work very well. You're basically asking to make the entire container const - which you could do if you copy to it from an already initialized container, but you can't really have a container - certainly not a standard container - which contains constants without some sort of indirection.
EDIT: If what you're looking to do is to mostly leave a container unchanged but still be able to change it in certain places in the code, then using a const ref in most places and then giving the code that needs to be able to change the container direct access or a non-const ref would make that possible.
So, use const vector<int>& in most places, and then either vector<int>& where you need to change the container, or give that portion of the code direct access to the container. That way, it's mostly unchangeable, but you can change it when you want to.
On the other hand, if you want to be able to pretty much always be able to change what's in the container but not change specific elements, then I'd suggest putting a wrapper class around the container. In the case of vector, wrap it and make the subscript operator return a const ref instead of a non-const ref - either that or a copy. So, assuming that you created a templatized version, your subscript operator would look something like this:
const T& operator[](size_t i) const
{
return _container[i];
}
That way, you can update the container itself, but you can't change it's individual elements. And as long as you declare all of the functions inline, it shouldn't be much of a performance hit (if any at all) to have the wrapper.
You can't create a vector of const ints, and it'd be pretty useless even if you could. If i remove the second int, then everything from there on is shifted down one -- read: modified -- making it impossible to guarantee that v[5] has the same value on two different occasions.
Add to that, a const can't be assigned to after it's declared, short of casting away the constness. And if you wanna do that, why are you using const in the first place?
You're going to need to write your own class. You could certainly use std::vector as your internal implementation. Then just implement the const interface and those few non-const functions you need.
Although this doesn't meet all of your requirements (being able to sort), try a constant vector:
int values[] = {1, 3, 5, 2, 4, 6};
const std::vector<int> IDs(values, values + sizeof(values));
Although, you may want to use a std::list. With the list, the values don't need to change, only the links to them. Sorting is accomplished by changing the order of the links.
You may have to expend some brain power and write your own. :-(
I would have all my const objects in a standard array.
Then use a vector of pointers into the array.
A small utility class just to help you not have to de-reference the objects and hay presto.
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
class XPointer
{
public:
XPointer(int const& data)
: m_data(&data)
{}
operator int const&() const
{
return *m_data;
}
private:
int const* m_data;
};
int const data[] = { 15, 17, 22, 100, 3, 4};
std::vector<XPointer> sorted(data,data+6);
int main()
{
std::sort(sorted.begin(), sorted.end());
std::copy(sorted.begin(), sorted.end(), std::ostream_iterator<int>(std::cout, ", "));
int x = sorted[1];
}
I'm with Noah: wrap the vector with a class that exposes only what you want to allow.
If you don't need to dynamically add objects to the vector, consider std::tr1::array.
If constness is important to you in this instance I think you probably want to work with immutable types all the way up. Conceptually you'll have a fixed size, const array of const ints. Any time you need to change it (e.g. to add or remove elements, or to sort) you'll need to make a copy of the array with the operation performed and use that instead.
While this is very natural in a functional language it doesn't seem quite "right" in C++. getting efficient implementations of sort, for example, could be tricky - but you don't say what you're performance requirements are.
Whether you consider this route as being worth it from a performance/ custom code perspective or not I believe it is the correct approach.
After that holding the values by non-const pointer/ smart pointer is probably the best (but has its own overhead, of course).
I've been thinking a bit on this issue and it seems that you requirement is off.
You don't want to add immutable values to your vector:
std::vector<const int> vec = /**/;
std::vector<const int>::const_iterator first = vec.begin();
std::sort(vec.begin(), vec.end());
assert(*vec.begin() == *first); // false, even though `const int`
What you really want is your vector to hold a constant collection of values, in a modifiable order, which cannot be expressed by the std::vector<const int> syntax even if it worked.
I am afraid that it's an extremely specified task that would require a dedicated class.
It is true that Assignable is one of the standard requirements for vector element type and const int is not assignable. However, I would expect that in a well-thought-through implementation the compilation should fail only if the code explicitly relies on assignment. For std::vector that would be insert and erase, for example.
In reality, in many implementations the compilation fails even if you are not using these methods. For example, Comeau fails to compile the plain std::vector<const int> a; because the corresponding specialization of std::allocator fails to compile. It reports no immediate problems with std::vector itself.
I believe it is a valid problem. The library-provided implementation std::allocator is supposed to fail if the type parameter is const-qualified. (I wonder if it is possible to make a custom implementation of std::allocator to force the whole thing to compile.) (It would also be interesting to know how VS manages to compile it) Again, with Comeau std::vector<const int> fails to compiler for the very same reasons std::allocator<const int> fails to compile, and, according to the specification of std::allocator it must fail to compile.
Of course, in any case any implementation has the right to fail to compile std::vector<const int> since it is allowed to fail by the language specification.
Using just an unspecialized vector, this can't be done. Sorting is done by using assignment. So the same code that makes this possible:
sort(v.begin(), v.end());
...also makes this possible:
v[1] = 123;
You could derive a class const_vector from std::vector that overloads any method that returns a reference, and make it return a const reference instead. To do your sort, downcast back to std::vector.
std::vector of constant object will probably fail to compile due to Assignable requirement, as constant object can not be assigned. The same is true for Move Assignment also. This is also the problem I frequently face when working with a vector based map such as boost flat_map or Loki AssocVector. As it has internal implementation std::vector<std::pair<const Key,Value> > .
Thus it is almost impossible to follow const key requirement of map, which can be easily implemented for any node based map.
However it can be looked, whether std::vector<const T> means the vector should store a const T typed object, or it merely needs to return a non-mutable interface while accessing.
In that case, an implementation of std::vector<const T> is possible which follows Assignable/Move Assignable requirement as it stores object of type T rather than const T. The standard typedefs and allocator type need to be modified little to support standard requirements.Though to support such for a vector_map or flat_map, one probably needs considerable change in std::pair interface as it exposes the member variables first & second directly.
Compilation fails because push_back() (for instance) is basically
underlying_array[size()] = passed_value;
where both operand are T&. If T is const X that can't work.
Having const elements seem right in principle but in practice it's unnatural, and the specifications don't say it should be supported, so it's not there. At least not in the stdlib (because then, it would be in vector).