18

I have found the intricacies of trivial types in C++ non-trivial to understand and hope someone can enlighten me on the following.

Given type T, storage for T allocated using ::operator new(std::size_t) or ::operator new[](std::size_t) or std::aligned_storage, and a void * p pointing to a location in that storage suitably aligned for T so that it may be constructed at p:

  1. If std::is_trivially_default_constructible<T>::value holds, is the code invoking undefined behavior when code skips initialization of T at p (i.e. by using T * tPtr = new (p) T();) before otherwise accessing *p as T? Can one just use T * tPtr = static_cast<T *>(p); instead without fear of undefined behavior in this case?
  2. If std::is_trivially_destructible<T>::value holds, does skipping destruction of T at *p (i.e by calling tPtr->~T();) cause undefined behavior?
  3. For any type U for which std::is_trivially_assignable<T, U>::value holds, is std::memcpy(&t, &u, sizeof(U)); equivalent to t = std::forward<U>(u); (for any t of type T and u of type U) or will it cause undefined behavior?
jotik
  • 17,044
  • 13
  • 58
  • 123
  • #2 will not free the memory (as far as I can comprehend your question) – Kupto Feb 17 '17 at 09:45
  • @Kupto Yes. As stated in the premises I'm managing the storage separately. Calling `delete` directly on `p` might not be a valid thing to do anyway... – jotik Feb 17 '17 at 09:58
  • Skipping destructors is never UB per se. The magic wording is that it's UB "if the program depends on the side effects of the destructor", words that even a real lawyer couldn't have said better. – Kerrek SB Feb 17 '17 at 10:04
  • 3
    `memcpy` is allowed for trivially copyable types, not trivially assignable ones. – Kerrek SB Feb 17 '17 at 10:04

2 Answers2

10
  1. No, you can't. There is no object of type T in that storage, and accessing the storage as if there was is undefined. See also T.C.'s answer here.

    Just to clarify on the wording in [basic.life]/1, which says that objects with vacuous initialization are alive from the storage allocation onward: that wording obviously refers to an object's initialization. There is no object whose initialization is vacuous when allocating raw storage with operator new or malloc, hence we cannot consider "it" alive, because "it" does not exist. In fact, only objects created by a definition with vacuous initialization can be accessed after storage has been allocated but before the vacuous initialization occurs (i.e. their definition is encountered).

  2. Omitting destructor calls never per se leads to undefined behavior. However, it's pointless to attempt any optimizations in this area in e.g. templates, since a trivial destructor is just optimized away.

  3. Right now, the requirement is being trivially copyable, and the types have to match. However, this may be too strict. Dos Reis's N3751 at least proposes distinct types to work as well, and I could imagine this rule being extended to trivial copy assignment across one type in the future.

    However, what you've specifically shown does not make a lot of sense (not least because you're asking for assignment to a scalar xvalue, which is ill-formed), since trivial assignment can hold between types whose assignment is not actually "trivial", that is, has the same semantics as memcpy. E.g. is_trivially_assignable<int&, double> does not imply that one can be "assigned" to the other by copying the object representation.

Community
  • 1
  • 1
