7

Let S be a struct type which contains a character array data which has the maximum alignment and a fixed size. The idea is that S is able to store any object of type T whose size does not exceed the limit and which is trivially-copy-constructible and trivially-destructible.

static constexpr std::size_t MaxSize = 16;
struct S {
    alignas(alignof(std::max_align_t)) char data[MaxSize];
};

Placement-new is used to construct an object of type T into the character array of a new S object. This object is then copied any number of times, including being returned and passed by value.

template <typename T>
S wrap(T t) {
    static_assert(sizeof(T) <= MaxSize, "");
    static_assert(std::is_trivially_copy_constructible_v<T>, "");
    static_assert(std::is_trivially_destructible_v<T>, "");

    S s;
    new(reinterpret_cast<T *>(s.data)) T(t);
    return s;
}

Later given a copy of this S value, reinterpret_cast is used to obtain T* from the pointer to the start of the character array, and then the T object is accessed in some way. The T type is the same as when the value was created.

void access(S s) {
    T *t = reinterpret_cast<T *>(s.data);
    t->print();
}

I would like to know if there is any undefined behavior involved in this scheme and how it would be solved. For instance, I am worried about:

  • Is there a problem with "reusing object storage", i.e. the problem that std::launder is designed to solve? I am not sure if it is valid to access data as a character array after constructing an instance of T there. Would I need std::launder in the place where the value is accessed, and why?
  • Is there a problem in the generated copy constructor of S which copies all the bytes in data, because some bytes might not have been initialized? I am worried both about bytes beyond sizeof(T) as well as possibly uninitialized bytes within the T object (e.g. padding).

My use case for this is implementation of a very lightweight polymorphic function wrapper which is able to be used with any callable satisfying those requirements that I have listed for T.

Ambroz Bizjak
  • 7,809
  • 1
  • 38
  • 49
  • I'm worried the biggest problem will be one you haven't mentioned and which I don't know how to solve: the access by the copy constructor to the underlying bytes after that storage has been re-used for other objects. And you cannot insert `std::launder` there. –  Apr 15 '18 at 12:03
  • @hvd: How could storage be reused for other objects? The only way I create `S` objects is through `create()`. At most I may assign these new `S` values to existing `S` values, but this is just copying bytes. – Ambroz Bizjak Apr 15 '18 at 12:17
  • You're reusing the storage in `new(reinterpret_cast(s.data)) T(t);`. After that, you access the storage directly, implicitly, in `return s;`. I may be wrong, but I *think* a compiler is allowed to see that the object created by placement-new is never accessed and optimise it away. –  Apr 15 '18 at 12:24
  • @hvd: Ah. I suppose constructing a local `T` object then `memcpy` into `S` would solve that? – Ambroz Bizjak Apr 15 '18 at 12:25
  • Good point, that should work. –  Apr 15 '18 at 12:41

2 Answers2

0
template<class T>
T* laundry_pod(void* ptr){
  char buff[sizeof(T)];
  std::memcpy(buff, ptr, sizeof(T));
  auto* r=::new(ptr)T;
  std::memcpy(ptr, buff, sizeof(T));
  return r;
}

this may be of use. It converts the compilers interpretation of data at a location from one pod type to another, and compiles to a noop under optimization.

So take your buffer, laundry pod it to T, write to it, laundry pod back to gdneric storage. To read, laundry pod to T. Note that all writes through T pointer should be followed by laundering it back to the generic storage to avoid aliasing rules being used to optimize away the writes on the seemingly discarded ptr.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

So there are three steps here: the creation of the T object in the char array, the copying of that array, and the access to the T. We will discuss them in turn.

Explicitly creating an object in an aligned buffer should only be done using unsigned char and std::byte buffers. Those have explicit rules in the language that allow them to "provide storage" for objects created within them. See [intro.object]/3 and examples therein. When you use a char buffer, it does not provide storage for the T object, and the T object is not "nested within" the buffer. Under [basic.life]/1.5, the creation of the T object ends the lifetimes of the char objects whose storage the T object occupies (although this section is not clear about whether the lifetime of the enclosing array object is also ended). Let's assume that an unsigned char buffer will be used for the remainder of the answer.

We now come to the question of whether unsigned char buffers that have T objects nested within them can be copied as unsigned char buffers without causing UB. With copy construction, [intro.object]/13 kicks in:

An operation that begins the lifetime of an array of char, unsigned char, or std​::​byte implicitly creates objects within the region of storage occupied by the array.
[Note 5: The array object provides storage for these objects. —end note]
...

(I believe the fact that char is included here is a CWG issue and it will eventually be removed, but I'm too lazy to double-check right now.)

When an S is copy-constructed, and the new object's lifetime begins, so does the lifetime of the unsigned char array member. This implicitly creates a T object in the new array if necessary. When the bytes are actually copied over from the old S object to the new one, then [basic.types.general]/3 applies:

For two distinct objects obj1 and obj2 of trivially copyable type T, where neither obj1 nor obj2 is a potentially-overlapping subobject, if the underlying bytes ([intro.memory]) making up obj1 are copied into obj2,31 obj2 shall subsequently hold the same value as obj1.

(The footnote says that std::memcpy and std::memmove are ways of doing this, but the copy constructor of S will do just as well.)

Copy-assignment of S will work fine as long as you don't expect it to perform implicit object creation. If the LHS already has a T object nested inside it and the RHS does as well, then [basic.types.general]/13 implies that after the assignment, the LHS's T object will hold the same value as the RHS's T object.

Finally, we come to the issue of access. In this case we begin with a pointer to the unsigned char array and reinterpret_cast it to T*. This will in fact not do what you expect, because the unsigned char array is not pointer-interconvertible with the T object that it provides storage for. See [expr.reinterpret.cast]/7 and [expr.static.cast]/14, which imply that when the pointer-interconvertibility criterion is not met, the result of the reinterpret_cast is a pointer to the original object (not a pointer to the T object you're trying to access). The function std::launder must be used to convert the T* value that you get back from reinterpret_cast into an actually valid pointer to the T object that's nested within the array. If this is not done, the behaviour will be undefined when you try to access a T object through a pointer that does not actually point to a T object.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312