C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relocation in C++

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Wed, 18 May 2022 11:56:35 +0200
Hi all,

> The wrinkle here is that for a relocator synthesized from move+destroy,
the
> destination and source lifetime do overlap, observably

I am honestly confused here: what kind of synthesized relocation are you
talking about? The reloc operator does not perform move+destruct. If the
object is not trivially relocatable, and does not have an accessible
operator reloc() member function, then all that reloc does is constructing
a new object using the move (or copy) constructor, no destruction is
involved by reloc per-se:

// T is not trivially relocatable, and does not have an accessible operator
reloc() member function

void foo(T foo_obj);

void bar()
{
    T bar_obj;
    foo(reloc bar_obj); // equivalent to: foo(T{const_cast<T&&>(bar_obj)});
    // The destructor of bar_obj is not called at the end of the foo()
function call.
    some_dummy_code();
    // The destructor of bar_obj is called here, as if std::move were used
when calling foo
}

> But actually (or at least potentially) throwing move constructors are
> reasonably common, so if we wrap them in an automatically generated
> noexcept-by-default synthesized special member function/operator, we're
> running the risk of destroying the credibility of the noexcept guarantee -
> we do not want to find that people (e.g. container authors) are avoiding
> calling relocate because they're worried about std::terminate from an
> automatically noexcept relocator and are instead performing move+destroy
> manually.

I am not sure how the move constructor gets involved in operator reloc()
member function. What is proposed is:
- to use trivial relocation
- use the operator reloc()
- or only move (knowing that the destructor will be called at some point
later)

The operator reloc() member function is not designed to fallback to the
move constructor + destructor (at least not in what is proposed currently).

A class that provides any user-defined copy constructor, move constructor,
or destructor will have its operator reloc() implicitly deleted. Class
authors are still free to default it or provide a definition.

If a class has any of its base class or data-member with an implicitly
deleted operator reloc(), then its operator reloc() will be implicitly
deleted as well. And writing a custom operator reloc() may prove to be
complicated. Consider:

// B has an implicitly deleted operator reloc(), as it provides for
instance a user-defined move constructor and destructor.
struct T : B
{
    D data;

    // User defined operator reloc()?
    operator reloc(T&& src) :
        B{std::move(src)}
        /* data is initialized using default rules */
    { src.~B(); /* manually destruct B part */ }
}

My take is that if T's operator reloc() were defaulted, then it would get
deleted instead of having a default definition, as B's operator reloc() is
implicitly deleted.

This is the same mechanism that applies to move constructors. AFAIK
default-generated move-constructors do not delegate to their base class
copy constructor if their base class move constructor is implicitly
deleted. I am not sure it is a good idea to have operator reloc() make a
synthesized relocation, calling move+destruct if the base class or
data-member has an implicitly deleted operator reloc() member function, if
that is what you are suggesting.

> Actually, can we rethink this? One of the success criteria here is that we
> should be able to pass std::unique_ptr (or equivalent) in registers. So
> that means that we're accepting at least some level of ABI breakage.
>
> Any class that is trivially relocatable or has an accessible relocate
> operator must either have been trivial already (in which case there is no
> ABI impact) or have been explicitly opted in to relocation, by having a
> relocator declared or defaulted. Since this is under the control of the
> library author and/or user, I don't see it as an issue to say that
> functions with parameters having trivial or user-defined relocate have a
> different ABI (callee-destroy, or if necessary having automatic
> accompanying flags in registers or on the stack). We accept that adding a
> user-defined destructor, or virtual functions/bases, or changing data
> members changes ABI, so why shouldn't defaulting/declaring a relocator?

That's interesting, but I see some obstacles:

operator reloc() is not necessarily opt-in. It may be implicitly declared
and defined. This requires that the class has no user-defined constructors
and destructors (among other things). A class may get an operator reloc()
for free, without any changes in the class declaration nor in any of its
base classes and data-members (recursively). Functions that will take such
classes as a prvalue parameter will then have an ABI break?
Under those circumstances the ABI break may not be necessary. Indeed such
classes are POD types, and their destructor are a no-op. I guess we don't
really care that their destructor call cannot be alleviated.

For the other cases, operator reloc() is opt-in. However it is vital that
most standard library classes support this operator reloc(). Most STL
classes provide a user-defined constructor or destructor, and as such will
get an implicitly deleted operator reloc(). If they don't default or
provide their own operator reloc(), then any class that has an STL
data-member will get its operator reloc() implicitly deleted as well. They
will then struggle writing their own operator reloc() to support
relocation, and will in addition be ruled out of trivial relocation (double
pain).

If the STL then explicitly supports operator reloc() (like it should be
required) then any function having an STL parameter passed by value (like
void foo(std::unique_ptr<T>); ) will get an ABI break. I really have no
idea if that is acceptable.

> We have a precedent for noexcept(true)-by-default and that's the
destructor. I
> think it could be acceptable to make the relocator obey the same rule
because
> after it's run -- and whether it has thrown or not -- one object's
lifetime
> has ended.

Given that, AFAIK, there is no synthesized relocation involved, then yes
the relocator (operator reloc() member function) can be noexcept(true) by
default. Then any exception that leaks through a relocator causes
std::terminate to be called.

Sébastien

Received on 2022-05-18 09:56:47