Date: Mon, 19 Sep 2022 11:41:22 +0200
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.
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};
}
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...
*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.
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...
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.
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};
}
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...
*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.
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...
Received on 2022-09-19 09:41:35