template<typename ...Args)
func(Args&& ...pack)
{
auto rev_agg = ...; //Some reverse-aggregate.
auto test1 = make_tuple(func(rev_agg)...); //Compile error; nothing in the expression is a value sequence.
auto test2 = make_tuple(func([*]rev_agg)...); //Success. Treats `rev_agg` as though it were a parameter pack.
auto test3 = make_tuple(func(rev_agg, pack)...); //Success. `rev_agg` gets no special treatment. Every call to `func` gets the same `rev_agg`, just as it would in current code.
auto test4 = make_tuple(func([*]rev_agg, pack)...); //Success. Unpacks both `rev_agg` and `pack`. Only works if the two are the same size, just like for two parameter packs.
}
auto ...stuff = {func([*]rev_agg)...};
auto {x, y, z} = {func([*]rev_agg)...};
auto (w, u, v} = {pack...};
<code>
<auto... t> = make_tuple(foo()...);
</code>
I decided to put the proposal on ice until the structured bindings proposal has matured.
tuple<int, float> f = ...
tuple<bool, std::string> g = ...
auto ...thing1 = func([*]f, [*]g)...;
auto ...thing2 = func([*]f, g)...;
auto ...thing3 = func(f, [*]g)...;
auto ...thing4 = func(f, g)...;
auto... name = <value sequence expression>
sizeof...(<value sequence>)
<value sequence expression>...
auto {x, y, z} = args; //Function parameter pack decomposed into 3 elements.
auto {x, y, z} = unpackable; //Unpackable type stored via normal structured binding.
auto {x, y, z} = [*]unpackable; //Value sequence stored via structured binding. Identical to previous statement.
auto {x, y, z} = {func([*]rev_agg)...};
auto {x, y, z} = func([*]rev_agg);
On 2016-01-19 10:40, Nicol Bolas wrote:
> auto ...thing1 = func([*]f, [*]g)...;
I alluded to this in my other reply, but I think it bears repeating...
what does this (RHS) mean?
- { func(f<0>, g<0>), func(f<1>, g<1>) }
- { func(f<0>, f<1>, g<0>), func(f<0>, f<1>, g<1>) }
- { func(f<0>, g<0>, g<1>), func(f<1>, g<0>, g<1>) }
- func(f<0>, f<1>, g<0>, g<1>)
I could legitimately want *any* of those.
Conflating concepts (template
argument packs and value sequence unpacking of tuple-likes) makes it
impossible to specify which. Not to mention that I'm not convinced the
syntax isn't overly obtuse.
If, instead, we have apply_each (and zip), I can be precise:
- apply_each([](auto p){return func([*]p);}, [*]zip(f, g))
- apply_each([](auto v){return func([*]f, v);}, [*]g)
- apply_each([](auto v){return func(v, [*]g);}, [*]f)
- func([*]f, [*]g)
Now there is no ambiguity as to my intent.
On 2016-01-19 11:44, Nicol Bolas wrote:
> On Tuesday, January 19, 2016 at 11:31:31 AM UTC-5, Matthew Woehlke wrote:
>> On 2016-01-19 10:40, Nicol Bolas wrote:
>>> auto ...thing1 = func([*]f, [*]g)...;
>>
>> I alluded to this in my other reply, but I think it bears repeating...
>> what does this (RHS) mean?
>>
>> - { func(f<0>, g<0>), func(f<1>, g<1>) }
>> - { func(f<0>, f<1>, g<0>), func(f<0>, f<1>, g<1>) }
>> - { func(f<0>, g<0>, g<1>), func(f<1>, g<0>, g<1>) }
>> - func(f<0>, f<1>, g<0>, g<1>)
>>
>> I could legitimately want *any* of those.
>
> And my syntax can provide *all* of them:
>
> func([*]f, [*]g)...;
> func([*]f..., [*]g)...;
> func([*]f, [*]g...)...;
> func([*]f..., [*]g...);
Why do I have to unpack it *twice*? Ugh...
> [...] lots and lots of heinous syntax.
I find your proposal much too complicated and severely lacking in
rationale why we need such as a *language* feature. (I have yet to see
you offer a rationale, besides "hey, wouldn't it be great if...".)
auto test4 = make_tuple(func([*]rev_agg, pack)...);
auto zipped = std::zip(rev_agg, make_tuple(pack...));
auto test4 =
std::apply_each([](auto pair){ return func([*]pair); }, zipped);
auto test4 = make_tuple(func([*]rev_agg, forward<Args>(pack))...);
On 2016-01-19 14:15, Nicol Bolas wrote:
> Your mechanism is too simplified to be of *general* use.
I disagree. I can think of immediate examples of unpacking in place (and
some have been given). I would like to see a reasonable example for:
auto result = make_tuple(func([*]f..., [*]g)...);
...along with a rationale why this merits a language feature.
p.s. If we have genuine promotion of value sequences to parameter packs,
why can't we do this?
auto&&... g_unpacked = g;
auto result = make_tuple(func([*]f, g_unpacked)...);
> ... The *only part* of your proposal that absolutely needs to be a
> *language* feature is the ability to unpack a tuple into a
> braced-init-list.
I think you're severely understating the usefulness of this case. I
don't feel that being able to unpack into e.g. constructor calls is so
rare as to be so lightly glossed over.
> Oh, and the temporary in this case must be named. Weren't you one of those
> who were saying that naming things is hard
Naming *API* is hard :-). I dub the temporary 't'. Or 't0'. Or whatever;
the names of temporar^Wlocal scratch variables don't matter the way the
names of API types do.
On 2016-01-20 01:50, Arthur O'Dwyer wrote:
> Aren't you both (Nicol and Matthew) proposing language features? Matthew is
> proposing `[*]`, and Nicol is proposing `[*]...`. I don't see how either
> of those can be implemented as a library-only feature.
Yes, to all points. I feel like I've provided better examples, however,
and that the specification of my version is significantly simpler. Maybe
if Nicol will present a specification of his version, I will change my
mind, but it still seems to me that his is more complicated and harder
to use.
> Notice that in the previous parenthetical I had to write std::tuple_size_v<decltype(std::tie([*]x))>
> - 1 to express the size of the "comma-separated sequence" [*]x under
> Matthew's proposal. I think that's really the simplest thing that would
> compile!
Uh... why? What's wrong with std::tuple_size_v(x)? Recall that in the
discussions of expanding the notion of "tuple-like", it's generally been
implied that tuple_size would be defined, in addition to get<N>.
> On Tuesday, January 19, 2016 at 12:23:25 PM UTC-8, Matthew Woehlke wrote:
>> p.s. If we have genuine promotion of value sequences to parameter packs,
>> why can't we do this?
>>
>> auto&&... g_unpacked = g;
>> auto result = make_tuple(func([*]f, g_unpacked)...);
>
> Because you can't have variables of parameter-pack type. Parameter packs
> aren't first-class citizens in C++.
I think you missed the point; that is exactly what I meant by "If we
have genuine promotion of value sequences to parameter packs". Note the
"auto..." syntax.
> - should tuples be convertible back into packs, or into some other kind of
> entity?
I'm not proposing "some other kind of entity". I'm proposing an
immediate code (AST?) transform at point of use. Introducing some new
(or even existing) "other kind of entity" is specifically something I
want to *avoid*.
> The questions of "why can't I declare a pack on the stack", or "why can't I
> init-capture a pack", or "why can't I declare a class A<T...> that has
> members of types T..." are all good questions, but are completely
> orthogonal to the two questions above. And they'll still be there for the
> solving later.
But that's somewhat the point... the question is, if we want the ability
as in the above example *anyway*, then what value is left in Nicol's
proposal over mine (besides that it saves a little typing)?
This would
make the common case (just want to unpack in place) easy and the
uncommon case possible, vs. making both possible, but also both ugly.
Anecdotal, of course, but I can't think that I have *ever* wanted to
invoke a function for each member of a tuple-like.
That suggests that they're rare
On 2016-01-20 01:50, Arthur O'Dwyer wrote:
> (Matthew doesn't really mean that literally anyway, unless he's proposing
> to allow
>
> [*]x;
The non-complicated version of the proposal would indeed allow this; it
would expand to `get<0>(x), get<1>x, ..., get<N>(x);`, which is valid
code. Silly, but valid.
> Notice that in the previous parenthetical I had to write std::tuple_size_v<decltype(std::tie([*]x))>
> - 1 to express the size of the "comma-separated sequence" [*]x under
> Matthew's proposal. I think that's really the simplest thing that would
> compile!
Uh... why? What's wrong with std::tuple_size_v(x)? Recall that in the
discussions of expanding the notion of "tuple-like", it's generally been
implied that tuple_size would be defined, in addition to get<N>.
> On Tuesday, January 19, 2016 at 12:23:25 PM UTC-8, Matthew Woehlke wrote:
>> p.s. If we have genuine promotion of value sequences to parameter packs,
>> why can't we do this?
>>
>> auto&&... g_unpacked = g;
>> auto result = make_tuple(func([*]f, g_unpacked)...);
>
> Because you can't have variables of parameter-pack type. Parameter packs
> aren't first-class citizens in C++.
I think you missed the point; that is exactly what I meant by "If we
have genuine promotion of value sequences to parameter packs". Note the
"auto..." syntax.
> The questions of "why can't I declare a pack on the stack", or "why can't I
> init-capture a pack", or "why can't I declare a class A<T...> that has
> members of types T..." are all good questions, but are completely
> orthogonal to the two questions above. And they'll still be there for the
> solving later.
But that's somewhat the point... the question is, if we want the ability
as in the above example *anyway*, then what value is left in Nicol's
proposal over mine (besides that it saves a little typing)? This would
make the common case (just want to unpack in place) easy and the
uncommon case possible, vs. making both possible, but also both ugly.
Okay, having said that, I *can* think of one case... can you show how
you would simplify this code?
stream << "begin" << get<0>(x) << get<1>(x)
<< ... << get<N>(x) << "end";
(Rules: 'stream' must be named exactly once, and no fair creating a
helper function. Strong preference to avoid assigning 'stream' to a
scratch variable.)
For bonus points, include `", "` between each item of `x` (okay to
include after the last item also).
((stream << "begin, ") , ... , (stream << x~ << ", ")) << "end";
On Wednesday, January 20, 2016 at 6:41:58 PM UTC-5, Arthur O'Dwyer wrote:template<class TupleLike>void print_to_stream(std::ostream& stream, const TupleLike& x){auto commaize = [](auto&& x) {return [&x](std::ostream& out) -> std::ostream& { return out << x << ", "; };};(stream << "begin, " << ... << commaize(x~)) << "end";}
If you can write stream twice, then a comma fold will do:
((stream << "begin, ") , ... , (stream << x~ << ", ")) << "end";Also, I'm fairly sure you need to parenthesize stream << "begin, ". The grammar only allows a cast-expression there.
On 2016-01-20 18:13, Arthur O'Dwyer wrote:
> Off-topic but FYI, I'm still a little generally uncomfortable with the idea
> of "tuple-like type". For example, in C++14 std::array<T,N> has become
> "tuple-like", even though it has only one template type parameter.
So? The intent of "tuple-like" is that it is a sequence of a fixed
number of values.
> If we were to standardize a "typelist" template (e.g.
> mpl::vector<T...>), would it *not* be "tuple-like" because you
> couldn't use std::get<I> on it at runtime?
Yeees.... also because tuple_size wouldn't (couldn't) be constexpr. IOW,
it is not "tuple-like" if it is not fixed(-at-compile-time)-size.
class tuple_size< mpl::vector<Types...> >
This is of course why std::array is tuple-like and std::vector is not.
> Using "..." to unpack things isn't the ugly part of the proposal IMHO,
> though!
> The ugly part, and the part that's going to get bikeshedded to death, is
> the choice of [*] as a prefix operator.
> Especially since there's another proposal
> <http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0018r0.html>
> before the Committee (or was, at Kona) suggesting that [*](){ ... } should
> be the syntax for "capture *this by copy".
I'm open to other (good) ideas... one reason I really like `[*]` however
is because it becomes natural to further extend it to slicing, e.g.
`[1]`, `[:3]`, `[2:4]`, `[1,2:4,5,8:]`, and so forth.
I think slicing is important; already with assignment unpacking, we have
the problem of how to deal with unwanted values. If we have slicing, we
can just write:
auto {a, b} = [2,5]tuple_like;
Per above, `[*]` wasn't chosen lightly. The use of `[]`s suggest the
indexing operator, which therefore implies something being extracted
from a container (and are extensible to multiple specifications of
"something"). The use of `*` is taken directly from Python, and per *my*
proposal, means the exact same thing. (Nicol's would make it... similar,
but less "exactly the same".)
`~` "comes out of nowhere", if you will... there is no existing use in
any language that would lead one to associate it with unpacking.
> If we postulate a made-up my::tuple_foldl, though, the whole problem
> collapses away and we don't need any language features at all:
>
> template<class Vec>
> auto dotproduct(const Vec& a, const Vec& b)
> {
> auto f = [](auto&& accumulator, auto&& ab) {
> return accumulator + (ab.first * ab.second);
> };
> return my::tuple_foldl(f, 0, my::tuple_zip(a, b)));
> }
Isn't that the same as `std::experimental::apply`?
On 2016-01-25 11:25, Ville Voutilainen wrote:
> On 25 January 2016 at 17:32, Matthew Woehlke <mwoehlk...@gmail.com> wrote:
>> On 2016-01-20 18:13, Arthur O'Dwyer wrote:
>>> would [mpl::vector] *not* be "tuple-like" because you couldn't use
>>> std::get<I> on it at runtime?
>>
>> ...but then, I don't understand why that would be true?
>>
>> Back to that question, yes, the idea as I understand it is that
>> "tuple-like" <=> "can use get<N>". Why is that an issue?
>
> mpl::vector is a container of types, not values. get<N> is a value-getter.
Ah. Um... how would you then *use* an mpl::vector in any context that
you would use a tuple-like? A std::tuple is certainly a value
collection. By extension, a tuple-like is also a value collection.
class tuple_size< mpl::vector<Types...> >
// recursive case template< std::size_t I, class Head, class... Tail > struct tuple_element<I, mpl::vector<Head, Tail...>> : std::tuple_element<I-1, mpl::vector<Tail...>> { }; // base case template< class Head, class... Tail > struct tuple_element<0, mpl::vector<Head, Tail...>> { typedef Head type;
// please stifle all irrelevant comments re: overload sets and CV-qualifiers
auto get(const mpl::vector<Types...>&)
-> std::add_rvalue_reference_t<tuple_element_t<I,mpl::vector<Types...>>>;
// deliberately left with no implementation, a la std::declval
On 2016-01-25 14:37, Arthur O'Dwyer wrote:
> However, there's no such "obvious" definition for [mpl::vector] std::get.
>
> So what we're left with is: mpl::vector is "not tuple-like", but it *does*
> implement tuple_size_v and tuple_element_t.
> The only part of the "tuple-like" contract that it fails to have an obvious
> implementation for is the one part of the "tuple-like" contract *whose name
> doesn't involve the word "tuple"*.
> *That* is why the concept of "tuple-like" bothers me.
LOL :-). (Note: not at you, at the observed absurdity of the situation¹.)
Thanks for explaining; I understand now.
Per my earlier comment, I don't see how you would use an mpl::vector in
most of the contexts in which you care about something being
"tuple-like" (i.e. unpacking, conversion to/from std::tuple).
template<typename Typelist, typename ...Ts>
void func(Ts ... ts)
{
return make_tuple(Typelist~(std::forward<Ts>(ts))...);
}
template<typename Typelist>
void func(tuple<whatever> ts)
{
return make_tuple(Typelist~(ts~)...);
}
Ergo,
while I understand (and to an extent agree with) your concern now, I'm
also thinking that it may just be one of those unfortunate facts of life.
I think the problem is that we have a few related but
not-quite-identical concepts, all of which are being called "tuple" (and
all of which can make a reasonable argument that "tuple is the correct
term, darn it").
I am open to suggestions? (Maybe we just avoid using the actual phrase
"tuple-like" in the standard?)
I made the analogy that a tuple-like type is analogous to a non-type template parameter pack or a function parameter pack. Therefore, an `mpl::vector` is like a type template parameter pack. So:
template<typename Typelist, typename ...Ts>
void func(Ts ... ts)
{
return make_tuple(Typelist~(std::forward<Ts>(ts))...);
}
Transforms the `ts` parameters in accord with the `mpl::vector` typelist.
There needs to be a recognition between what is a tuple-like type and what is a tuple-like value.
You can access the number of elements in a tuple-like type, and you can access the types of each of those elements. But types obviously don't have values, so that is a third operation only applicable to values of tuple-like types.
On Mon, Jan 25, 2016 at 1:23 PM, Nicol Bolas <jmck...@gmail.com> wrote:There needs to be a recognition between what is a tuple-like type and what is a tuple-like value.
You can access the number of elements in a tuple-like type, and you can access the types of each of those elements. But types obviously don't have values, so that is a third operation only applicable to values of tuple-like types.I tend to disagree with the above statement, because I tend to disagree with any statement that treats "operations on types" and "operations on values" as fundamentally different, ever since I saw Louis Dionne speak for the first time. :)
As of C++11, we have decltype(E) — which takes the value E and produces its type — and we have std::declval<T>() — which takes a type T and produces a value of that type. And they compose, so I can do a bunch of "value-space" computations and then take the decltype of the resulting expression. Any construct that gets in the way of my doing that is objectionable.In other words, rather than saying that mpl::vector<Ts...> doesn't have a value and so std::get<I>(mplvec) should be ill-formed, I would prefer we simply say that std::get<I>(mplvec) doesn't have a value — but it should still have a type!
In short, I'd strongly prefer that every "typelist-like" type also be "tuple-like", at least from the point of view of the type system, even if that means leaving some "tuple-like operations" declared-but-not-defined.
Having said all that, this "postfix twiddle means std::get<> (and/or std::tuple_element_t<>)" thing still feels too ad-hoc to me.
If we could come up with a nice syntax for it, I'd prefer to have the std::get<> written out and just build a simple syntax for getting a parameter-pack of integer indices. I have no idea what that syntax would look like, though, or how it would end up remotely as clean as this seductive example:template<class Vec>auto dotproduct(const Vec& a, const Vec& b){return (... + (a~ * b~)); // this is a C++17 fold-expression}
The only thing I can think to do is to introduce first-class parameter packs, and writetemplate<size_t I, class... Ts>auto getp(const std::tuple<Ts...>&) -> Ts...; // "return a pack"template<class Vec>auto dotproduct(const Vec& a, const Vec& b){return (... + (getp(a) * getp(b)));}But first-class parameter packs seem like a nightmare to specify, don't they?
On Friday, January 29, 2016 at 2:21:00 PM UTC-5, Arthur O'Dwyer wrote:As of C++11, we have decltype(E) — which takes the value E and produces its type — and we have std::declval<T>() — which takes a type T and produces a value of that type. And they compose, so I can do a bunch of "value-space" computations and then take the decltype of the resulting expression. Any construct that gets in the way of my doing that is objectionable.In other words, rather than saying that mpl::vector<Ts...> doesn't have a value and so std::get<I>(mplvec) should be ill-formed, I would prefer we simply say that std::get<I>(mplvec) doesn't have a value — but it should still have a type!
You can want whatever you want, but the fact is that in C++, types and values are different things. And we have different mechanisms for dealing with each.
Functions take values and return them. Template metafunctions take types and can evaluate to types. `declval` doesn't really generate a value in any meaningful way, since it can only exist in a non-evaluated context. And while `decltype` does evaluate to a type, that type is not based on a value, since the expression itself is never actually evaluated.
Maybe sometimes these things will be equivalent someday. But until then, C++ is what it is. Values and types are different, and we shouldn't pretend that they're not.
Having said all that, this "postfix twiddle means std::get<> (and/or std::tuple_element_t<>)" thing still feels too ad-hoc to me.
It's no more ad-hoc than range-based for using `std::begin` and `std::end`. You have to have some customization point for user-defined types.
If we could come up with a nice syntax for it, I'd prefer to have the std::get<> written out and just build a simple syntax for getting a parameter-pack of integer indices. I have no idea what that syntax would look like, though, or how it would end up remotely as clean as this seductive example:template<class Vec>auto dotproduct(const Vec& a, const Vec& b){return (... + (a~ * b~)); // this is a C++17 fold-expression}
That example seems to be missing what the return value is supposed to be. Presumably it's some `Vec` object, but you don't use the typename anywhere.
The only thing I can think to do is to introduce first-class parameter packs, and writetemplate<size_t I, class... Ts>auto getp(const std::tuple<Ts...>&) -> Ts...; // "return a pack"template<class Vec>auto dotproduct(const Vec& a, const Vec& b){return (... + (getp(a) * getp(b)));}But first-class parameter packs seem like a nightmare to specify, don't they?
Um, how exactly is that easier than the user implementing the appropriate interfaces? It certainly isn't easier for the reader to understand.
On Fri, Jan 29, 2016 at 1:08 PM, Nicol Bolas <jmck...@gmail.com> wrote:On Friday, January 29, 2016 at 2:21:00 PM UTC-5, Arthur O'Dwyer wrote:As of C++11, we have decltype(E) — which takes the value E and produces its type — and we have std::declval<T>() — which takes a type T and produces a value of that type. And they compose, so I can do a bunch of "value-space" computations and then take the decltype of the resulting expression. Any construct that gets in the way of my doing that is objectionable.In other words, rather than saying that mpl::vector<Ts...> doesn't have a value and so std::get<I>(mplvec) should be ill-formed, I would prefer we simply say that std::get<I>(mplvec) doesn't have a value — but it should still have a type!
You can want whatever you want, but the fact is that in C++, types and values are different things. And we have different mechanisms for dealing with each.
Functions take values and return them. Template metafunctions take types and can evaluate to types. `declval` doesn't really generate a value in any meaningful way, since it can only exist in a non-evaluated context. And while `decltype` does evaluate to a type, that type is not based on a value, since the expression itself is never actually evaluated.
Maybe sometimes these things will be equivalent someday. But until then, C++ is what it is. Values and types are different, and we shouldn't pretend that they're not.That's one point of view, but I tend to think that it's outdated (as of C++11). We don't need template metafunctions anymore (well, not so many of them),
Values and types are different, but they are still intimately related (for example, every value has a type), and while we can use different mechanisms to deal with each, we no longer need to.
decltype(get<I>(declval<TypeSeq>()))
tuple_element_t<TypeSeq, I>
Having said all that, this "postfix twiddle means std::get<> (and/or std::tuple_element_t<>)" thing still feels too ad-hoc to me.
It's no more ad-hoc than range-based for using `std::begin` and `std::end`. You have to have some customization point for user-defined types.Agreed, I think, grudgingly, reluctantly, in practice. In other words, I agree that's the current state of the world, but I don't like it. It feels much too ad-hoc. begin and end "work" as well as they do because they had a couple of decades of implementation experience behind them before people started making up syntactic sugar for them. Right now we have maybe 5 years of experience using std::get and probably even less with std::tuple_element_t. Are those exactly the primitives we want to syntactic-sugar up, or will we regret the decision in another 5 years when we realize "how tuples should have been done"?
I mean, imagine if instead of iostreams using operator overloading, we had gotten a syntactic sugar where << in certain contexts expanded to a call to std::printf(). Makes sense, right? But it would have been horrible for the flexibility and symmetry of the language as a whole.
The only thing I can think to do is to introduce first-class parameter packs, and writetemplate<size_t I, class... Ts>auto getp(const std::tuple<Ts...>&) -> Ts...; // "return a pack"template<class Vec>auto dotproduct(const Vec& a, const Vec& b){return (... + (getp(a) * getp(b)));}But first-class parameter packs seem like a nightmare to specify, don't they?
Um, how exactly is that easier than the user implementing the appropriate interfaces? It certainly isn't easier for the reader to understand.My idea was that std::getp would probably be standardized as the "packful" version of std::get, so the user wouldn't have to actually write out the implementation of getp in practice.I agree that the syntax inside this version of dotproduct is much more off-putting than the postfix-twiddle syntax. It is uglier. However, it is conceptually cleaner, because it doesn't introduce any significantly new grammar (no postfix-twiddle operator)
and it doesn't introduce any new "magic names" (no hard-coding of the names get and tuple_element_t into the compiler). This is a trade-off, and I'm honestly not sure which way would be better for the language. It's quite possible (IMHO) that the best course of action for now would be not doing either one.
From: Arthur O'Dwyer Sent: Monday, January 25, 2016 2:37 PM Reply To: std-pr...@isocpp.org Subject: Re: [std-proposals] Re: RFC: Unpacking tuples to value sequences |
On 2016-01-30 11:33, Nicol Bolas wrote:
> On Friday, January 29, 2016 at 8:59:22 PM UTC-5, Arthur O'Dwyer wrote:
>> My idea was that std::getp would probably be standardized as the
>> "packful" version of std::get, so the user wouldn't have to actually
>> write out the implementation of getp in practice.
>> I agree that the syntax inside this version of dotproduct is much more
>> off-putting than the postfix-twiddle syntax. *It is uglier.* However, it
>> is *conceptually cleaner*, because it doesn't introduce any significantly
>> new grammar (no postfix-twiddle operator)
>
> It does introduce new grammar; just not at the cite of *use*. You have to
> have new grammar to have multiple return values.
Not only that, but you're no longer returning a tuple-like, you're
returning *an actual parameter pack*. This implies, by extension, [...]
all sorts of interesting implications...
what is `decltype(x)`?
Can I pass `x` as a "single" parameter to a function? Can
I pass multiple packs as distinct entities (i.e. and still know on the
other side what belongs to which pack)?
>> and it doesn't introduce any new "magic names" (no hard-coding of the names
>> get and tuple_element_t into the compiler).
...which is completely irrelevant. In light of P0144, we *already have
that*. Inventing a new mechanism to do *the exact same thing*¹ is idiocy.