C++ Logo


Advanced search

Re: [ub] memcpy blessing a new object of statically unknown type

From: Gabriel Dos Reis <gdr_at_[hidden]>
Date: Sat, 9 Jan 2016 21:42:14 +0000
My assumption has always been that std::launder is a zero-overhead abstraction (really a no-op at the machine level) whole sole purpose is to inform the static semantics elaborator about the lifetime and type of a given storage. I hope we aren’t considering anything more complicated than that.

-- Gaby

From: ub-bounces_at_[hidden] [mailto:ub-bounces_at_[hidden]] On Behalf Of David Krauss
Sent: Friday, January 8, 2016 10:36 PM
To: WG21 UB study group <ub_at_[hidden]>
Cc: joel.lamotte_at_[hidden]
Subject: Re: [ub] memcpy blessing a new object of statically unknown type

On 2016–01–09, at 11:35 AM, Hubert Tong <hubert.reinterpretcast_at_[hidden]<mailto:hubert.reinterpretcast_at_[hidden]>> wrote:

Yes, std::launder requires a static type; however, it does not limit the ability of memcpy to operate without knowing the type of the object being copied. The std::launder call is involved after the completion of memcpy to access the object that the memcpyinitialized.

Right, but in type erasure, the static type must be determined by inspecting the blob somehow. (Stashing a discriminating value elsewhere is one solution, but it’s more common and often more efficient to use an abstract base class or a discriminator inside a union.)

My library would launder the erasure_base subobject to retrieve its dispatch table, but then it’d be stuck. Dispatching to a derived class would lead back to UB.

One workaround could be to launder the same address repeatedly as the type becomes better resolved. For example, the call wrapper could launder a base class address, then perform an indirect call, then the callee could launder again to the derived class. For the common case of virtual dynamic dispatch, this sounds like it would incur UB before first line of the callee. My library doesn’t use virtual, but similar ones do. If only complete objects can be laundered, devirtualization could kick in… or launder could refuse to handle an abstract class at all. The workaround would also imply an excessive number of derived-type launder calls, which could compromise optimization by suggesting that bitwise manipulations are occurring when none are. Reloading the dispatch table pointer costs cycles.

Perhaps a second style of laundering could implement a compromise. First, auto &header = *launder(header_ptr) gets a fully-formed header object from a blob, and then auto &whole = launder_extend<whole_type>(header) revises the object identity to make header a subobject sharing its address with another already-fully-formed object of type whole. (For example, header could be a base, a union member, or an initial struct member.) The launder_extend function differs in that it acts only if its argument was believed to be a complete object (i.e. fresh from launder), and it only launders the remainder of the new complete object. To solve the virtual issue, do not let launder imply that its result is most-derived. Perhaps, let virtual dispatch implicitly do launder_extend.

This scheme leaves launder open-ended so a polymorphic object or union can be used, yet still laundered further. A simple implementation can opt to treat launder_extend the same as launder.

struct discriminator { int value; };
struct foo { discriminator d; int i; };
struct bar { discriminator d; float f; };
union foobar { foo a; bar b; };
struct baz : discriminator { double x; };
struct bad : discriminator { virtual ~ bad(); };

void unpack( discriminator * p ) {
    std::launder( p ); // OK: now we can access p.
    int disc = p->value;
    if ( disc == 0 ) {
        auto & f = std::launder_extend< foo >( * p ); // OK: now we can access a foo.
        int q = p->value; // Load may be elided. Value is already in disc, equal to zero.
        auto & fb = std::launder_extend< foobar >( * f ); // OK: a further extension to a super-object.
        auto & f2 = std::launder_extend< foo >( * p ); // OK, no-op: p was already extended, becoming a subobject.
        auto & b = std::launder_extend< bar >( * p ); // UB: there’s already a different object there.
        auto & i = std::launder_extend< int >( * p ); // Library precondition violation: invalid object extension.
    } else if ( disc == 1 ) {
        baz & z = std::launder_extend< baz >( * p ); // OK, but implementation-dependent in theory.
    } else if ( disc == 2 ) {
        // Library precondition violation: no discriminator subobject shares an address with class bad.
        bad & x = std::launder_extend< bad >( * p );

Received on 2016-01-10 00:14:50