59

cppreference states that:

Objects with trivial default constructors can be created by using reinterpret_cast on any suitably aligned storage, e.g. on memory allocated with std::malloc.

This implies that the following is well-defined code:

struct X { int x; };
alignas(X) char buffer[sizeof(X)];    // (A)
reinterpret_cast<X*>(buffer)->x = 42; // (B)

Three questions follow:

  1. Is that quote correct?
  2. If yes, at what point does the lifetime of the X begin? If on line (B), is it the cast itself that is considered acquiring storage? If on line (A), what if there were a branch between (A) and (B) that would conditionally construct an X or some other pod, Y?
  3. Does anything change between C++11 and C++1z in this regard?

Note that this is an old link. The wording was changed in response to this question. It now reads:

Unlike in C, however, objects with trivial default constructors cannot be created by simply reinterpreting suitably aligned storage, such as memory allocated with std::malloc: placement-new is required to formally introduce a new object and avoid potential undefined behavior.

11
  • I actually tried to figure out the question of when lifetime begins of those objects. I was not able to find a definitive answer in standard, and I believe, it is vague in this regard. As for first question, I doubt the quote is correct, since there is an aliasing rule to pay attention to. Commented Nov 29, 2016 at 18:53
  • @SergeyA as long as the buffer is a char buffer, strict aliasing is not an issue. Commented Nov 29, 2016 at 19:01
  • 3
    No, and I thought we went over this multiple times already? [intro.object]/1 exhaustively enumerates which language constructs can create objects. Commented Nov 29, 2016 at 19:13
  • 4
    @RichardHodges, nope. char* can alias anything, but anything can't alias char* Commented Nov 29, 2016 at 19:16
  • 4
    @RichardHodges Actually you can use a (recursive) union, and must use one if you want constexpr. Commented Nov 29, 2016 at 19:30

3 Answers 3

39

There is no X object, living or otherwise, so pretending that there is one results in undefined behavior.

[intro.object]/1 spells out exhaustively when objects are created:

An object is created by a definition ([basic.def]), by a new-expression ([expr.new]), when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).

With the adoption of P0137R1, this paragraph is the definition of the term "object".

Is there a definition of an X object? No. Is there a new-expression? No. Is there a union? No. Is there a language construct in your code that creates a temporary X object? No.

Whatever [basic.life] says about the lifetime of an object with vacuous initialization is irrelevant. For that to apply, you have to have an object in the first place. You don't.

C++11 has roughly the same paragraph, but doesn't use it as the definition of "object". Nonetheless, the interpretation is the same. The alternative interpretation - treating [basic.life] as creating an object as soon as suitable storage is obtained - means that you are creating Schrödinger's objects*, which contradicts N3337 [intro.object]/6:

Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.


* Storage with the proper alignment and size for a type T is by definition storage with the proper alignment and size for every other type whose size and alignment requirements are equal to or less than those of T. Thus, that interpretation means that obtaining the storage simultaneously creates an infinite set of objects with different types in said storage, all having the same address.

Sign up to request clarification or add additional context in comments.

29 Comments

