Is allocators' allocate and construct well-defined through [basic.life]p8?

89 views Asked by At

cppreference's example of std::allocator contains this code (shortened for simplicity):

// default allocator for ints
std::allocator<int> alloc1;

using traits_t1 = std::allocator_traits<decltype(alloc1)>; // The matching trait
p1 = traits_t1::allocate(alloc1, 1);
traits_t1::construct(alloc1, p1, 7);  // construct the int
std::cout << *p1 << '\n';

Rather straight-forward as far as allocators are concerned. However, what wording in the standard guarantess that p1 actually points to the new object?

According to cppreference documentation on std::allocate and [allocator.members], the default allocator's allocate() function

creates an array of type T[n] in the storage and starts its lifetime, but does not start lifetime of any of its elements.

and returns

[a pointer] to the first element of an array of n objects of type T whose elements have not been constructed yet.

Afaik, the array creation wording was added to the standard so that pointer-arithmetic on the pointer is valid. In any case, this means that the returned pointer points the first element of the T[] and the lifetime of this first element was not started.

construct() then creates an object at this location, however, it does not return a pointer to this object. The only pointer we have is still the one allocate returned.

Usually when an object is placed in the location of an expired object, it can "transparently replace" the old one under the conditions laid out in [basic.life]p8: (emphasis mine)

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object [...] will automatically refer to the new object and, once the lifetime of the new object has started [...]

The lifetime of this array element was never started so it cannot have ended, so this should not apply. How else can it then be guaranteed that accessing the newly constructed object is well-defined? Is std::launder supposed to be used after every construct call?

Please keep in mind that this is a [language-lawyer]-tagged question. It's not about whether this code practically works but about the "laws" of the standard.

1

There are 1 answers

0
Brian Bi On

I think we mostly agree that this code ought to work. std::launder is typically only needed when a const complete object is replaced by another object of the same type (ignoring top-level cv-qualifiers)1; [basic.life]/8.3. The rationale here is that if the compiler can see the code that creates the const complete object, it's allowed to assume that a pointer to that object or a reference to that object is a pointer or reference to an unchanging value, which provides useful optimization opportunities. You have to use std::launder to disable this optimization. This suggests that if there was no value in the first place (because this is the first time you are starting the lifetime of an object in a particular region of storage), then std::launder should not be needed (even if the object is const).

I think the OP is right that there's a wording gap in the standard. It might, unfortunately, be one that is not easily fixable at the present time. The problem is that we don't have a formal characterization of the identity of an object that is not within its lifetime. Perhaps the allocate call creates an uninitialized object with a particular identity, o1, and construct starts the lifetime of o1. If that's the case, then there is no problem: any pointer to o1 continues to be a pointer to o1 unless the pointer has its value changed by assigning to it. But that leads to an obvious follow-up question. If o1 is destroyed and a new object o2 has its lifetime begun in the same storage, we know that o1 and o2 are not the same object, but what can we say about the storage after o1's lifetime ended and before o2's lifetime began? We have a partial characterization of what can be done using a pointer or reference to that storage; [basic.life]/6–7. We do not have any explanation of when exactly the storage transitions from being occupied by "o1, whose lifetime has ended" to "o2, whose lifetime has not yet begun". Someone needs to do the work of figuring out an answer and working through all the implications of it. (And I suspect that there is no answer that has all the desired implications.)

Still, given that the object in OP's example is not even const, there is practically no chance that the eventual resolution to this ambiguity would make OP's code have undefined behaviour.

1 Note that such a const object must have dynamic storage duration; [basic.life]/10.