15

Does the C++ standard guarantee that uninitialized POD members retain their previous value after a placement new?

Or more precisely, will the following assert always be satisfied according to C++11?

#include <cstdlib>
#include <cassert>

struct Foo {
    int alpha; // NOTE: Uninitialized
    int beta = 0;
};

int main()
{
    void* p = std::malloc(sizeof (Foo));
    int i = some_random_integer();
    static_cast<Foo*>(p)->alpha = i;
    new (p) Foo;
    assert(static_cast<Foo*>(p)->alpha == i);
}

Is the answer the same for C++03?

Kristian Spangsege
  • 2,903
  • 1
  • 20
  • 43

2 Answers2

20

Does the C++ standard guarantee that uninitialized POD members retain their previous value after a placement new?

Will the following assert always be satisfied according to C++11?

No.

Uninitialized data members have an indeterminate value, and this is not at all the same as saying that the underlying memory is left alone.

[C++11: 5.3.4/15]: A new-expression that creates an object of type T initializes that object as follows:

  • If the new-initializer is omitted, the object is default-initialized (8.5); if no initialization is performed, the object has indeterminate value.
  • Otherwise, the new-initializer is interpreted according to the initialization rules of 8.5 for direct-initialization.

[C++11: 8.5/6]: To default-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is an array type, each element is default-initialized;
  • otherwise, no initialization is performed.

[C++11: 12.1/6]: A default constructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used (3.2) to create an object of its class type (1.8) or when it is explicitly defaulted after its first declaration. The implicitly-defined default constructor performs the set of initializations of the class that would be performed by a user-written default constructor for that class with no ctor-initializer (12.6.2) and an empty compound-statement.

[C++11: 12.6.2/8]: In a non-delegating constructor, if a given non-static data member or base class is not designated by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer) and the entity is not a virtual base class of an abstract class (10.4), then

  • if the entity is a non-static data member that has a brace-or-equal-initializer, the entity is initialized as specified in 8.5;
  • otherwise, if the entity is a variant member (9.5), no initialization is performed;
  • otherwise, the entity is default-initialized (8.5).

(NB. the first option in 12.6.2/8 is how your member beta is handled)

[C++11: 8.5/6]: To default-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is an array type, each element is default-initialized;
  • otherwise, no initialization is performed.

[C++11: 8.5/11]: If no initializer is specified for an object, the object is default-initialized; if no initialization is performed, an object with automatic or dynamic storage duration has indeterminate value.

A compiler could choose to zero-out (or otherwise alter) the underlying memory during allocation. For example, Visual Studio in debug mode is known to write recognisable values such as 0xDEADBEEF into memory to aid debugging; in this case, you're likely to see 0xCDCDCDCD which they use to mean "clean memory" (reference).

Will it, in this case? I don't know. I don't think that we can know.

What we do know is that C++ doesn't prohibit it, and I believe that brings us to the conclusion of this answer. :)


Is the answer the same for C++03?

Yes, though through slightly different logic:

[C++03: 5.3.4/15]: A new-expression that creates an object of type T initializes that object as follows:

  • If the new-initializer is omitted:
    • If T is a (possibly cv-qualified) non-POD class type (or array thereof), the object is default-initialized (8.5). If T is a const-qualified type, the underlying class type shall have a user-declared default constructor.
    • Otherwise, the object created has indeterminate value. If T is a const-qualified type, or a (possibly cv-qualified) POD class type (or array thereof) containing (directly or indirectly) a member of const-qualified type, the program is ill-formed;
  • If the new-initializer is of the form (), the item is value-initialized (8.5);
  • If the new-initializer is of the form (expression-list) and T is a class type, the appropriate constructor is called, using expression-list as the arguments (8.5);
  • If the new-initializer is of the form (expression-list) and T is an arithmetic, enumeration, pointer, or pointer-to-member type and expression-list comprises exactly one expression, then the object is initialized to the (possibly converted) value of the expression (8.5);
  • Otherwise the new-expression is ill-formed.

Now, all that was my strict interpretation of the rules of initialisation.

