C++ Logo

std-proposals

Advanced search

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

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Fri, 7 Oct 2022 16:35:34 +0200
On Sun, Oct 2, 2022 at 11:09 PM Edward Catmur <ecatmur_at_[hidden]>
wrote:

> On Fri, 30 Sept 2022 at 12:06, Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> OK, let's run down the cases. Bear in mind that only objects of value
>> category prvalue can be decomposed; that is, relocating structured binding
>> would not apply if there are any ref-qualifiers between `auto` and `[`.
>>
>> For an array type: yes, there could be dependencies between the members,
>> but an array prvalue can only be created in-place (it can't be returned
>> from a function) and so the responsibility would be on the function author.
>> So decomposition into individual, independent elements is fine.
>>
>> For a tuple-like type: the trigger for relocating/decomposing structured
>> binding would be that the hidden object `e` is prvalue and that the (new,
>> extension) customization point is found by lookup. So the responsibility is
>> on the class author to ensure that decomposition is safe; if they can't
>> guarantee that, they just don't provide the new customization point and the
>> structured binding yields xvalue references to the results of calling
>> get<I>.
>>
>> For binding to data members: yes, this should only proceed as
>> decomposition if the class E (and every base class between E and the base
>> class the data members are found in) does not have a user-defined
>> destructor. Again, if this does not apply we fall back to structured
>> bindings as references; lvalues in this case.
>>
>> For case 1 (array) and case 3 (binding to data members) this is
>> completely invisible because of relocation elision; the effect is exactly
>> as if the compiler were to track which array elements or data members are
>> subsequently relocated and call the destructor on those which are not.
>>
>
> Also, any issues caused by out-of-order destruction exist today for moving
> from those objects, for the most part.
>

Yes, I agree. I guess what worries me the most is incorrect uses of
std::decompose.


> For case 2 (tuple protocol), there is a very visible effect in which
>> customization point is called; it's the responsibility of the class author
>> to ensure that they are equivalent.
>>
>> Another way would be to have a new statement: `auto reloc [x, y] =
>>> foo();` (or `auto &< [x, y] = foo();`...) to clearly mark that we want to
>>> split into individual objects. This best expresses the intent, and would
>>> not break existing code.
>>>
>>
>> As above, I don't think there's any real danger of breaking existing
>> code; class authors could write a bad destructuring get_all in the
>> tuple-like case, but that's their choice and responsibility.
>>
>> What I would worry about is the fallback being invisible and degrading
>> performance, but I think that we're fine if we just make `reloc x`
>> ill-formed in that case.
>>
>
> On second thoughts, I don't think it should be ill-formed; it should just
> fall back to move, just as it would for a class that is movable but not
> relocatable. If the class is truly relocate-only, then it would be
> ill-formed.
>

It could act normally, simply changing the value-category, and ruling out
the relocation ctor during overload resolution. (Unless `x` is
ref-qualified, in which case it's equivalent to
`static_cast<decltype(x)>(x)`).
This is what we are doing when relocating a function parameter captured by
value, if the function ABI is caller-detroy.
In both cases it would be ill-formed for relocate-only types.

Note that `auto reloc [x, y] = foo()` could also give the guarantee that
`x` and `y` are individual objects (would be ill-formed otherwise). Hence
C++ would not silently rule out the relocation ctor.
Not to say I am in favor of `auto reloc`, I think proper compiler error
messages can do the job of explaining why the relocation ctor was ruled out.


> Instead, consider: `std::decompose` is accessing (on behalf of its caller)
>>>>>> each direct subobject (base and data member) that is returned. But it is
>>>>>> also accessing the *other* direct subobjects that it does not return, in
>>>>>> order to destroy them. So let's say that to call std::decompose, you must
>>>>>> have access to each direct subobject, including those that you don't
>>>>>> request. (Plus their relocators or destructors, respectively.)
>>>>>>
>>>>>> Then `auto [p] = std::decompose<&PainterWithGuard::_p>(reloc
>>>>>> painterWithGuard);` would be ill-formed because in that context,
>>>>>> `painterWithGuard._guard` is ill-formed.
>>>>>>
>>>>>> Do you think this would work?
>>>>>>
>>>>>
>>>>> But if _guard were declared as public by mistake then all those
>>>>> safeties are bypassed and std::decompose will cause trouble.
>>>>>
>>>>
>>>> In that case, aggregate-style structured binding would likely also
>>>> work. And that's definitely the class author's fault.
>>>>
>>>
>>> Yes but it would not break things the way std::decompose does. (type may
>>> be movable but not decomposable).
>>>
>>
>> True. I guess you have to accept some small risk of breakage; even an
>> aggregate struct S { X x; Y y; }; could have invariants established
>> (post-construction) between x and y that break if x is destroyed before y.
>> But that's fragile code already; the author should have made those data
>> members private. At least they can forestall this by adding a user-declared
>> destructor.
>>
>
Yes. I agree that we need this, but I would have liked it to be safer.