Columbo
  • 60,038
  • 8
  • 155
  • 203
  • If your claim 1 would be true, you just broke thousands of lines of legacy C code. Like it or not, the intent of C++ is to keep `*(int*)malloc(sizeof int) = 42;` legal. That's why the lifetime of objects of trivial type starts with the allocation of sufficient space, which `malloc` does. – MSalters Feb 17 '17 at 10:59
  • @MSalters Sorry, that's just plain nonsense. See http://stackoverflow.com/a/36024383/3647361 Your note on objects with vacuous initialization (not being of trivial type...) applies to objects that have a definition within the program. – Columbo Feb 17 '17 at 11:04
  • I don't see in the intro of 3.8 any restriction similar to "only objects that have a definition". The only restriction is the type of the object. In particular `int` is _not_ a class type, has _vacuous initialization, and thus the part about "if non-vacuous initialization, its initialization is complete" doesn't apply. – MSalters Feb 17 '17 at 11:16
  • @MSalters That's irrelevant. As elucidated [here](http://stackoverflow.com/a/40874245/3647361), there is no object to begin with, so your argument makes no sense. – Columbo Feb 17 '17 at 11:17
  • 2
    @Msalters Like it or not, but the current C++ standard makes `*(int*)malloc(sizeof(int)) = 42` illegal. This seems like a horrible idea to me, but me not liking what the standard says doesn't change what the standard says. It isn't thousands of lines -- it is many **millions** of lines of code that are broken due to these rules. In tiny new projects without a huge C legacy, they could be justified -- but it is simply ridiculous that people defend that as a reasonable position for the standard to take. – Yakk - Adam Nevraumont Feb 17 '17 at 15:38
  • @Columbo Millions of lines of code that where originally C and are now compilied as C++. That worked for decades, because no compiler enforced the complex rules, and now that the standard is clearer, people claim should never have worked. Improving the wording of a flaw in the standard brings the flaw to light, it doesn't justify the flaw. – Yakk - Adam Nevraumont Feb 17 '17 at 15:40
  • @Yakk Perhaps C code should be compiled with a C compiler and linked against via linkage specifications. The C and C++ object models are incompatible, and that's not a flaw. – Columbo Feb 17 '17 at 15:45
  • @Columbo You are 30 years too late for that suggestion? Billions of lines of C code have been recompiled as C++. Millions of lines of this code uses malloc'd objects without calling new and are still in use. The fairy tale land you describe has nothing to do with the real world; if C++ intends not to break existing functioning C++ code en mass, these clauses of the standard cannot stand. "You ought to" you state just means "all your code should be thrown out or rewritten", because the horse is out of the barn, saying the door should have been closed is not helpful. – Yakk - Adam Nevraumont Feb 17 '17 at 16:00
  • @Columbo Thank you for your answer! But could you please clarify on 1. in your answer whether using placement new (`T * tPtr = new (p) T();`) is still a valid way to access the storage as `T`? I find that your choice of words in the first sentence of your answer might imply that using placement new and then accessing the storage as `T` might lead to undefined behavior as well? – jotik Feb 17 '17 at 21:10
  • @jotik No, precisely not. Placement new creates an object for you to work with. Raw storage allocation does not per se. – Columbo Feb 17 '17 at 23:19
  • What if trivially copyable structure transmitted to me as part of TCP/IP packet? That structure is just copyed (but not using `std::memcpy`). Can I assume, that the mentioned chunk of memory contains already constructed the structure? Is it still legal to `reinterpret_cast` that part of packet to type of structure? – Tomilov Anatoliy Feb 18 '17 at 05:17
  • @Orient No, it's not legal. What you need to do is `memcpy` those raw bytes into an object of that type. – Columbo Feb 18 '17 at 10:05
  • @Columbo What if I previously call in-place `operator new` for the pointer to corresponding place into the packet storage? – Tomilov Anatoliy Feb 18 '17 at 14:50
  • @Orient I don't see why that wouldn't work, but I'm not certain if the implementation regards the uninitialized members as having indeterminate values or not. Perhaps you could open an std-discussion thread. – Columbo Feb 19 '17 at 12:51
  • @Columbo I know at least one implementation that treats such a placement new as clobbering the storage. – T.C. Feb 20 '17 at 17:41
  • @T.C. ..which makes no difference as to whether or not it should, or whether the wording('s intent) agrees. (I'm trying to say, this still needs clarification.) – Columbo Feb 20 '17 at 17:45
  • @Columbo Well, in my reading [dcl.init]/12 clearly indicates that the object has an indeterminate value. – T.C. Feb 20 '17 at 17:51
  • @T.C. We're talking about overwriting storage we provide ourselves, not one that has been allocated. – Columbo Feb 20 '17 at 17:54
  • @Columbo Sure, but I don't see anything in that paragraph that draws a distinction based on how the allocation function obtains the storage. – T.C. Feb 20 '17 at 17:58
  • @T.C. We're not calling an allocation function. – Columbo Feb 20 '17 at 17:58
  • The reserved placement allocation function is still an allocation function. – T.C. Feb 20 '17 at 17:59
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/136180/discussion-between-columbo-and-t-c). – Columbo Feb 20 '17 at 18:06
  • @jotik Oh, btw., turns out your approach would not be valid, so you do have to perform a `memcpy`. – Columbo Feb 20 '17 at 20:04
  • @Columbo You mean using placement-new would not be valid? Or something else? – jotik Feb 20 '17 at 20:11
  • @jotik The former. – Columbo Feb 20 '17 at 20:12
  • @Columbo To be more precise, you mean that for some `T someT;` using `memcpy(p, &someT, sizeof(T)); T * tPtr = new (p) T;` might not yield `someT == *tPtr`, whereas `T * tPtr = new (p) T; memcpy(p, &someT, sizeof(T));` will? – jotik Feb 20 '17 at 20:39
  • @jotik Correct. Note that this is not generalisable; http://eel.is/c++draft/basic.life#8 – Columbo Feb 20 '17 at 20:43
5
  1. Technically reinterpreting storage is not enough to introduce a new object as. Look at the note for Trivial default constructor states:

A trivial default constructor is a constructor that performs no action. All data types compatible with the C language (POD types) are trivially default-constructible. 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.

But the note says it's a formal limitation, so probably it is safe in many cases. Not guaranteed though.

  1. No. is_assignable does not even guarantee the assignment will be legal under certain conditions:

This trait does not check anything outside the immediate context of the assignment expression: if the use of T or U would trigger template specializations, generation of implicitly-defined special member functions etc, and those have errors, the actual assignment may not compile even if std::is_assignable::value compiles and evaluates to true.

What you describe looks more like is_trivially_copyable, which says:

Objects of trivially-copyable types are the only C++ objects that may be safely copied with std::memcpy or serialized to/from binary files with std::ofstream::write()/std::ifstream::read().

  1. I don't really know. I would trust KerrekSB's comments.
luk32
  • 15,812
  • 38
  • 62
  • 1
    "probably safe in many cases" does not warrant employing undefined constructs in your code. – Columbo Feb 17 '17 at 10:13
  • I agree. I have explicitly said it is not guaranteed. I am not hiding anything, and I am honest. Some people are fine playnig with fire. I have seen people calling `p->member()` when `p` was `null_ptr` saying it's fine when `member` didn't touch anything related to `this`... – luk32 Feb 17 '17 at 10:20
  • The committee would've agreed some time ago: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#315 – Columbo Feb 17 '17 at 10:22
  • Also FWIW, I have noticed, that what violations are warranted and what are not, is usually handled by managers. I like legal code. So by heart I do agree with you. – luk32 Feb 17 '17 at 10:23