Speaking practically, I think you're probably correct in seeing a potential conflict with the definition of placement operator new syntax:

[C++11: 18.6.1/3]: Remarks: Intentionally performs no other action.

An example that follows explains that placement new "can be useful for constructing an object at a known address".

However, it doesn't actually talk about the common use of constructing an object at a known address without mungling the values that were already there, but the phrase "performs no other action" does suggest that the intention is that your "indeterminate value" be whatever was in memory previously.

Alternatively, it may simply prohibit the operator itself from taking any action, leaving the allocator free to. It does seem to me that the important point the standard trying to make is that no new memory is allocated.

Regardless, accessing this data invokes undefined behaviour:

[C++11: 4.1/1]: A glvalue (3.10) of a non-function, non-array type T can be converted to a prvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If the object to which the glvalue refers is not an object of type T and is not an object of a type derived from T, or if the object is uninitialized, a program that necessitates this conversion has undefined behavior. If T is a non-class type, the type of the prvalue is the cv-unqualified version of T. Otherwise, the type of the prvalue is T.

So it doesn't really matter: you couldn't compliantly observe the original value anyway.

Community
  • 1
  • 1
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • @LightnessRacesinOrbit: Could you try out my example in Visual Studio in debug mode. It could easily be that the filling in of recognizable values is performed only by the actual allocator, which does not run as part of placement new. I'm assuming you are using MSVC++. I'm not. – Kristian Spangsege Feb 02 '13 at 09:05
  • @KristianSpangsege: I don't have access to Visual Studio. – Lightness Races in Orbit Feb 02 '13 at 09:12
  • 1
    `operator new` may intentionally do nothing, but then there's still the rest of the new expression to happen (i.e., the business at 5.3.4 that makes it indeterminate). – R. Martinho Fernandes Feb 02 '13 at 09:22
  • @R.MartinhoFernandes: The way I interpret it (for now) is that 'indeterminate' is used here to make it clear that allocated memory (on stack or dynamic) is not guaranteed to be zero-initialized. – Kristian Spangsege Feb 02 '13 at 09:29
  • It doesn't make any of that clear. (If it did, this question would not be here). I like to assume that if the intention was to make that clear, the standard would make it clear with some text like "the storage is not guaranteed to be zero-initialized". – R. Martinho Fernandes Feb 02 '13 at 09:33
  • @KristianSpangsege I've checked the placement new in VS2012 and the results are as follows. *Release*: (After allocation) Alpha: garbage, Beta: garbage; (After new) Alpha: **unchanged value**, Beta: 0. *Debug*: (After allocation) Alpha: 0xCDCDCDCD, Beta: 0xCDCDCDCD; (After new) Alpha: **unchanged value**, Beta: 0. 0xCD... is obviously the debug mode mapping of allocated memory. While not requested by standard VS2012 doesn't clear/overwrite the allocated memory for uninitialized members. – Red XIII Feb 02 '13 at 13:16
  • I think you have the operator and the allocator backwards. The allocation function is prohibited from doing anything to the memory, the operator can do whatever it wants as long as the allocation function and constructor get called. – Ben Voigt Dec 22 '14 at 02:55
  • The fact that operator new "performs no other action" is unfortunately not sufficient. Please see this document to find out why: https://gcc.gnu.org/gcc-6/porting_to.html#flifetime-dse In short, even if the underlying memory won't change, the previous stores to it can be removed, so it still won't contain the expected value. – stsp Feb 07 '19 at 23:07
  • @stsp: Yep, like I said, the values are unspecified and cannot be observed anyway. Thanks for a good practical example of how that can be "problematic" if you wanted to ignore this prohibition! – Lightness Races in Orbit Feb 07 '19 at 23:09
  • Yes, practical example and a practical command-line option to avoid this problem (but no such option in clang!). I've seen you made a valid conclusion by other means anyway. – stsp Feb 07 '19 at 23:14
0

C++11 12.6.2/8 "Initializing bases and members" says:

In a non-delegating constructor, if a given non-static data member or base class is not designated by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer) and the entity is not a virtual base class of an abstract class (10.4), then

  • if the entity is a non-static data member that has a brace-or-equal-initializer, the entity is initialized as specified in 8.5;
  • otherwise, if the entity is a variant member (9.5), no initialization is performed;
  • otherwise, the entity is default-initialized (8.5).

Default initialization on an int does nothing (8.5/6 "Initializers"):

To default-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is an array type, each element is default-initialized;
  • otherwise, no initialization is performed.

So the member alpha should be left alone.

Michael Burr
  • 333,147
  • 50
  • 533
  • 760
  • I'm not sure that "no initialization is performed" can be inferred to mean "the bytes will have the same physical value that they did before this object existed". It means "the bytes will have an indeterminate value". – Lightness Races in Orbit Feb 02 '13 at 08:02
  • Sorry - my mistake. I was quoting the C++11 standard, not the C standard. – Michael Burr Feb 02 '13 at 08:03
  • I agree with Lightness. If no initialisation is performed on an object, you are left with an uninitialised object. As we all know, accessing an uninitialised object does not have to produce well-defined results. – R. Martinho Fernandes Feb 02 '13 at 08:05
  • 1
    I think that interpreting a plainly stated "**no** initialization" as "unspecified initialization" or some such is a stretch. Keep in mind that the standard specfically notes that "Such use of explicit placement and destruction of objects can be necessary to cope with dedicated hardware resources and for writing memory management facilities". I think that handling hardware resources would be one area where the "no initialization" promise might be meaningful and important. – Michael Burr Feb 02 '13 at 08:48
  • @MichaelBurr: But if you keep going down the rabbit hole, you find wording that gives uninitialised objects _indeterminate values_. "No initialisation" means that the standard's idea of initialising things is not applied, and the result is an indeterminate value i.e. the implementation can do whatever it likes instead. See my answer for the necessary trail of quotes. – Lightness Races in Orbit Feb 02 '13 at 08:52
  • I think there's an issue with the wording in the standard here, like Michael says the consequence is you can't necessarily use placement new to create an object over the top of some significant hardware location, since the implementation is permitted to scribble on it, which might have arbitrary implementation-defined (or implementation-undefined) effect depending on the value written. But I think Lightness's interpretation of the wording is correct. C++11 defines exactly what "no initialization is performed" means, albeit the meaning is counter-intuitive. – Steve Jessop Feb 02 '13 at 08:59
  • 1
    @LightnessRacesinOrbit: I'm not sure I buy your arguments. I've been studying the standard too. I'll try to put a detailed argument together later, but for now, check out section 18.6.1.3. The placement versions are explicitely stated as doing nothing and they cannot be overridden by an application. Couple that with the fact that initialization is explicitely skipped for uninitialized POD members. – Kristian Spangsege Feb 02 '13 at 09:00
  • @SteveJessop such an implementation is also permitted to guarantee not scribbling it. (i.e. why would anyone write a broken implementation for that machine?) – R. Martinho Fernandes Feb 02 '13 at 09:00
  • @R.MartinhoFernandes: agreed, but if you can point to the section in the GCC manual that bothers to say this then I'll be much surprised ;-) – Steve Jessop Feb 02 '13 at 09:01
  • @KristianSpangsege What you need to be looking is the part that requires the storage to remain the same. – R. Martinho Fernandes Feb 02 '13 at 09:02
  • @SteveJessop: I see your point in practice, though speaking theoretically (of a kind) I've never seen any indication _in the standard_ that it cares or wants to care about memory locations that are shared by some hardware. Certainly not enough to warrant a claim that the basic rules of initialisation are cancelled out by contradiction. C++ doesn't care about your hardware; you'd need _implementation guarantees_ (like GCC saying "I won't do what VS does") in order to implement such _implementation-defined_ behaviour. – Lightness Races in Orbit Feb 02 '13 at 09:02
  • @KristianSpangsege: The "intentionally performs no other action" bit _is_ somewhat worrying. – Lightness Races in Orbit Feb 02 '13 at 09:04
  • @LightnessRacesinOrbit: obviously it doesn't mention hardware, but it does say that volatile writes are observable behaviour. Permitting placement new to scribble is permitting unspecified observable behavior if you use it to construct a volatile object (or one with volatile non-static data members). In practice, the way you'd observe it is for example some LED lights up that you weren't expecting. – Steve Jessop Feb 02 '13 at 09:04
  • @SteveJessop: In practice, if my software controls LEDs (and it does) then it takes control and retains control, setting all the LEDs to be the proper value. ;) – Lightness Races in Orbit Feb 02 '13 at 09:05
  • @LightnessRacesinOrbit: sure, but I can easily imagine some system with low-level bootstrap code in C or asm that initializes the LEDs to a clean state, but then you want to use C++ to manage them once the "driver" gets going. It would be nice, but obviously not essential, to be able to do this just by constructing a POD class with one `int` data member over the top of your nice clean `int` hardware location. So I think it's a shame if the standard doesn't mandate that POD non-initialization *of memory that already has a defined state* doesn't leave it alone. – Steve Jessop Feb 02 '13 at 09:07
  • More importantly, @KristianSpangsege 18.6.1.3 is irrelevant. `new` is *not* `operator new`. – R. Martinho Fernandes Feb 02 '13 at 09:08
  • @LightnessRacesinOrbit: if the standard says what Michael wants it to say, then the value would not be indeterminate. I'm with you, I don't think it does say that, but I'm kind of hoping to be wrong ;-) – Steve Jessop Feb 02 '13 at 09:11
  • @R.MartinhoFernandes: Section 18.6.1.3 is definitely relevant. The global new operator is called as part of the placement new construct. – Kristian Spangsege Feb 02 '13 at 09:11
  • @KristianSpangsege *and other things happen*, like what is described on 5.3.4/15. – R. Martinho Fernandes Feb 02 '13 at 09:11
  • @Steve: Whatever 18.6.1.3 says and no matter how relevant it is, no initialisation is performed, and accessing the data member invokes UB. – Lightness Races in Orbit Feb 02 '13 at 09:14
  • For a non-volatile-related example of this being annoying, suppose you use `calloc` or `new char()[]` to get yourself some lovely zeros. Then to me it would make a lot of sense to allow placement new of a POD struct containing only `int` data members, into part of that memory, *without* then having to zero them again afterwards. These kinds of use for placement new are why I think the standard is weak here, it (seems to me to) rule them out for no positive reason that I can think of in terms of useful implementation freedom. – Steve Jessop Feb 02 '13 at 09:15
  • Actually, I take that last thing back. Suppose a POD member of size 2 shares a word with a non-POD member of size 2. Then the implementation can *usefully* exploit the counter-intuitive meaning of "no initialization is performed" to default-initialize the class with a single word write, giving the non-POD member its proper value and trashing the POD member. Useful because a word write might be cheaper than a half-word write that preserves the POD half of the word. So there is plausible motivation for the unexpected meaning of "no initialization is performed" for POD members. – Steve Jessop Feb 02 '13 at 09:21
  • @SteveJessop: I think it is reasonable to assume that the standard intends to only allow such optimizations when all members are explicitly initialized or when a non-placement new is used. – Kristian Spangsege Feb 02 '13 at 09:34
  • @KristianSpangsege: I don't think it's reasonable to assume that. The standard is very pedantic, and compiler-writers are very pedantic. So users of C++ need to be very pedantic, if the standard fails to say something then you can complain (like I have) that you'd prefer it to say it, but you can't just go ahead and act as though it really does say it. – Steve Jessop Feb 02 '13 at 09:37
  • @SteveJessop: Agreed. I should have used 'plausible' instead of 'reasonable to assume'. – Kristian Spangsege Feb 02 '13 at 09:39
  • 1
    @KristianSpangsege: Yes, if the standard intended to say it but failed to actually get it into writing then there's a case for a defect report and a correction (provided it won't break existing implementations worse than the committee is willing). I'm still half-hoping there's a clause somewhere that hasn't been raised yet in this discussion, that saves the day ;-) – Steve Jessop Feb 02 '13 at 09:40