When I first proposed `get_bindings`, it took all subobjects of the class
passed by prvalue. The language itself would split the source object, and
pass all parts to get_bindings. This approach has several problems, it is
quite inconvenient to use when we have a large amount of subobjects, and
will not detect manual same-type data-member reordering in the class
declaration. And it does not even support arrays. But at least, it was
*opt-in*. You could only decompose an object if the type had implemented
that weird get_bindings function, which would give the guarantee that this
was a safe operation.

This is what is lacking to std::decompose IMO. The class has almost no say
in this, it opts in by default. They can provide a user-defined destructor,
but that feels like an opt-out side effect.
I'd prefer if things were reversed, a class type opts out by default
(std::decompose is ill-formed) but can opt-in.


> This also limits the use of std::decompose to the class implementation and
>>>>> friends. It is not necessarily a bad thing as it requires the owner to know
>>>>> of the class internals (or at least to be in the position of knowing...).
>>>>> Besides I expect the main use of std::decompose to be from the new special
>>>>> get_bindings function.
>>>>>
>>>>
>>>> Yes, and that customization point (or some `get_all`,
>>>> `get<index_sequence<I...>>`, etc.) would be expected to be a member or
>>>> friend.
>>>>
>>>
>>> That could work. BTW I am more inclined to give it another name than
>>> `get` as we could have a tuple that contains an index_sequence as
>>> parameter, and then we don't know what std::get would refer to.
>>>
>>
>> Yes, good point.
>>
>> Actually, from a brutally practical perspective, handling tuple-like
>> types could be omitted from a MVP. You'd still be able to destructure
>> std::pair by wrapping it in another (derived) type to suppress the tuple
>> protocol (since `first` and `second` are direct data members of the same
>> class), and authors of tuple-like types outside the Standard Library would
>> be able to write their own function to return the relocated elements as
>> data members of an aggregate.
>>
>
I don't see how it could work for a custom tuple class. You cannot declare
a structure with a variable amount of heterogeneous types. `template
<class... Args> struct Tpl { Args... args; };` does not work AFAIK. If the
compiler does not provide one for you as it does for std::tuple, then I
don't see how boost::tuple and the like could support this.


> In addition, now that I think of it, the no user-provided special ctors
>>>>> and dtor clause is required to allow structure binding relocation from a
>>>>> class-type. In auto [x, y] = reloc data, data must be a C array (no problem
>>>>> there), a class-type with all data-members coming from the class-type or
>>>>> the same base, or provide a get_binding function that applies recursively.
>>>>> The end result of get_binding must fall into the C array case or the
>>>>> class-type case. For the class-type case, we must be allowed to split the
>>>>> type into individual subobjects, and have again that sort of guarantee that
>>>>> subobjects can be considered individually. Hence this no user-provided
>>>>> special ctors and dtor clause must apply to the class-type. This is the
>>>>> case for whatever std::decompose returns, but it is worth mentioning. And
>>>>> that bit makes me think that this clause should also apply to
>>>>> std::decompose.
>>>>>
>>>>
>>>> Remember, the motivation is for libraries (not necessarily the Standard
>>>> Library) to be able to write decompositions for their types; in particular,
>>>> for tuple types. For example, Boost.Tuple should be able to support
>>>> relocating decomposition without any change to its current ABI or API. So
>>>> user-provided special member functions must be allowed.
>>>>
>>>
>>> Fair point.
>>>
>>>
>>>> I can't feel satisfied with this solution. We end up with two ways of
>>>>> decomposing: std::decompose and the get_binding function. get_binding is
>>>>> still necessary as you can't access nested subobjects from std::decompose,
>>>>> or even generate objects on demand when using structured bindings. And
>>>>> other times get_bindings can be bypassed if the user performs structured
>>>>> binding by calling std::decompose directly. I find this dual approach a bit
>>>>> confusing.
>>>>>
>>>>
>>>> Well, the `get` protocol is for public use, and `std::decompose` for
>>>> privileged access - although it can be used on aggregate types. Also it'd
>>>> be useful for prvalue qualified member functions, etc. where previously the
>>>> "union hack" would have been necessary, e.g.:
>>>>
>>>> template<class T>
>>>> T* unique_ptr<T>::release(this unique_ptr self) {
>>>> auto const [ptr] = std::decompose<&unique_ptr::ptr_>(reloc self);
>>>> return ptr;
>>>> }
>>>>
>>>
>>> Yes, I agree with that. I worry about mistaken uses of std::decompose
>>> for classes where access control is badly designed.
>>>
>>
>> Yes. But if we don't provide it, the union hack will be used, and then
>> there's the same risk plus the risk of forgetting subobject destructors.
>> Maybe this is OK for an initial proposal.
>>
>
I think I'll propose std::decompose, and we will see the reception it gets
:) Although I am still thinking of a way to make it safer.

Received on 2022-10-07 14:35:47