On Tue, 20 Sept 2022 at 09:54, Sébastien Bini <sebastien.bini@gmail.com> wrote:
Hi all,

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?

Indeed... I am not in favor of expanding arrays, as the other subobjects are not automatically expanded. Maybe pass them as std::array? Not a fan of this either... Your std::decompose is more promising.
 
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.

Yes.

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.

This looks good, but how are you able to mix type (B) and non-type (&D::x) parameters in the function declaration?

Oh, cheating. Actually, it looks like there's some renewed momentum on P1985 Universal Template Parameters, so that would help here. Otherwise the non-type parameters could be passed by function argument, although that's less elegant.

One annoying thing about this is that there's no proof that the caller has the right to access base B of D. We could mandate that by fiat, but it'd be nicer if there were a way to denote a specific base-of-derived, e.g. with the same syntax as a pointer to data member. That would be a lot more work, though, so just mandating that the base be accessible and unambiguous in the context of the caller might have to do.

If we have such a function, then we can mix it with a get/get_bindings API, keeping the recursive bit:

auto get_bindings(E e)
{
    auto [base, x, arr] = std::decompose<B, &D::_x, &D::_arr>(reloc e);
    auto [a1, a2] = reloc arr;
    return std::tuple(reloc base, reloc x, reloc a2);
}

That also allows passing unaccessible (private) members or bases to std::decompose.

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.

Sure.
 
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.

That could also do. But here we introduce a reloc_wrapper which leaves an open door for bugs (operator T() called twice). It also has the cost of a double relocation (once inside the wrapper, once out).

If `operator T()` were prvalue qualified that would help prevent bugs, using deducing this on the object parameter:

    T relocating_wrapper<T>::operator T(this relocating_wrapper self) { return self.opt_.pop(); }

Users would have to go to considerable lengths to misuse it and the resulting code would have a manifest use-after-move bug. Yes, there's still a double relocation but that should be elidable.

On the other hand, with: `template <class... UTypes> tuple(relocate_tag, UTypes... types)`: nothing is relocated twice. We introduce a wrapper as well (mref), but it's a safe one. And even if std::move is used by mistake instead of std::mref then thanks to the tag the right constructor is still picked, and a temporary is created (suboptimized but harmless).

Yes, that's also fine.

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.

This would be a solution, but then how do we forward the parameters? We don't have a `reloc_of_forward` function. Even if we manage to make one (new reloc... operator?), then we will end up with the prvalue parameters being destructively relocated, while the others just got their reference passed along. This makes the prvalues no longer usable (as somehow they are passed to reloc), which in turn should make the whole argument pack unusable.

Well, this is a good argument for `reloc` to DTRT on references: that is, `reloc r` should behave on reference typed variables r as static_cast<decltype(r)>(r) while ending the lifetime of the reference and removing it from scope.

I believe there are things to be done in that area, but maybe for a second proposal? This one is already large enough, and there is no strong need to do that now (AFAICT), as we can get around with std::mref or std::reloc_wrapper.

Fair enough, though I think it'd be worth mentioning in a "further directions" section.