C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Mon, 19 Sep 2022 22:28:23 +0100
On Mon, 19 Sept 2022 at 10:41, S├ębastien Bini <sebastien.bini_at_[hidden]>
wrote:

> Hi all,
>
> I have another idea, which is to use recursion in the manner of
>>>>> operator->(). For example, let's say that structured binding for
>>>>> tuple-like class types first looks for
>>>>> `get<std::make_index_sequence<std::tuple_size<E>::value>>(reloc e)` (the
>>>>> exact syntax isn't important, but it should be something clearly novel and
>>>>> `e` should be prvalue if the original object was). Then, whatever this
>>>>> call returns (which must have the same `tuple_size` as `E`) is in turn
>>>>> submitted for structured binding (as a prvalue).
>>>>>
>>>>
>>>>> Of course, library authors will then need to create a struct type with
>>>>> the appropriate number of fields, but it won't be difficult for compiler
>>>>> authors to write an intrinsic to do that, and once it's available for
>>>>> std::tuple and std::array then everyone else can just recurse to those
>>>>> types. Anyone who can't/ doesn't want to recurse to the Library can use
>>>>> preprocessor hackery, other code generation, or possibly metaclasses (once
>>>>> those are available), to support any sensible number of elements.
>>>>>
>>>>> It'd be ugly to convert `std::array<T, N>` to essentially `struct { T
>>>>> t0, t1, ...tn-1; };` but N should be small (at least until we get variadic
>>>>> structured binding) and compiler authors can always hack in a better
>>>>> solution for privileged Library classes as long as the general case can be
>>>>> made to work for non-Standard library authors. A more ambitious solution
>>>>> would be to allow returning arrays from functions.
>>>>>
>>>>
> I really like this idea. It's a bit of a shame though that std::pair will
> have to provide its own get<index_sequence<0, 1>> and perform an extra
> relocation, while it could already fit the data-member binding protocol :/
> But that's a minor detail.
> Nice solution though :)
>
> For example, one could write:
>>>>>
>>>>> template<std::size_t... I, class... T> requires
>>>>> std::same_as<std::index_sequence<I...>, std::index_sequence_for<T...>>
>>>>> auto get<std::index_sequence<I...>>(my_tuple<T...> t) {
>>>>> union { my_tuple<T...> tt; } = {.tt = reloc t}; // prevent
>>>>> destructor
>>>>> return std::tuple<T...>(std::relocate(&get<I>(tt))...);
>>>>> }
>>>>>
>>>>
> The union trick is a bit hacky, and it's not exception-safe (if exceptions
> leak through the std::relocate calls). Also what happens if I want to write
> this function for a custom class, but only want to provide a subset of all
> data-members? Then, if I use the union trick, I need to manually call the
> destructor on all non-relocated data-members...
> But well, this is just reserved for library writers so it may be
> acceptable.
>
> I see another unorthodox solution though. This get<index_sequence> is to
> be called by the language, and not manually. Hence what if the signature
> were:
> auto get_bindings<E>(D1 d1, D2 d2, D3 d3, ...), where Di is the i-th
> subobject of E in declaration order, passed as prvalue. There are as many
> parameters passed as there are subobjects in E. The E template parameter
> must not be there if defined as an E's member function (which mmakes the
> function non-template). If such a function exists then the language will
> manually split E into all of its subobjects by relocation, and make the
> function call. We can keep the recursive bit so as long as there exists
> such a get_bindings function that matches for the return type, it is
> applied.
>

I like it, but...

This way we can safely write our own without being hacky:
> template<class... T>
> auto get_bindings<my_tuple<T...>>(T... t) {
> return std::tuple<T...>(reloc t...);
> }
>
> If we need to retrieve base class data-members, or nested data-members,
> that's still doable:
> struct B { T _b }; struct D : B { T _d; T_arr[2]; };
>
> template <>
> auto get_bindings<D>(B base, T d, T arr[2])
> {
> auto [b] = reloc base; // calls get_bindings<B>(T) if it exists, or else
> relies on data-member protocol.
> auto [a1, a2] = reloc arr;
> return std::tuple{reloc b, reloc d, reloc a2};
> }
>

There's a problem here; you can't pass arrays by value. An array-typed
function parameter is decayed to a pointer in the signature, so `arr` is
actually of type `T*`. I'm not sure what can be done here; perhaps expand
out arrays into their elements?

When structured relocation is used we can issue a warning if a get_bindings
> is declared for the class type but its parameters don't match (it may very
> well lead to an error if other binding protocols fail to apply). That way
> we can warn the user that they forgot to update the get_binding function
> when they changed their class layout.
>
> I know it's unorthodox, but it's safe. I wonder how it mixes with virtual
> inheritance; maybe we should forbid such types...
>

Yes, virtual bases would have to be banned - or, at least, complete types
with overlapping immediate subobjects.

I almost want to propose a magic library function that takes a prvalue, a
list of immediate bases and pointers to data members, and returns those
subobjects as a destructurable class type. Something like:

    auto [b, x, arr] = std::decompose<B, &D::x_, &D::arr_>(reloc obj);

It would check that the returned bases and data members are direct,
distinct, non-overlapping and relocatable, and destroy any subobjects not
mentioned. Then you would be able to return that `std::decompose` result
directly, or perform further processing it.


> *About tuples:*
> Another problem is that there are no std::tuple constructors that allow to
> relocate its input parameters. Sure, we could write one, but I fail to see
> a good signature. I fear, because of the variadic template, that either all
> input parameters are prvalues, or all are forwarding references. With such
> a signature we cannot build a tuple by moving its first parameter and
> relocating its second.
>
> Acknowledging that, we could simply write (Types... being the std::tuple
> template parameters):
> std::tuple::tuple(Types... types)
>
> However there are already:
> std::tuple::tuple(Types const&... types)
> template <class... UTypes>
> std::tuple::tuple(UTypes&&... types)
>
> I can already see people asking why the wrong constructor was picked. See
> for instance: std::tuple<std::string, RelocOnlyTp, int>{reloc a, reloc b,
> reloc c}, but unfortunately c is long long int in our Murphy scenario,
> which makes the forwarding reference constructor being picked because of
> the type discrepancy. Then the whole thing is ill-formed because `b` is
> relocate-only and cannot be moved.
>

It would also pose problems for immovable types that must be constructed
in-place by conversion or converting constructor... although those are
probably rather more rare than your scenario.


> I can think of another solution:
> template <class... UTypes>
> std::tuple::tuple(UTypes... types)
>
> Using this constructor, all parameters must be passed as prvalues. This
> means for users to use reloc on a parameter they wish to relocate,
> std::cref on a parameter they wish to copy, and a new std::mref on a
> parameter they wish to move (std::mref being a helper function that builds
> a new std::move_reference_wrapper. We cannot use std::move because it
> returns an xvalue, not a prvalue...). Thankfully for us, std::cref and
> std::mref return a prvalue. This tuple constructor would be able to detect
> the two kinds of reference wrappers and unwrap them to call the appropriate
> constructor.
>
> However I see two potential problems. First, I am not sure how things
> might conflict against the tuple(UTypes&&... types) constructor. Second
> users have been taught to use std::move since C++11, and now they would
> need to use std::mref in some circumstances...
>

Yes, I think if this constructor were to be provided it would need to take
a tag argument as first parameter, for disambiguation.

Another workaround would be to add a relocating_wrapper<T> that wraps a
std::optional<T> and has a one-shot conversion operator to T:
    T relocating_wrapper<T>::operator T() && { return opt_.pop(); }
Users would then write std::tuple<std::string, RelocOnlyTp, int>(reloc a,
std::relocating_wrapper(reloc b), reloc c) and it would work... sort of.

A rather more ambitious possibility would be to invent a new type of
deduction for perfect forwarding:
    std::tuple::tuple(decltype(auto)... args); // args deduced to decltype
of arguments so T, T&, T&& dependent on value category
This currently parses and is rejected at the semantic level, so it's "free
real estate", so to speak. There may be other available syntaxes, of course.

Received on 2022-09-19 21:28:36