Look at this example (godbolt):
#include <memory>
union U {
int i[1];
};
constexpr int foo() {
U u;
std::construct_at(u.i, 1);
return u.i[0];
}
constexpr int f = foo();
gcc and msvc successfully compile this, but clang complains:
construction of subobject of member 'i' of union with no active member is not allowed in a constant expression
Which compiler is right? I think that clang is wrong here, because C++20's implicit creation of objects (P0593) should make this program valid (because the array should be implicitly created, which should make u.i active), but I'm not sure.
U u;
does not begin the lifetime of the i subobject. Beginning the lifetime of a variable other than an array of type char, unsigned char or std::byte is also not one of the operations specifically qualified to be implicitly creating objects. [basic.intro.object]/13
Therefore at this point the i member is definitively not active and the array object is not alive.
As mentioned by #Sebastian in the question comments, calling std::construct_at on u.i is then not allowed in a constant expression since [expr.const]/6.1 specifically requires the provided pointer to point to an object whose lifetime began during the evaluation of the constant expression (or be storage returned from std::allocator).
Therefore Clang seems correct to me. There is an open GCC bug for exactly this issue here.
I am not sure that this is the intended interpretation though, since Clang does accept the program if a non-array type is used for the member, which by my reasoning would equally not be allowed.
The relevant wording is a consequence of this comment.
In any case, it is not intended that implicit object creation happens in constant expressions although it currently seems to (question), see CWG issue 2469.
Without implicit object creation as explained below, the use in a context requiring a constant expression should then be ill-formed independently of the std::construct_at restriction and the following considerations.
Whether the construction has defined behavior if used outside a constant expression context, I am not entirely sure.
But I think that std::construct_at being specified to be equivalent to a new-expression means that it will call operator new, which is specified to implicitly create objects in the storage it returns. [basic.intro.object]/13
Whether operator new must be an allocating operator new call for this to be true is not fully clear to me. I think the wording "in the returned region of storage" does not require it.
i is of type int[1], which is an implicit-lifetime type, which are implicitly created if necessary by operations qualified to implicitly create objects. [basic.types.general]/9
Therefore I think that construct_at will implicitly create the an array object at u.i and begin its lifetime. I also think that [basic.intro.object]/2 will guarantee that this object becomes subobject of the union, so that u.i will refer to it.
However, given that the storage operated on is only the size of a single int and assuming that this is also the storage meant in [basic.intro.object]/13, only an array of length 1 can be implicitly created in it. Therefore if i was of length larger than 1, the implicitly created array could not overlap exactly with the member and can therefore not become subobject of the union.
In this case implicit object creation could not make return u.i[0]; defined behavior.
There is a discussion of this issue here which seems to indicate that already forming the pointer to the first element of u.i outside its lifetime is UB, in which case the construct_at version with array would more directly have UB, but at least compilers accept both auto x = u.i; and auto x = &u.i[0]; in a constant expression without complaining. As mentioned in the comments to this answer, this also seems wrong.
All in all I think that std::construct_at can generally not be used to activate an array member of a union.
But, suppose you replace the std::construct_at call with
u.i[0] = 1;
Then this assignment will begin the lifetime of the array object, as described in [class.union.general]/6. This is not disqualified for constant expressions since C++20 either. Therefore the code will not be ill-formed if used in a context requiring a constant expression, nor will it have undefined behavior outside of that.
Deferred initialization of an array in a constexpr environment can be achieved with std::allocator and std::construct_at():
#include <memory>
constexpr int foo() {
std::allocator<int> alloc;
int* i; // pointer to first element of array
i = alloc.allocate(100); // allocate memory for 100 elements
std::construct_at(&i[0], 1); // initialize first element (first call of constructor)
int r = i[0];
alloc.deallocate(i, 100); // deallocate before leaving
return r;
}
constexpr int f = foo();
Pointers to the relevant standard clauses:
allocate:
[utilities.memory.default.allocator.members]/5: std::allocator<>::allocate() obtains storage by calling operator ::new and starts the lifetime of the array object, but not the lifetime of the array elements themselves.
[expr.const]/5.19: Explicitly allows std::allocator<>::allocate() in constant expressions, if the memory is deallocated again within the constant expression
construct_at:
[algorithms.specialized.construct]/2: std::construct_at() effectively calls placement new.
[expr.const]/6: Explicitly allows std::construct_at in constant expressions, if the memory is allocated by std::allocator
deallocate:
[expr.const]/5.19: Explicitly allows std::allocator<>::deallocate() in constant expressions, if the memory was allocated before within the constant expression
Related
I ran across this seemingly odd behavior. A const at top level can be used for array size declarations, but not when the const is in a class.
Here's a compiler explorer demo for MSVC, CLANG and GCC all producing an error:
expression did not evaluate to a constant
It's not really a constant if it's not const at the top level?
There's some argument to be made because top level constants can often be stored in read-only memory, while constants that are not at top level cannot. But is this behavior correct?
struct A {
const int i{ 3 };
};
int main()
{
const int ii{ 3 };
A a;
int j[a.i]{}; // C2131: expression did not evaluate to a constant
int k[ii]{};
}
Generally, you can use (meaning perform an lvalue-to-rvalue conversion on) objects with lifetime starting outside a constant expression only if they are variables marked constexpr or their subobjects (plus some other special cases, that I don't think are important here, see [expr.const]/4 for details).
That you can use a const int variable at all in a constant expression is already a very specific exception. Essentially const-qualified integral and enumeration type variables are also usable in constant expressions if you could have added constexpr to them (meaning that their initializer expression is a constant expression).
This exception is there I guess purely for historical reasons, since it had been allowed before constexpr was introduced in C++11.
Note that all of this talks about variables and their subobjects. Non-static data members are specifically not variables and the exception doesn't apply to them. With constexpr this is more obvious by not allowing it on the declaration of a non-static data member in the first place.
The historical rule was never extended to encompass other types that could be marked constexpr, so e.g. const A a; will not help although that would actually cause a to be storable in read-only memory the same way a const int would.
If an object is none of the cases mentioned above, then an lvalue-to-rvalue conversion on it in a constant expression is not allowed, since it is assumed that the value of the object is not determined at compile-time.
Now, in theory the compiler could still do some constant folding and determine that even other objects' values are definitively known at compile-time. But I think the intention is that whether or not an expression is a constant expression should be (reasonably) well-defined independently of the implementation and so shouldn't rely on how much analysis the compiler can do.
For example
A a;
A b(a);
is also guaranteed to result in b.i == 3. How far do you want to require a compiler to go back or keep track of evaluations? You would need to make some definitive specification if you want to keep the behavior consistent between compilers. But there is already a simple method to indicate that you want the compiler to keep track of the values. You just have to add constexpr:
constexpr A a;
constexpr A b(a);
Now b.i can be used as array index (whether or not it is const and whether or not it is initialized).
With the current rules, any compiler only needs to evaluate the value of objects at compile-time when it sees a constexpr variable or a const integral/enumeration type variable. For all other variables it doesn't need to keep track of values or backtrack when it sees them used in a context which requires a constant expression.
The additional effect of constexpr implying const on the variable makes sure that its value will also never be changed in a valid program and so the compiler doesn't need worry about updating or invalidating the value after the initial computation either. And whether or not an expression is a constant expression is (mostly) implementation-dependent.
ii is a compile-time constant. Its value is known at compile-time, and cannot be changed at runtime. So, ii can be used for fixed array sizes at compile-time.
A::i is not a compile-time constant. It is a non-static instance member. Its value is not known until runtime. After an A object is constructed and its i member is initialized, the value of i cannot be changed because of the const, but the caller can initialize i with whatever value it wants, eg: A a{123};, and thus different A objects can have different i values. So, i cannot be used for fixed array sizes at compile-time. But, it can be used for dynamic array sizes at runtime, via new[], std::vector, etc.
TL;DR
Your assumption that const always implies compile time constant is incorrect. See examples at the end of this answer for more details on this.
Now the problem in using a.i as the size of an array is that in standard C++, the size of an array must be a compile time constant, but since i is a non-static data member, it requires an object to be used on. In other words, after construction of the class object nonstatic data member i gets initialized, which in turn means that a.i is not a constant expression, hence we get the mentioned error saying:
expression did not evaluate to a constant
To solve this, you can make i be a constexpr static data member, as shown below. This works because using a static data member doesn't require an object instance (and hence no this pointer).
struct A {
constexpr static int i{ 3 };
};
int main()
{
const int ii{ 3 };
A a;
int j[a.i]{}; //Correct now and works in all compilers
int k[ii]{};
}
I just don't get why a regular const works in some places but not others.
Perhaps you assuming that const implies compile time constant which is a wrong assumption. An example might help you understand this better:
int i = 10; //i is not a constant expression
const int size = i; //size is not a constant expression as the initializer is not a constant expression
//------vvvv------>error here as expected since size is not a constant expression
int arr[size]{};
On the other hand if you were to make i const as shown below, the program will work fine.
const int i = 10; //note the const added here so that now i is a constant expression
const int size = i; //size is a constant expression as the initializer is a constant expression
//------vvvv------>NO ERROR HERE as expected since size is a constant expression
int arr[size]{};
struct A
{
int x;
}
A t{};
t.x = 5;
new (&t) A;
// is it always safe to assume that t.x is 5?
assert(t.x == 5);
As far as I know, when a trivial object of class type is created, the compiler can omit the call of explicit or implicit default constructor because no initialization is required.
(is that right?)
Then, If placement new is performed on a trivial object whose lifetime has already begun, is it guaranteed to preserve its object/value representation?
(If so, I want to know where I can find the specification..)
Well, let's ask some compilers for their opinion. Reading an indeterminate value is UB, which means that if it occurs inside a constant expression, it must be diagnosed. We can't directly use placement new in a constant expression, but we can use std::construct_at (which has a typed interface). I also modified the class A slightly so that value-initialization does the same thing as default-initialization:
#include <memory>
struct A
{
int x;
constexpr A() {}
};
constexpr int foo() {
A t;
t.x = 5;
std::construct_at(&t);
return t.x;
}
static_assert(foo() == 5);
As you can see on Godbolt, Clang, ICC, and MSVC all reject the code, saying that foo() is not a constant expression. Clang and MSVC additionally indicate that they have a problem with the read of t.x, which they consider to be a read of an uninitialized value.
P0593, while not directly related to this issue, contains an explanation that seems relevant:
The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.
That is, reusing the storage occupied by an object in order to create a new object always destroys whatever value was held by the old object, because an object's value dies with its lifetime. Now, objects of type A are transparently replaceable by other objects of type A, so it is permitted to continue to use the name t even after its storage has been reused. That does not imply that the new t holds the value that the old t does. It only means that t is not a dangling reference to the old object.
Going off what is said in P0593, GCC is wrong and the other compilers are right. In constant expression evaluation, this kind of code is required to be diagnosed. Otherwise, it's just UB.
From looking at the Standard, the program has undefined behavior because of an invalid use of an object with indeterminate value.
Per [basic.life]/8, since the object of type A created by the placement new-expression exactly overlays the original object t, using the name t after that point refers to the A object created by the new-expression.
In [basic.indet]/1, we have:
When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced ([expr.ass]).
One important detail here (which I missed at first) is that "obtaining storage" is different from "allocating storage" or the storage duration of a storage region. The "obtain storage" words are also used to define the beginning of an object's lifetime in [basic.life]/1 and in the context of a new-expression in [expr.new]/10:
A new-expression may obtain storage for the object by calling an allocation function ([basic.stc.dynamic.allocation]). ... [ Note: ... The set of allocation and deallocation functions that may be called by a new-expression may include functions that do not perform allocation or deallocation; for example, see [new.delete.placement]. — end note ]
So the placement new-expression "obtains storage" for the object of type A and its subobject of type int when it calls operator new(void*). For this purpose, it doesn't make a difference that the memory locations in the storage region actually have static storage duration. Since "no initialization is performed" for the created subobject of type int with dynamic storage duration, it has an indeterminate value.
See also this Q&A: What does it mean to obtain storage?
In C++, is this code correct?
#include <cstdlib>
#include <cstring>
struct T // trivially copyable type
{
int x, y;
};
int main()
{
void *buf = std::malloc( sizeof(T) );
if ( !buf ) return 0;
T a{};
std::memcpy(buf, &a, sizeof a);
T *b = static_cast<T *>(buf);
b->x = b->y;
free(buf);
}
In other words, is *b an object whose lifetime has begun? (If so, when did it begin exactly?)
This is unspecified which is supported by N3751: Object Lifetime, Low-level Programming, and
memcpy which says amongst other things:
The C++ standards is currently silent on whether the use of memcpy to
copy object representation bytes is conceptually an assignment or an
object construction. The difference does matter for semantics-based
program analysis and transformation tools, as well as optimizers,
tracking object lifetime. This paper suggests that
uses of memcpy to copy the bytes of two distinct objects of two different trivial copyable tables (but otherwise of the same size) be
allowed
such uses are recognized as initialization, or more generally as (conceptually) object construction.
Recognition as object construction will support binary IO, while still
permitting lifetime-based analyses and optimizers.
I can not find any meeting minutes that has this paper discussed, so it seems like it is still an open issue.
The C++14 draft standard currently says in 1.8 [intro.object]:
[...]An object is created by a definition (3.1), by a new-expression
(5.3.4) or by the implementation (12.2) when needed.[...]
which we don't have with the malloc and the cases covered in the standard for copying trivial copyable types seem to only refer to already existing objects in section 3.9 [basic.types]:
For any object (other than a base-class subobject) of trivially
copyable type T, whether or not the object holds a valid value of type
T, the underlying bytes (1.7) making up the object can be copied into
an array of char or unsigned char.42 If the content of the array of
char or unsigned char is copied back into the object, the object shall
subsequently hold its original value[...]
and:
For any trivially copyable type T, if two pointers to T point to
distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a
base-class subobject, if the underlying bytes (1.7) making up obj1 are
copied into obj2,43 obj2 shall subsequently hold the same value as
obj1.[...]
which is basically what the proposal says, so that should not be surprising.
dyp points out a fascinating discussion on this topic from the ub mailing list: [ub] Type punning to avoid copying.
Propoal p0593: Implicit creation of objects for low-level object manipulation
The proposal p0593 attempts to solve this issues but AFAIK has not been reviewed yet.
This paper proposes that objects of sufficiently trivial types be created on-demand as necessary within newly-allocated storage to give programs defined behavior.
It has some motivating examples which are similar in nature including a current std::vector implementation which currently has undefined behavior.
It proposes the following ways to implicitly create an object:
We propose that at minimum the following operations be specified as implicitly creating objects:
Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array.
A call to malloc, calloc, realloc, or any function named operator new or operator new[] implicitly creates objects in its returned storage.
std::allocator::allocate likewise implicitly creates objects in its returned storage; the allocator requirements should require other allocator implementations to do the same.
A call to memmove behaves as if it
copies the source storage to a temporary area
implicitly creates objects in the destination storage, and then
copies the temporary storage to the destination storage.
This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.
A call to memcpy behaves the same as a call to memmove except that it introduces an overlap restriction between the source and destination.
A class member access that nominates a union member triggers implicit object creation within the storage occupied by the union member. Note that this is not an entirely new rule: this permission already existed in [P0137R1] for cases where the member access is on the left side of an assignment, but is now generalized as part of this new framework. As explained below, this does not permit type punning through unions; rather, it merely permits the active union member to be changed by a class member access expression.
A new barrier operation (distinct from std::launder, which does not create objects) should be introduced to the standard library, with semantics equivalent to a memmove with the same source and destination storage. As a strawman, we suggest:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
In addition to the above, an implementation-defined set of non-stasndard memory allocation and mapping functions, such as mmap on POSIX systems and VirtualAlloc on Windows systems, should be specified as implicitly creating objects.
Note that a pointer reinterpret_cast is not considered sufficient to trigger implicit object creation.
The code is legal now, and retroactively since C++98!
The answer by #Shafik Yaghmour is thorough and relates to the code validity as an open issue - which was the case when answered. Shafik's answer correctly refer to p0593 which at the time of the answer was a proposal. But since then, the proposal was accepted and things got defined.
Some History
The possibility of creating an object using malloc was not mentioned in the C++ specification before C++20, see for example C++17 spec [intro.object]:
The constructs in a C++ program create, destroy, refer to, access, and manipulate
objects. An object is created by a definition (6.1), by a new-expression (8.5.2.4),
when implicitly changing the active member of a union (12.3), or when a temporary
object is created (7.4, 15.2).
Above wording does not refer to malloc as an option for creating an object, thus making it a de-facto undefined behavior.
It was then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 and accepted as a DR against all C++ versions since C++98 inclusive, then added into the C++20 spec, with the new wording:
[intro.object]
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)...
...
Further, after implicitly creating objects within a specified region of
storage, some operations are described as producing a pointer to a
suitable created object. These operations select one of the
implicitly-created objects whose address is the address of the start
of the region of storage, and produce a pointer value that points to
that object, if that value would result in the program having defined
behavior. If no such pointer value would give the program defined
behavior, the behavior of the program is undefined. If multiple such
pointer values would give the program defined behavior, it is
unspecified which such pointer value is produced.
The example given in C++20 spec is:
#include <cstdlib>
struct X { int a, b; };
X *make_x() {
// The call to std::malloc implicitly creates an object of type X
// and its subobjects a and b, and returns a pointer to that X object
// (or an object that is pointer-interconvertible ([basic.compound]) with it),
// in order to give the subsequent class member access operations
// defined behavior.
X *p = (X*)std::malloc(sizeof(struct X));
p->a = 1;
p->b = 2;
return p;
}
As for the use of memcpy - #Shafik Yaghmour already addresses that, this part is valid for trivially copyable types (the wording changed from POD in C++98 and C++03 to trivially copyable types in C++11 and after).
Bottom line: the code is valid.
As for the question of lifetime, let's dig into the code in question:
struct T // trivially copyable type
{
int x, y;
};
int main()
{
void *buf = std::malloc( sizeof(T) ); // <= just an allocation
if ( !buf ) return 0;
T a{}; // <= here an object is born of course
std::memcpy(buf, &a, sizeof a); // <= just a copy of bytes
T *b = static_cast<T *>(buf); // <= here an object is "born"
// without constructor
b->x = b->y;
free(buf);
}
Note that one may add a call to the destructor of *b, for the sake of completeness, before freeing buf:
b->~T();
free(buf);
though this is not required by the spec.
Alternatively, deleting b is also an option:
delete b;
// instead of:
// free(buf);
But as said, the code is valid as is.
From a quick search.
"... lifetime begins when the properly-aligned storage for the object is allocated and ends when the storage is deallocated or reused by another object."
So, I would say by this definition, the lifetime begins with the allocation and ends with the free.
Is this code correct?
Well, it will usually "work", but only for trivial types.
I know you did not ask for it, but lets use an example with a non-trivial type:
#include <cstdlib>
#include <cstring>
#include <string>
struct T // trivially copyable type
{
std::string x, y;
};
int main()
{
void *buf = std::malloc( sizeof(T) );
if ( !buf ) return 0;
T a{};
a.x = "test";
std::memcpy(buf, &a, sizeof a);
T *b = static_cast<T *>(buf);
b->x = b->y;
free(buf);
}
After constructing a, a.x is assigned a value. Let's assume that std::string is not optimized to use a local buffer for small string values, just a data pointer to an external memory block. The memcpy() copies the internal data of a as-is into buf. Now a.x and b->x refer to the same memory address for the string data. When b->x is assigned a new value, that memory block is freed, but a.x still refers to it. When a then goes out of scope at the end of main(), it tries to free the same memory block again. Undefined behavior occurs.
If you want to be "correct", the right way to construct an object into an existing memory block is to use the placement-new operator instead, eg:
#include <cstdlib>
#include <cstring>
struct T // does not have to be trivially copyable
{
// any members
};
int main()
{
void *buf = std::malloc( sizeof(T) );
if ( !buf ) return 0;
T *b = new(buf) T; // <- placement-new
// calls the T() constructor, which in turn calls
// all member constructors...
// b is a valid self-contained object,
// use as needed...
b->~T(); // <-- no placement-delete, must call the destructor explicitly
free(buf);
}
So this answer made me think about the scenario where you assign the result of new to a pointer to a const. AFAIK, there's no reason you can't legally const_cast the constness away and actually modify the object in this situation:
struct X{int x;};
//....
const X* x = new X;
const_cast<X*>(x)->x = 0; // okay
But then I thought - what if you actually want new to create a const object. So I tried
struct X{};
//....
const X* x = new const X;
and it compiled!!!
Is this a GCC extension or is it standard behavior? I have never seen this in practice. If it's standard, I'll start using it whenever possible.
new obviously doesn't create a const object (I hope).
If you ask new to create a const object, you get a const object.
there's no reason you can't legally const_cast the constness away and actually modify the object.
There is. The reason is that the language specification calls that out explicitly as undefined behaviour. So, in a way, you can, but that means pretty much nothing.
I don't know what you expected from this, but if you thought the issue was one of allocating in readonly memory or not, that's far from the point. That doesn't matter. A compiler can assume such an object can't change and optimise accordingly and you end up with unexpected results.
const is part of the type. It doesn't matter whether you allocate your object with dynamic, static or automatic storage duration. It's still const. Casting away that constness and mutating the object would still be an undefined operation.
constness is an abstraction that the type system gives us to implement safety around non-mutable objects; it does so in large part to aid us in interaction with read-only memory, but that does not mean that its semantics are restricted to such memory. Indeed, C++ doesn't even know what is and isn't read-only memory.
As well as this being derivable from all the usual rules, with no exception [lol] made for dynamically-allocated objects, the standards mention this explicitly (albeit in a note):
[C++03: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). [Note: because references are not objects, references cannot be created by new-expressions. ] [Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. ] [..]
[C++11: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). It is implementation-defined whether over-aligned types are supported (3.11). [ Note: because references are not objects, references cannot be created by new-expressions. —end note ] [ Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. —end note ] [..]
There's also a usage example given in [C++11: 7.1.6.1/4].
Not sure what else you expected. I can't say I've ever done this myself, but I don't see any particular reason not to. There's probably some tech sociologist who can tell you statistics on how rarely we dynamically allocate something only to treat it as non-mutable.
My way of looking at this is:
X and const X and pointers to them are distinct types
there is an implicit conversion from X* to const X*, but not the other way around
therefore the following are legal and the x in each case has identical type and behaviour
const X* x = new X;
const X* x = new const X;
The only remaining question is whether a different allocator might be called in the second case (perhaps in read only memory). The answer is no, there is no such provision in the standard.
Consider this code:
template <typename T>
void f()
{T x = T();}
When T = int, is x equal to 0 or to an arbitrary value?
Bonus question: and consequently, are arrays (both T[N] and std::array<T, N>) the only types where such a syntax may leave contents with arbitrary values.
T() gives value initialization, which gives zero-initialization for a type other than a class, union or array. (§8.5/7 bullet 3): "otherwise, the object is zero-initialized." For an array, each element of the array is value initialized.
For an array, the content will be of an arbitrary value if it's auto storage class, but zero initialized if it's static storage class -- i.e., global (assuming, of course, that you don't specify any initialization).
Regarding your first question, it is called value-initialization, and is well-covered in the standard (C++11 § 8.5 covers initialization in detail, with the specifics on () initialization and how it eventually leads to zero-initialization, starting with 8.5/16 (covers ()), which leads to 8.5/7 (value-initialization), and finally 8.5/5 (zero-initialization).
Regarding std::array<T,N>, if T is a class-type the constructors will fire for each element (user-provided or default if none are provided by the user). If default construction happens default-initialization will fire (which isn't very exciting). By the standard (8.5/6), each element is default-initialized. For T that is not a class type this is effectively the same as T ar[N]; which as you have pointed out, is indeterminate as well (because it is default initialized, which by the standard "no initialization is performed.".
Finally, if static storage is declared for your fixed array of non-class-type, it resides in zero-filled memory on inception. For automatic storage, its back to "no initialization is performed." as your end-game.
I hope i didn't miss something (and I know i'll hear it if I did). There are lots of interesting questions on SO that cover areas like this. If I get a chance I'll link a few.