@RichardHodges First, footnotes are non-normative. Second, that footnote pertains to the definition of "safely-derived pointer", which is completely unrelated - that concept is there for GC support. Third, it is fairly well-established that malloc alone is not sufficient to create an object under the current wording - P0137 explicitly refers to that as the status quo.
@T.C. While I understand that, the inability to make using auto p_int = (int*)malloc(sizeof(int)); defined behavior seems like a really bad idea. I get that making it defined behavior is hard, but the alternative is horrible. Reems and reems of legacy code gone from "in practice working" to "anathema". If the standard did not permit that, the standard was wrong; the way to fix it is to fix the standard, not make the standard's error more explicit.
@RichardHodges Airlifting a footnote out of context doesn't help your case. That footnote is attached to [basic.stc.dynamic.safety]/2.1, and by happenstance [basic.life] started on the same page in that particular version of the working draft. "safely-derived pointer" is only relevant on implementations with strict pointer safety (aka GC'd implementations), which is an empty set AFAIK. It sheds absolutely zero light on the meaning of [basic.life], because it is dealing with a completely different subject.
@T.C. I'll be explicit: auto p_int = (int*)malloc(sizeof(int)); *p_int = 0; std::cout << *p_int << "\n"; -- anything that doesn't make that standards compliant should be a non-starter. That is legacy C-style memory handling, and it exists in massive legacy code bases that have compiled and worked in C++ for 30+ years. If the C++ standard says "that isn't defined", it is a flaw in the standard. I get why it is hard, but leaving it ambiguous or poorly worded is better than explicitly stating that is undefined.
@Yakk IMO having the standard be clear is better than having it be ambiguous or poorly worded. Then discussion can at least move onto fixing it instead of having endless threads like this where people apply their own interpretation and we argue about whose interpretation is [better | was the intent | etc.]
|
8

Based on p0593r6 I believe the code in the OP is valid and should be well defined. The new wording, based on the DR retroactively applied to all versions from C++98 inclusive, allows implicitly object creation as long as the created object is well defined (tautology is sometimes the rescue for complicated definitions), see § 6.7.2.11 Object model [intro.object]):

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 [...]

See also: https://stackoverflow.com/a/61999151/2085626

Comments

4

This analysis is based on n4567, and uses section numbers from it.

§5.2.10/7: When a prvalue v of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_cast<cv T*>(static_cast<cv void*>(v)).

So, in this case, the reinterpret_cast<X*>(buffer) is the same as static_cast<X *>(static_cast<void *>(buffer)). That leads us to look at the relevant parts about static_cast:

§5.2.9/13: A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. The null pointer value is converted to the null pointer value of the destination type. If the original pointer value represents the address A of a byte in memory and A satisfies the alignment requirement of T, then the resulting pointer value represents the same address as the original pointer value, that is, A.

I believe that's enough to say that the original quote is sort of correct--this conversion gives defined results.

As to lifetime, it depends on what lifetime you're talking about. The cast creates a new object of pointer type--a temporary, which has a lifetime starting from the line where the cast is located, and ending whenever it goes out of scope. If you have two different conversions that happen conditionally, each pointer has a lifetime that starts from the location of the cast that created it.

Neither of these affects the lifetime of the object providing the underlying storage, which is still buffer, and has exactly the same lifetime, regardless of whether you create a pointer (of the same or converted type) to that storage or not.

5 Comments

What's the conclusion though? Is your claim that the the pointer to X is created legally, but that it can't actually be dereferenced (e.g., the ->x is UB) because they don't point to a created X object? It isn't clear to be the relevance of the lifetime of the pointers themselves and it's hard to understand on what side of the debate this answer comes down on.
Yes, creating the pointer has defined behavior, but dereferencing the pointer gives UB. I considered his question about lifetime somewhat ambiguous, so I pointed out the lifetime of every object in the code, even though I agree that the lifetime of the pointers themselves probably isn't what he cared about. He asked about the lifetime of the X, and there is no actual X involved, just a pointer to X initialized with the address of a buffer of chars.
Right, but at the end, the code dereferences the pointer as if there was an X - if that isn't going to work (the crux of the question, really), maybe point it out?
@BeeOnRope: I'm hesitant to say that. The reality is that it's officially undefined behavior, but it will work (for almost any reasonable definition of the word) on every known implementation, and I'd expect it to continue working essentially permanently. The simple fact is that breaking this breaks essentially all C compatibility, and I doubt there's even one compiler vendor that's willing to throw that away.
Fair enough - it's that exact "problem" that caused me to come here, since I find it hard to believe (for example) that memcpying a trivially copyable type into suitable aligned uninitialized storage isn't allowed by the standard, but that seems to be the place we're in today :(.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.