17

In C++, is this code correct?

#include <cstdlib>
#include <cstring>

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    std::memcpy(buf, &a, sizeof a);
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

In other words, is *b an object whose lifetime has begun? (If so, when did it begin exactly?)

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
M.M
  • 138,810
  • 21
  • 208
  • 365
  • Related: http://stackoverflow.com/questions/26171827/is-memcpy-of-a-trivially-copyable-type-construction-or-assignment – M.M May 08 '15 at 01:36
  • The only potential issue I can think of is [strict aliasing](http://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule). Which you could correct by changing the type of `buf` in which case I would say that both `b` and `buff` are one and the same, ergo having the same lifetime. – nonsensickle May 08 '15 at 01:44
  • 1
    @nonsensickle I don't think strict aliasing applies here: if `*b` is an object of type `T` then there's no aliasing violation to use it as such; and if it isn't then it is UB because `b->y` tries to read a non-existent object . Certainly it would make no difference to change the type of `buf`; casting a pointer does not change the dynamic type of the object it points to – M.M May 08 '15 at 01:54
  • Yeah, I think you're right. So long as you are not using `buf` as an *lvalue* it shouldn't violate the strict aliasing rule. I withdraw my argument but I'll leave the comment if you don't mind. – nonsensickle May 08 '15 at 02:13

4 Answers4

18

This is unspecified which is supported by N3751: Object Lifetime, Low-level Programming, and memcpy which says amongst other things:

The C++ standards is currently silent on whether the use of memcpy to copy object representation bytes is conceptually an assignment or an object construction. The difference does matter for semantics-based program analysis and transformation tools, as well as optimizers, tracking object lifetime. This paper suggests that

  1. uses of memcpy to copy the bytes of two distinct objects of two different trivial copyable tables (but otherwise of the same size) be allowed

  2. such uses are recognized as initialization, or more generally as (conceptually) object construction.

Recognition as object construction will support binary IO, while still permitting lifetime-based analyses and optimizers.

I can not find any meeting minutes that has this paper discussed, so it seems like it is still an open issue.

The C++14 draft standard currently says in 1.8 [intro.object]:

[...]An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed.[...]

which we don't have with the malloc and the cases covered in the standard for copying trivial copyable types seem to only refer to already existing objects in section 3.9 [basic.types]:

For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char.42 If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value[...]

and:

For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied into obj2,43 obj2 shall subsequently hold the same value as obj1.[...]

which is basically what the proposal says, so that should not be surprising.

dyp points out a fascinating discussion on this topic from the ub mailing list: [ub] Type punning to avoid copying.

Propoal p0593: Implicit creation of objects for low-level object manipulation

The proposal p0593 attempts to solve this issues but AFAIK has not been reviewed yet.

This paper proposes that objects of sufficiently trivial types be created on-demand as necessary within newly-allocated storage to give programs defined behavior.

It has some motivating examples which are similar in nature including a current std::vector implementation which currently has undefined behavior.

It proposes the following ways to implicitly create an object:

We propose that at minimum the following operations be specified as implicitly creating objects:

  • Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array.

  • A call to malloc, calloc, realloc, or any function named operator new or operator new[] implicitly creates objects in its returned storage.

  • std::allocator::allocate likewise implicitly creates objects in its returned storage; the allocator requirements should require other allocator implementations to do the same.

  • A call to memmove behaves as if it

    • copies the source storage to a temporary area

    • implicitly creates objects in the destination storage, and then

    • copies the temporary storage to the destination storage.

    This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.

  • A call to memcpy behaves the same as a call to memmove except that it introduces an overlap restriction between the source and destination.

  • A class member access that nominates a union member triggers implicit object creation within the storage occupied by the union member. Note that this is not an entirely new rule: this permission already existed in [P0137R1] for cases where the member access is on the left side of an assignment, but is now generalized as part of this new framework. As explained below, this does not permit type punning through unions; rather, it merely permits the active union member to be changed by a class member access expression.

  • A new barrier operation (distinct from std::launder, which does not create objects) should be introduced to the standard library, with semantics equivalent to a memmove with the same source and destination storage. As a strawman, we suggest:

    // Requires: [start, (char*)start + length) denotes a region of allocated
    // storage that is a subset of the region of storage reachable through start.
    // Effects: implicitly creates objects within the denoted region.
    void std::bless(void *start, size_t length);
    

In addition to the above, an implementation-defined set of non-stasndard memory allocation and mapping functions, such as mmap on POSIX systems and VirtualAlloc on Windows systems, should be specified as implicitly creating objects.

Note that a pointer reinterpret_cast is not considered sufficient to trigger implicit object creation.

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • @dyp wow, that is an awesome discussion, it is going to take a while to digest it but it is priceless, Thank you for pointing that out. – Shafik Yaghmour May 10 '15 at 17:03
  • 1
    Unfortunately, it is incomplete as far as I can tell (the beginning is missing and the conclusion is vague at best IMHO). – dyp May 10 '15 at 18:01
  • 1
    I think you meant "not specified" rather than "unspecified" (the latter term has a specific meaning in the C++ standard) ? – M.M Jun 10 '15 at 23:46
  • 1
    Also I have a corollary question (not sure if it is worth posting this as a separate question or not); do you feel it would make any difference if `T` had a non-trivial default constructor? (But is still trivially-copyable). – M.M Jun 10 '15 at 23:57
  • For what it's worth, the `N3751` [paper](http://open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3751.pdf) is targeting quite a different use-case than @M.M's original code. The code above copies a `T` object into uninitialized storage (where no object exists) and asks if the storage now contains a `T` (the consensus based on the current standard seems to be "no"). The N3751 paper talks about copying between two _existing_ differently typed objects from `T` to `U` and asks if it is legal to access `U` afterwards, and what the semantics are with respect to the lifetime of `U`. – BeeOnRope Oct 22 '17 at 20:42
  • The proposed remedy in `N3751` is simply to allow this `T` to `U` copy and to treat it as construction (not assignment). The proposed language wouldn't apply to the case posted by the OP. At their core, I think the two cases have quite different motivations: the N3751 case seems largely motivated by "type punning": allowing cases where it is useful to examine or modify the "bits" of one object through the lens of a different type, which is useful for some low-level operations if you assume a certain representation for types, e.g., floats. – BeeOnRope Oct 22 '17 at 20:45
  • 1
    On the other hand, the "does `memcpy` create an object" question seems more motivated by general purpose manipulation of trivially copyable types. For example, it seems "obvious" that when when `std::vector` needs to expand and copy it's underlying storage consisting of trivially copyable `T` objects, it can simply allocated new uninitialized storage of a larger size, and `memcpy` the existing over objects (indeed the standard explicitly guarantees that such copies between two `T` objects is well-defined). It's not allowed though because there is no`T` object yet in the uninitialized storage. – BeeOnRope Oct 22 '17 at 20:50
  • Now every standard library that I've checked simply does it via a direct `memcpy` anyways (as implementors, they can perhaps simply rely on the define-UB-however-you-want-it rule), but that's of little consolation for people who want to write portable code that uses the same idiom. For _trivially constructible_ types you can probably work around it by calling placement `new` before copying, which the compiler will usually optimize away to nothing, but for types with non-trivial constructors there is no good general workaround. Maybe [one day](https://stackoverflow.com/q/46840853/149138). – BeeOnRope Oct 22 '17 at 20:54
  • Considering the vector, isn't such operation (from sight of C++) rather moving than copying the objects to a new location in memory? If it was "copying", wouldn't we be required to call the destructors of the source objects? So the vector example appears to me not being applicable here. On the other hand, it shows that memcpy possibly might be legally used for both moving *and* copy initialising objects... – Aconcagua Jun 18 '18 at 10:42
  • "_An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed_" Nobody takes that statement seriously. It means that unions cannot be used. Actually the whole C++ object/storage model is a train wreck with obvious holes everywhere. – curiousguy Jun 18 '18 at 13:23
  • Also relevant [p0593 Implicit creation of objects for low-level object manipulation](http://wg21.link/p0593) – Shafik Yaghmour Jun 18 '18 at 15:45
  • 1
    I believe that this answer shall reflect that p0593 was embedded into C++20 and approved as a DR for all C++ versions from C++98 inclusive. So it seems that memcpy can be used to initialize a trivially-copyable object on a properly created storage. See also: https://stackoverflow.com/a/61999151/2085626 – Amir Kirsh May 25 '20 at 10:59
10

The code is legal now, and retroactively since C++98!

The answer by @Shafik Yaghmour is thorough and relates to the code validity as an open issue - which was the case when answered. Shafik's answer correctly refer to p0593 which at the time of the answer was a proposal. But since then, the proposal was accepted and things got defined.

Some History

The possibility of creating an object using malloc was not mentioned in the C++ specification before C++20, see for example C++17 spec [intro.object]:

The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.5.2.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).

Above wording does not refer to malloc as an option for creating an object, thus making it a de-facto undefined behavior.

It was then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 and accepted as a DR against all C++ versions since C++98 inclusive, then added into the C++20 spec, with the new wording:

[intro.object]

  1. The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)...

...

  1. Further, after implicitly creating objects within a specified region of storage, some operations are described as producing a pointer to a suitable created object. These operations select one of the 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. If no such pointer value would give the program defined behavior, the behavior of the program is undefined. If multiple such pointer values would give the program defined behavior, it is unspecified which such pointer value is produced.

The example given in C++20 spec is:

#include <cstdlib>
struct X { int a, b; };
X *make_x() {
   // The call to std​::​malloc implicitly creates an object of type X
   // and its subobjects a and b, and returns a pointer to that X object
   // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
   // in order to give the subsequent class member access operations   
   // defined behavior. 
   X *p = (X*)std::malloc(sizeof(struct X));
   p->a = 1;   
   p->b = 2;
   return p;
}

As for the use of memcpy - @Shafik Yaghmour already addresses that, this part is valid for trivially copyable types (the wording changed from POD in C++98 and C++03 to trivially copyable types in C++11 and after).


Bottom line: the code is valid.

As for the question of lifetime, let's dig into the code in question:

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) ); // <= just an allocation
    if ( !buf ) return 0;

    T a{}; // <= here an object is born of course
    std::memcpy(buf, &a, sizeof a);      // <= just a copy of bytes
    T *b = static_cast<T *>(buf);        // <= here an object is "born"
                                         //    without constructor    
    b->x = b->y;

    free(buf);
} 

