ARTICLE AD BOX
This is a purely theoretical language-lawyer question, as the situation it describes has no practical use (it would be really a bad design).
The scenario is creating a storage from an array of non-byte type and then placing contiguously in it objects of an unrelated type:
#include <cstddef> #include <memory> #include <new> // unrelated types using store_t = double; using data_t = short; // some data_t object generator template <typename T> data_t make_data(T val) { return data_t{static_cast<data_t>(val)}; } int main() { // creating a storage from store_t (not providing storage, not implicitly // creating objects) constexpr std::size_t N = 100; auto* Storage = new store_t[N]; // retrieving a suitably aligned pointer in storage to hold a data_t object std::size_t sz = sizeof(store_t) * N; void* ptr = static_cast<void*>(Storage); if (nullptr == std::align(alignof(data_t), sizeof(store_t) * N, ptr, sz)) { throw std::bad_alloc(); } // inplace construction of contiguous data_t object inside the original // storage // ends the array of store_t lifetime constexpr std::size_t n = 10; for (std::size_t i = 0; i < n; ++i) { std::construct_at(reinterpret_cast<data_t*>(ptr) + i, make_data(i)); } // is there a data_t array in its lifetime at ptr? ... }According to https://eel.is/c++draft/dcl.array#6:
An object of type “array of N U” consists of a contiguously allocated non-empty set of N subobjects of type U, known as the elements of the array, and numbered 0 to N-1. The element numbered 0 is termed the first element of the array.
Thus I theoretically constructed something that has the layout of an array object that could be used with pointer arithmetic (https://eel.is/c++draft/basic.compound#3, for instance):
data_t const* const array = std::launder( reinterpret_cast<data_t*>(ptr)); // is this a living array? for (std::size_t i = 0; i < n; ++i) { std::cout << array[i]; // is this legal? }But, as far as I understand, this array nether started its lifetime, either explicitly or implicitly.
operator new[] implicitly creates objects and arrays are implicit lifetime type, but I'm not sure we can rely on implicit object creation for two reasons. First, https://eel.is/c++draft/intro.object#14 and https://eel.is/c++draft/intro.object#16. If I understand these sections correctly, the address of the implicitly created object should be the one returned by operator new[] but due to alignment adjustment, especially for non-new-aligned type, it might not be the case. Besides, the data_t array is not nested within the store_t array because the latest does not provide storage. Thus we would have two unrelated objects sharing the same location which is UB (strict aliasing rule violation: https://eel.is/c++draft/basic.lval#11).
So, is the array of data_t in its lifetime and why?
This is the full example:
#include <cstddef> #include <iostream> #include <memory> #include <new> // unrelated types using store_t = double; using data_t = short; // some data_t object generator template <typename T> data_t make_data(T val) { return data_t{static_cast<data_t>(val)}; } int main() { // creating a storage from store_t (not providing storage, not implicitly // creating objects) constexpr std::size_t N = 100; auto* Storage = new store_t[N]; // retrieving a suitably aligned pointer in storage to hold a data_t object std::size_t sz = sizeof(store_t) * N; void* ptr = static_cast<void*>(Storage); if (nullptr == std::align(alignof(data_t), sizeof(store_t) * N, ptr, sz)) { throw std::bad_alloc(); } // inplace construction of contiguous data_t object inside the original // storage // ends the array of store_t lifetime constexpr std::size_t n = 10; for (std::size_t i = 0; i < n; ++i) { std::construct_at(reinterpret_cast<data_t*>(ptr) + i, make_data(i)); } // is there a data_t array in its lifetime at ptr? data_t const* const array = std::launder( reinterpret_cast<data_t*>(ptr)); // is this a living array? for (std::size_t i = 0; i < n; ++i) { std::cout << array[i]; // is this legal? } // cleanly releasing resources for (std::size_t i = 0; i < n; ++i) { std::destroy_at(std::launder(reinterpret_cast<data_t*>(ptr) + i)); } // releasing storage operator delete[](Storage); }