10

A trivial class is trivially copyable and has a trivial default constructor (a trivial type is either one of those or a built-in class that works similarly).

Since you can use memcpy to copy objects of trivial types, and since default-initializing trivial types doesn't change any of the bytes in the representation¹, does the following code (using C++20 concepts) correctly initialize a copy of the passed-in object?

#include <cstdlib>
#include <cstring>

#include <new>
#include <type_traits>

template <typename T>
T* copy_trivial(T orig) requires std::is_trivial_v<T>
{
    void* buf = std::aligned_alloc(alignof(T), sizeof(T));

    // Note the order of these statements
    std::memcpy(buf, &orig, sizeof(T));
    return new(buf) T;
}

(Compiler Explorer, with a couple instantiations provided)

It seems like this code would work, because there would be an initialized object with the correct object representation. However, given that nothing happens after initialization, would the object instead have an indeterminate value²?


¹ Not specified in a single location, but the linked procedure for default-initialization calls the constructor, which must be implicitly defined; the implicitly defined constructor default-initializes all the members and bases; and that recursion bottoms out at no initialization is performed for the built-in trivial types.

² The note in [expr.new]p18.1 says that it would, but notes are non-normative and it's plausible that this is only the case for non-placement new.

Daniel H
  • 7,223
  • 2
  • 26
  • 41
  • Note: This question was inspired by [this one](https://stackoverflow.com/q/51344271/27302), but I've simplified it a bit to avoid complications with `realloc`. – Daniel H Jul 15 '18 at 02:48
  • 1
    Interesting question, but I'm curious: what possible benefit is there in constructing code this way, instead of calling placement `new` first, followed by `memcpy`? – John Zwinck Jul 15 '18 at 03:08
  • @JohnZwinck The memory in the sample was obtained by `malloc`. If it were instead obtained by `realloc` (or potentially other functions intended for use in C code), then the copy would happen before there was a chance to do the placement `new`. – Daniel H Jul 15 '18 at 03:13

1 Answers1

8

does the following code (using C++20 concepts) correctly initialize a copy of the passed-in object?

No, it does not.

It does return a pointer to a valid T, but there is nothing in the standard which requires the value of that T to be a copy of the value of orig.

Default initialization of trivial types is said to perform "no initialization". But this is not the same as preserving the current storage of that memory: [dcl.init]/12

When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced.

Note that it says that it retains "an indeterminate value", not "the same value that was in that memory". Without having that explicit protection, the standard does not require the implementation to preserve the memory's contents.

Consider debugging builds. To catch errors, cases where "no initialization is performed" will sometimes fill uninitialized memory with specific bytes, so that you can detect when you're accessing uninitialized memory. That is only legal for an implementation if "an indeterminate value" does not preserve the current values in the memory.

There is no way in C++ to initialize an object with a byte-wise copy of another object. You can do memcpy's into arbitrary memory, but you cannot manifest an object in that memory which takes its value from that memory. And you can memcpy into an existing object, but that object will already have been initialized (unless no initialization was performed when it was being created).

So the best you can do is reverse the order of the two statements.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I would assume debugging builds fill the uninitialized memory "when storage [...] is obtained", not on every default initialization. It also says "*retain*" an indeterminate value; after the `memcpy`, I'm not sure there is an indeterminate value any more; again, that is after the storage is obtained. – Daniel H Jul 15 '18 at 03:26
  • @DanielH: Storage being obtained for an object happens when the object is being created. That is, as part of the `new` expression. Storage for the object is not being obtained merely by calling `malloc`. So while `malloc` could fill in those values, so could placement `new`. – Nicol Bolas Jul 15 '18 at 03:27
  • It looks like "obtain storage" isn't defined; I'd assumed it approximately meant "allocate", but your interpretation works too (especially given [\[expr.new\]p8](https://timsong-cpp.github.io/cppwp/n4659/expr.new#8), which might imply that it may obtain storage some other way). However, under that interpretation, placement `new` obtains storage from [the non-allocating `operator new` forms](https://timsong-cpp.github.io/cppwp/n4659/new.delete.placement), which are all specified as "Intentionally performs no other action.". Changing the bytes pointed to by `ptr` would be an "other action". – Daniel H Jul 15 '18 at 03:49
  • So apparently this is the common interpretation of this section in the one thread on `std-discuss` where it's actually being talked about and everybody accepted that placement `new` didn't work here once that was pointed out to you, so my interpretation is wrong and I'm accepting your answer. At least when searching there I learned about [p0593](http://wg21.link/p0593) and your own [`std::in_place_construct`](https://github.com/NicolBolas/Proposal-Ideas/blob/master/Trivial%20Operations.md) which should hopefully make this less of an issue. – Daniel H Jul 16 '18 at 07:05