Note that one may add a call to the destructor of *b, for the sake of completeness, before freeing buf:

b->~T();
free(buf);

though this is not required by the spec.

Alternatively, deleting b is also an option:

delete b;
// instead of:
// free(buf);

But as said, the code is valid as is.

Amir Kirsh
  • 12,564
  • 41
  • 74
  • Actually, according to [P0593R6](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html), the object is 'born' (or created) as part of the `memcpy()` call and **not** as a part of the `static_cast()`. – maxschlepzig Jun 10 '23 at 11:03
2

From a quick search.

"... lifetime begins when the properly-aligned storage for the object is allocated and ends when the storage is deallocated or reused by another object."

So, I would say by this definition, the lifetime begins with the allocation and ends with the free.

g3cko
  • 920
  • 1
  • 6
  • 21
  • 4
    It seems a bit fishy to say that `void *buf = malloc( sizeof(T) )` has created an object of type `T`. After all, it could equally well have created an object of any type whose size is `sizeof(T)` , we don't yet know whether this code will go on to point `T *b` at it, or `U *u` for example – M.M May 08 '15 at 02:02
  • That's a good point; since this is not based on the scoping rules, that made the most sense to me. The alternative, maybe `*b` is not an object, and only `b` is. – g3cko May 08 '15 at 02:23
  • Yes but does it matter? If it is a trivial constructor we know it is mostly a noop. So long as their size and alignment constraints are met, `malloc` can be considered a trivial constructor equivalent can it not? – nonsensickle May 08 '15 at 02:23
  • 2
    @nonsensickle I'm hoping for a "language lawyer" quality answer, e.g. text from the C++ standard to support that malloc can be considered a trivial constructor – M.M May 08 '15 at 02:25
  • 2
    @MattMcNabb, memory from `malloc` has "no *declared type*". http://stackoverflow.com/questions/31483064/what-is-the-c-equivalent-of-an-allocated-object-having-no-declared-type-in-c As such, its *effective type* can change many times through its lifetime; each time it is written to it takes the type of the written data. In particular, that answers quotes how `memcpy` copies the effective type of the source data. But I guess that's C, not C++, and maybe it's different – Aaron McDaid Jul 21 '15 at 17:22
  • @AaronMcDaid yes that's C and it is different – M.M Jul 22 '15 at 03:39
  • @MattMcNabb A trivial ctor call is trivial, equivalent to nothing. You can pretend it happened. – curiousguy Aug 16 '15 at 01:42
  • @AaronMcDaid This effective type invention is a mistake of the C committee. – curiousguy Aug 16 '15 at 14:58
  • 1
    @curiousguy: The Strict Aliasing Rule would be meaningless without the concept of "effective type". On the other hand, I consider the concept of type-based aliasing rules itself to be a mistake, since it simultaneously forces programmers to write inefficient code using `memcpy` or `memmove` and hope an optimizer can fix it, while failing to allow compilers to make what should be simple and easy optimizations in cases where a programmer knows (and could tell the compiler) that certain things won't alias. – supercat Sep 15 '15 at 19:05
  • @supercat With the C concept of effective type, even `memcpy` doesn't solve the issue of aliasing. – curiousguy Sep 15 '15 at 20:49
  • 1
    @curiousguy: I thought it did (which was the reason `char` got special treatment)? Though I'll admit I don't understand all the rules of what's legitimate and not, since the rules are horrible compared with what could be achieved by adding a `__cache(x) {block}` statement which would entitle a compiler to assume that the value of `x` will not be changed by any means outside the control of the attached block. Any compiler could be compatible with such a statement merely by having `__cache(x)` be a macro that expands to nothing, but it would allow compilers to do a lot of register... – supercat Sep 15 '15 at 21:05
  • ...optimizations easily that would otherwise require full-program optimization [and in some cases involving indirect function calls can't be done even with FPO]. – supercat Sep 15 '15 at 21:05
  • @supercat The issue is that "copying bytes" (which is not a well defined concept in the first place, as I have demonstrated in my C/C++ questions about pointers) is defined as transferring the effective type, which defeats the "use `memcpy` to avoid strict aliasing violation" concept. – curiousguy Sep 23 '15 at 20:10
  • 1
    @curiousguy: I think I gotcha. Sounds like the Committee discovered that there were so many cases where requiring `memcpy` to not invoke the Strict Aliasing Rule would impair useful optimizations that they decided the solution was to make it so memcpy could work but leave the rule in effect, rather than replace the Strict Aliasing Rule with some new qualifiers (which compilers could ignore if they didn't know how to benefit from them) which could offer far greater benefits with fewer dangers. – supercat Sep 23 '15 at 20:18
1

Is this code correct?

Well, it will usually "work", but only for trivial types.

I know you did not ask for it, but lets use an example with a non-trivial type:

#include <cstdlib>
#include <cstring>
#include <string>

struct T   // trivially copyable type
{
    std::string x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    a.x = "test";

    std::memcpy(buf, &a, sizeof a);    
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

After constructing a, a.x is assigned a value. Let's assume that std::string is not optimized to use a local buffer for small string values, just a data pointer to an external memory block. The memcpy() copies the internal data of a as-is into buf. Now a.x and b->x refer to the same memory address for the string data. When b->x is assigned a new value, that memory block is freed, but a.x still refers to it. When a then goes out of scope at the end of main(), it tries to free the same memory block again. Undefined behavior occurs.

If you want to be "correct", the right way to construct an object into an existing memory block is to use the placement-new operator instead, eg:

#include <cstdlib>
#include <cstring>

struct T   // does not have to be trivially copyable
{
    // any members
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T *b = new(buf) T; // <- placement-new
    // calls the T() constructor, which in turn calls
    // all member constructors...

    // b is a valid self-contained object,
    // use as needed...

    b->~T(); // <-- no placement-delete, must call the destructor explicitly
    free(buf);
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • struct T containing a ::std::string is not trivially copyable in c++14 and onwards – user1095108 Sep 01 '16 at 13:57
  • 2
    An object containing a `std::string` has never been trivially copyable. It looks like a copy+paste mistake, the code in the question has a "trivially copyable" comment, and when the code was edited for the answer, the comment wasn't updated. – Ben Voigt Feb 07 '18 at 21:44