3

This is almost standard textbook use of placement new

template<size_t Len, size_t Align>
class aligned_memory
{
public:
    aligned_memory() : data((char*)(((std::uintptr_t)mem + Align - 1) & -Align)) {}
    char* get() const {return data;}
private:
    char mem[Len + Align - 1];
    char* data;
};

template<typename T, size_t N>
class Array
{
public:
    Array() : sz(0) {}
    void push_back(const T& t)
    {
        new (data.get() + sz++ * sizeof(T)) T(t);
    }
    void pop_back()
    {
        ((T*)data.get() + --sz)->~T();
    }

private:
    aligned_memory<N * sizeof(T), alignof(T)> data;
    size_t sz;
};

Seems pretty fine, until we look into strict-aliasing, there seems to be some conflict in whether this is well-formed

Camp ill-formed

Camp well-formed

They all agree on char* may always reference another object, but some point out its ill-formed to do so the other way round.

Clearly our char[] converts to char* then casted to T*, with which it is used to call its destructor.

So, does the above program break the strict-aliasing rule? Specifically, where in the standard does it says it is well-formed or ill-formed?

EDIT: as background info, this is written for C++0x, before the advent of alignas and std::launder. Not asking specifically for a C++0x solution, but it is preferred.

alignof is cheating, but its here for example purposes.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • Read [[basic.life](http://eel.is/c++draft/basic.life)] like a billion times by now, can't decide on answer – Passer By Jul 11 '17 at 17:29
  • Internally, the `std::vector` class typically implements its dynamic array as an array of bytes. – Some programmer dude Jul 11 '17 at 17:30
  • 2
    @Someprogrammerdude The standard library doesn't need to (and generally cannot) be implemented in 100% strictly conforming C++. –  Jul 11 '17 at 17:31
  • [Those answers](https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule/99010#99010) might help if you have not already read them. – Rakete1111 Jul 11 '17 at 17:31
  • @Rakete1111 I've read (and upvoted) that answer before, `aligned_memory` won't work with `union`s. I ran into this when answering a question, and it requires C++0x, without `alignas` – Passer By Jul 11 '17 at 17:35
  • @Rakete1111 And I think type-punning through unions is UB prior C++17, could be wrong though. – Passer By Jul 11 '17 at 17:36
  • I'm in "well-formed" camp, on the strength of this passage (emphasis mine): "**[basic.compound]/3** ... If an object of type `T` is located at an address `A`, a pointer of type *cv* `T*` whose value is the address `A` is said to *point* to that object, **regardless of how the value was obtained**. [ *Note:* For instance, the address one past the end of an array (5.7) would be considered to point to an unrelated object of the array’s element type that might be located at that address... — *end note* ]" – Igor Tandetnik Jul 11 '17 at 20:23
  • @IgorTandetnik Would that imply something like `istream is; is->~istream(); (new (&is) T)->~T(); new (&is) istream;` is well-formed? As in, any memory, as long as the lifetime of its occupying object(s) has ended or hasn't begun, can be used as storage for any other object? – Passer By Jul 12 '17 at 07:18
  • Funny thing, saw [this example](http://eel.is/c++draft/class.dtor#15) in the standard, I'd guess they wouldn't invoke UB in correct examples. From what I can observe, the difference is that the pointer is obtained from the placement new and not casted from the original pointer. – Passer By Jul 12 '17 at 07:37
  • @PasserBy Why do you think type-punning through `union`s becomes allowed in C++17? I've not run into any news about that. – underscore_d Jul 12 '17 at 09:13
  • @underscore_d I think it has something to do with accessing inactive objects going from unconditionally UB to something else, again, not sure about this – Passer By Jul 12 '17 at 09:16
  • @PasserBy I'd say it's legal, assuming storage originally occupied by `is` is sufficiently large and suitably aligned for `T`. **[basic.life]** talks about reusing storage all over the place. – Igor Tandetnik Jul 12 '17 at 12:04
  • @IgorTandetnik What's not apparent from __[basic.life]__ is the strict-aliasing part, nowhere did it mention issues with aliasing. See my answer for what I think is a full explanation. – Passer By Jul 12 '17 at 13:16

2 Answers2

2

Gathering from the hints throughout the countless helpful comments, here is my interpretation of what's happening.

TLDR its well-formed‡see edit


Quoting in the order I find more logical from [basic.life]

The properties ascribed to objects and references throughout this International Standard apply for a given object or reference only during its lifetime.


An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. [...] The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and

  • if the object has non-vacuous initialization, its initialization is complete.


The lifetime of an object o of type T ends when:

  • if T is a class type with a non-trivial destructor , the destructor call starts, or

  • the storage which the object occupies is released, or is reused by an object that is not nested within o

From [basic.lval]

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined

  • the dynamic type of the object,

  • a cv-qualified version of the dynamic type of the object,

  • a type similar to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,

  • an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),

  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,

  • a char, unsigned char, or std​::​byte type.

We deduce that

  1. The lifetime of the chars in the char[] ends when another object reuses that space.

  2. The lifetime of an object of type T started when push_back is called.

  3. Since the address ((T*)data.get() + --sz) is always that of an object with type T whose lifetime has started and not yet ended, it is valid to call ~T() with it.

  4. During this process, the char[] and char* in aligned_memory aliases objects of type T but it is legal to do so. Also, no glvalue is obtained from them, so they could have been pointers of any type.

To answer my own question in the comments whether using any memory as storage is also well-formed

U u;
u->~U();
new (&u) T;
((T*)&u)->~T();
new (&u) U;

Following the 4 points above, the answer is yes‡see edit, as long as the alignment of U is not weaker than T.

‡ EDIT: I've neglected another paragraph of [basic.life]

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, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and

  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and

  • the original object was a most derived object of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

Which means even though using the object is well-formed, the means which the object is obtained is not. Specifically, post C++17, std::launder has to be called

(std::launder((T*)data.get()) + --sz)->~T();

Prior C++17, a workaround would be to use the pointer acquired from the placement new instead

T* p = new (data.get() + sz++ * sizeof(T)) T(t);  // store p somewhere

† Quoted from n4659, as far as I can see, same holds for n1905

Community
  • 1
  • 1
Passer By
  • 19,325
  • 6
  • 49
  • 96
0

Placement-new creates an object at the specified location (C++14 expr.new/1), and ends the lifetime of any other object that was occupying the location (basic.life/1.4).

The code ((T*)data.get() + --sz)->~T(); accesses an object of type T at the location where there is an object of type T. This is fine. It is irrelevant if there used to be a char array at the location.

M.M
  • 138,810
  • 21
  • 208
  • 365