Core Language feature: Multiple assignments from multiple return values via tuple

558 views
Skip to first unread message

Matthew Fioravante

unread,
May 20, 2015, 9:14:30 PM5/20/15
to std-pr...@isocpp.org
So this idea came out of the number parsing thread. Many languages like python have support for functions returning multiple values.

In C++, one option we have currently is this:

std::tuple<int,float,double> foo();

int x;
double d;
std
::tie(x, std::ignore, d) = foo();

This works but it has a lot of problems:
- We have to declare the receiving variables beforehand. This means they have to be default constructed and then assigned and we cannot use auto.
- Its overly verbose and clumsy to use. First we have to declare the variables, then we need 8 character std::tie and 11 character std::ignore.
- tuple and tie are not as well known to beginners. A "standard practice" for capturing multiple values would be better as a core language feature than a library feature.

So one solution would be to invent a new syntax:
int x;
[x; void; auto d] = foo();


Which is the equivalent of:
int x;
auto _t = foo();
x
= std::get<0>(std::move(_t));
auto d = std::get<2>(std::move(_t));

Each element in this "assignment list" corresponds to a element of a tuple of the right hand side of the expression. Each element can be one of:
- An lvalue expression: the resulting lvalue is assigned with operator=() to the corresponding tuple element.
- void: the corresponding tuple element is ignored
- A local variable declaration. A local variable is created and constructed with the corresponding tuple element.

I've chosen to use [] instead of {} because so far braces always introduce a scope and in this example, d which is created inside the braces actually lives in the outer scope.
I've chosen to use ; instead of , to avoid confusion with the comma operator.

We can also allow the user to specify a shorter list, with default values of void for the trailing elements not mentioned:

[auto x] = foo(); //same as [auto x, void, void] = foo()

What do you think about such a feature?

Morwenn

unread,
May 21, 2015, 3:49:44 AM5/21/15
to std-pr...@isocpp.org
While I don't really like the syntax (it's hard to make something short when you have to declare a type anyway), I want to say that I really much like the use of std::get to get the parameters. That means that any type compatible with std::get, such as std::pair or std::array, could be used for multi-valued return types. This is IMO better than forcing users to use std::tuple everywhere and make some standard library functions returning std::pair work with the syntax out of the box.

There have already been a number of discussions about such a feature by the way. I bet that you can find some in this very mailing list.

Péter

unread,
May 26, 2015, 10:47:41 AM5/26/15
to std-pr...@isocpp.org
I'd also like to see this feature added to c++.  I don't really like the proposed syntax either (the square brackets are out-of-place here, imo). The best would be to simply use 

int x;
(x; void; auto d) = foo();

but I guess that'd introduce parsing issues.  How about:

int x;
auto (x; void; auto d) = foo();

But even the proposed square-bracket syntax is better than nothing, so good luck!


Matthew Woehlke

unread,
May 26, 2015, 12:09:27 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-20 21:14, Matthew Fioravante wrote:
> I've chosen to use [] instead of {} [...]

Given the syntax comments, I would encourage you to read the previous
thread(s). That said, I have to say I'm actually in favor of the []
syntax; it "feels" least likely to have parsing issues, and [] normally
being indexing operators, it makes sense to me to use them for
unpacking. (It probably also helps that I write the odd bit of Python,
and Python uses [] to write lists :-).)

> I've chosen to use ; instead of , to avoid confusion with the comma
> operator.

I strongly agree with using ';' to separate elements, since multiple
declarations should be permitted, but '[auto x, auto y]' clearly makes
no syntactic sense. That said, is ',' allowed inside the []'s at all? If
yes, what does it mean?

[auto x, y] = bar(); // forces decltype(x) == decltype(y)?

> We can also allow the user to specify a shorter list, with default values
> of void for the trailing elements not mentioned:
>
> [auto x] = foo(); //same as [auto x, void, void] = foo()

I'd be somewhat strongly inclined to follow Python, and not discard any
values. So the last item in the list becomes a tuple of whatever is left
over.

So, to take only the first value, you would write:

[auto x; void] = foo();

I also wonder if it should be an error if the number of arguments don't
match, and require '...' to allow an item to expand to a (partial)
tuple. For example:

// assume foo() -> tuple<int, double, int>
[auto x; void...] = foo(); // like above
[auto x; auto... y] = foo(); // decltype(y) == tuple<double, int>

The down side of course is that this (except the 'void...' case)
requires the compiler to know how many items are in the result. For
tuple this is probably easy. For "generic" types it may be harder, but I
imagine if necessary we could introduce a companion to std::get to
return the largest index that can be used with std::get, if we don't
already have ways to accomplish that.

> What do you think about such a feature?

As you may recall from (at least one of) the time(s) this came up
previously, I am in favor of this :-).

This may be implied anyway, but I'll say it explicitly; let's please
also support this in range-based for:

for ([auto key; auto value] : some_map) { ... }

--
Matthew

Miro Knejp

unread,
May 26, 2015, 12:51:01 PM5/26/15
to std-pr...@isocpp.org
Just an FYI: there might be a conflict with the Objective-C language, specifically the Objective-C++ dialect, where "[x foo]" is the member function calling syntax. Something to keep in mind as I’d be very surprised if there weren’t committee members with an interest to keep C++ compatible to said dialect.
> --
>
> ---
> You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
> To post to this group, send email to std-pr...@isocpp.org.
> Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Nicol Bolas

unread,
May 26, 2015, 2:10:11 PM5/26/15
to std-pr...@isocpp.org
If we're going to have special syntax for using multiple return values, I would much rather we have actual multiple return value syntax, not something built on `tuple`.

Using the `[]` syntax noted here, you could have matching return and capture:

[int,float,double] foo()
{
 
return [5; 23.4f; 10447.023d];
}

double v{};
[auto x, float g, v] = foo();

The reason to want this to be a legitimate language feature is so that we can have all of the aspects of return values still apply to returning multiple values. Specifically, there's no reason we shouldn't be able to have copy/move elision just because you want to have a function return multiple values.

Matthew Fioravante

unread,
May 26, 2015, 2:11:49 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Thursday, May 21, 2015 at 3:49:44 AM UTC-4, Morwenn wrote:
While I don't really like the syntax (it's hard to make something short when you have to declare a type anyway), I want to say that I really much like the use of std::get to get the parameters. That means that any type compatible with std::get, such as std::pair or std::array, could be used for multi-valued return types. This is IMO better than forcing users to use std::tuple everywhere and make some standard library functions returning std::pair work with the syntax out of the box.

This makes sense, but has some pitfalls when combined with Matthew W.'s ideas (see below).


On Tuesday, May 26, 2015 at 10:47:41 AM UTC-4, Péter wrote:
I'd also like to see this feature added to c++.  I don't really like the proposed syntax either (the square brackets are out-of-place here, imo). The best would be to simply use 

int x;
(x; void; auto d) = foo();


While I agree with you that this syntax seems better, I'm not a C++ parsing expert and I don't know if such a syntax would be possible. If it is, I would probably prefer it also over [].
 
but I guess that'd introduce parsing issues.  How about:

int x;
auto (x; void; auto d) = foo();


Not sure this one is a good idea as it seems like a hijacking of the auto keyword. auto is always used to declare a variable (or template parameter if some new proposals are accepted). The () expression itself is not a local variable so I don't think auto is appropriate.

This example doesn't create any new variables even though auto is used.
int x;
int y;
auto (x; y; void) = foo();


On Tuesday, May 26, 2015 at 12:09:27 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-20 21:14, Matthew Fioravante wrote:
> I've chosen to use [] instead of {} [...]

Given the syntax comments, I would encourage you to read the previous
thread(s). That said, I have to say I'm actually in favor of the []
syntax; it "feels" least likely to have parsing issues, and [] normally
being indexing operators, it makes sense to me to use them for
unpacking. (It probably also helps that I write the odd bit of Python,
and Python uses [] to write lists :-).)

But python also uses () to define tuples. The () syntax to me seems more natural but again I don't know it its possible with the current parsing rules.
 

> I've chosen to use ; instead of , to avoid confusion with the comma
> operator.

I strongly agree with using ';' to separate elements, since multiple
declarations should be permitted, but '[auto x, auto y]' clearly makes
no syntactic sense. That said, is ',' allowed inside the []'s at all? If
yes, what does it mean?

  [auto x, y] = bar(); // forces decltype(x) == decltype(y)?

It should be equivalent to:

auto t = bar();
auto x, y = std::get<0>(t);

Which is currently an error because x is not initialized.

However you could do this:

[int x, y;void;void] = foo();

Which when decomposed like the last example compiles (but may not give you what you want).

You could have some horrible thing like this, which would be allowed:

int& operator,(int& a, int& b) { return a; }

int x;
int y;
[ x,y; void; void] = foo(); //assignes std::get<0>(foo()) to x

 

> We can also allow the user to specify a shorter list, with default values
> of void for the trailing elements not mentioned:
>
> [auto x] = foo(); //same as [auto x, void, void] = foo()

I'd be somewhat strongly inclined to follow Python, and not discard any
values. So the last item in the list becomes a tuple of whatever is left
over.

So, to take only the first value, you would write:

  [auto x; void] = foo();

I also wonder if it should be an error if the number of arguments don't
match, and require '...' to allow an item to expand to a (partial)
tuple. For example: 

  // assume foo() -> tuple<int, double, int>
  [auto x; void...] = foo(); // like above
  [auto x; auto... y] = foo(); // decltype(y) == tuple<double, int>


I think this is better as its more explicit and ... is not too verbose. We should also support support empty list as I believe this should be useful for generic code. You can extract one parameter and pass along the others.

tuple<int,int> foo();

[auto x; auto y; auto... z] = foo();
//decltype(z) is tuple<>.

This feature conflicts with allowing this syntax to work with any std::get compatible type. What type should auto... z be?

tuple<int,int> a();
array
<int,2> b();
pair
<int,int> c();

[auto xa; auto xva] = a(); //decltype(xva) is tuple<int>?
[auto xb; auto xvb] = b(); //decltype(xvb) is array<int,1>?
[auto xc; auto xvc] = c(); //What is decltype(xvc)???

Here is another generic code use case which is trickier if we are only based on std::get().
Imagine  we want to extract the second parameter, and pass long a concatenated list of the first and the tail.

[auto first; auto second; auto... tail] = foo();
use(first);
auto params = concat({ first }, tail)
bar
(params)

Such a thing is possible for tuple and array but not pair.
 
The down side of course is that this (except the 'void...' case)
requires the compiler to know how many items are in the result. For
tuple this is probably easy. For "generic" types it may be harder, but I
imagine if necessary we could introduce a companion to std::get to
return the largest index that can be used with std::get, if we don't
already have ways to accomplish that.

This language feature is built ontop of std::get<> which itself is designed for container types where we can dereference elements by index at compile time. Calling std::get with an invalid index is a compiler error and thus it should be possible to determine the valid index range at compile time using SFINAE tricks.


> What do you think about such a feature?

As you may recall from (at least one of) the time(s) this came up
previously, I am in favor of this :-).

This may be implied anyway, but I'll say it explicitly; let's please
also support this in range-based for:

  for ([auto key; auto value] : some_map) { ... }

This is one major use case of such a feature and hopefully it should fall out automagically without any special provisions for it.

Matthew Fioravante

unread,
May 26, 2015, 2:21:31 PM5/26/15
to std-pr...@isocpp.org


On Tuesday, May 26, 2015 at 2:10:11 PM UTC-4, Nicol Bolas wrote:
If we're going to have special syntax for using multiple return values, I would much rather we have actual multiple return value syntax, not something built on `tuple`.

Using the `[]` syntax noted here, you could have matching return and capture:

[int,float,double] foo()
{
 
return [5; 23.4f; 10447.023d];
}

double v{};
[auto x, float g, v] = foo();


I'd probably still use braces in your return statement for initialization.

[int,float,double] foo()
{
 
return {5, 23.4f, 10447.023d};
}

double v{};
[auto x; float g; v] = foo();

The idea is that [int,float,double] is shorthand for some type (maybe tuple)  and in the return statement you are just initializing it using the same old initialization rules. If we allow return [x;y;z...] then we have to define all kinds of rules for how this yet another initialization syntax works.
 
The reason to want this to be a legitimate language feature is so that we can have all of the aspects of return values still apply to returning multiple values. Specifically, there's no reason we shouldn't be able to have copy/move elision just because you want to have a function return multiple values.

Your proposed type specification syntax could just be syntactic sugar for tuple. This would automatically give support to any interface that already returns a tuple and generic code that works with tuples. 

Is there any reason why [int,float,double] should be a new type different than tuple<int,float,double>?

//syntactic sugar for tuple<int,float,double> foo()
[int,float,double] foo();

//Extract the first value, and store the tail in a tuple<float,double>
[auto x; auto... yz] = foo();
//Extract the values of yz later.
[auto y; auto z] = yz;

//Construct a tuple directly using new syntax
//Different from lambda because the [] contains only a list of types, not variable names
auto xyz = [int,float,double]();



Matthew Woehlke

unread,
May 26, 2015, 2:24:22 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 14:10, Nicol Bolas wrote:
> If we're going to have special syntax for using multiple return values, I
> would much rather we have actual multiple return value syntax, not
> something built on `tuple`.
>
> The reason to want this to be a legitimate language feature is so that we
> can have all of the aspects of return values still apply to returning
> multiple values. Specifically, there's no reason we shouldn't be able to
> have copy/move elision just because you want to have a function return
> multiple values.

Is there a reason this is not possible with returning std::tuple? Is
there a reason we should not support unpacking of e.g. std::pair,
std::tuple as return values? (In particular I would like std::pair
unpacking for the sake of API's that already return a std::pair, where
unpacking would make sense. Like std::map::iterator.)

Put differently, is there a reason this needs to be part of the initial
proposal rather than a follow-on proposal? (That said, I *would* be in
favor of a simplified mechanism for returning multiple values;
std::tuple is... aesthetically challenged :-).)

--
Matthew

Matthew Fioravante

unread,
May 26, 2015, 2:29:43 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 2:24:22 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-26 14:10, Nicol Bolas wrote:
> If we're going to have special syntax for using multiple return values, I
> would much rather we have actual multiple return value syntax, not
> something built on `tuple`.
>
> The reason to want this to be a legitimate language feature is so that we
> can have all of the aspects of return values still apply to returning
> multiple values. Specifically, there's no reason we shouldn't be able to
> have copy/move elision just because you want to have a function return
> multiple values.

Is there a reason this is not possible with returning std::tuple? Is
there a reason we should not support unpacking of e.g. std::pair,
std::tuple as return values?

std::pair<X,Y> is convertible to std::tuple<X,Y>, so we could allow the syntax to perform a conversion

std::pair<int,int> foo();
[auto x; auto y] = foo();

//is like
auto p = foo();
std
::tuple<int,int> t = std::move(p);
auto x = std::get<0>(t);
auto y = std::get<1>(t);



 
(In particular I would like std::pair
unpacking for the sake of API's that already return a std::pair, where
unpacking would make sense. Like std::map::iterator.)

Put differently, is there a reason this needs to be part of the initial
proposal rather than a follow-on proposal? (That said, I *would* be in
favor of a simplified mechanism for returning multiple values;
std::tuple is... aesthetically challenged :-).)

I agree std::tuple is ugly is somewhat non-obvious for beginners. Having a syntax for defining multiple return functions and also capturing the values would probably be the way to go.
 

Thiago Macieira

unread,
May 26, 2015, 2:34:23 PM5/26/15
to std-pr...@isocpp.org, Nicol Bolas
On Tuesday 26 May 2015 11:10:11 Nicol Bolas wrote:
> Using the `[]` syntax noted here, you could have matching return and
> capture:
>
> [int,float,double] foo()
> {
> return [5; 23.4f; 10447.023d];
> }

I think the matching return and capture is a very important point, since a
lambda and this return are both basically anonymous structs.

return [x = 5, y = 10] {}

If it were possible to extract the captured variables, we'd have half the job
done.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Matthew Fioravante

unread,
May 26, 2015, 2:47:42 PM5/26/15
to std-pr...@isocpp.org, jmck...@gmail.com


On Tuesday, May 26, 2015 at 2:34:23 PM UTC-4, Thiago Macieira wrote:

I think the matching return and capture is a very important point, since a
lambda and this return are both basically anonymous structs.

        return [x = 5, y = 10] {}

If it were possible to extract the captured variables, we'd have half the job
done.

I'm not sure returning an anonymous type is actually desirable at all for this feature. Lambda types are anonymous because they have a function call operator that can do whatever and the type system cannot compare the behavior of 2 lambdas operator() to prove they behave identically. An [int,float,double] on the other hand does not need to be anonymous because there is no ambiguity. Its just a type list like container holding these 3 objects.

Anonymous types come with a lot of baggage
- We have to use auto as the return type of the function, which for a large function can reduce readability.
- Any function returning multiple values will need to be written in the header file, no out of line functions allowed. (this is really bad imo)
- The function body can only have 1 return statement, as each invocation returns a different anonymous type.

Is there any feature of an anonymous type that is actually beneficial?

A proposal that just defines a syntax sugar for defining a tuple type and a syntax sugar for generating optimal code to extract values from tuple would be simple to understand and give us the multiple return value idiom as first class citizens in C++.

Miro Knejp

unread,
May 26, 2015, 2:49:38 PM5/26/15
to std-pr...@isocpp.org
The question here is really whether you want
1. a syntax and language support for multiple return values
or
2. a destructuring syntax for tuple/pair (or anything compatible with
unqualified get<N>(x))

These are orthogonal features.

1 brings along a big changelog, I don't even want to imagine how many
parts of the standard would need to be touched to integrate multiple
return values into functions, lambdas, and all the other funny corner
cases involving return values.

2 is a much more isolated change and would give multiple return values
"for free" to anything returning tuples/pairs. One might then think
about a convenient shorthand syntax for declaring tuple types. However
that than begs the question of whether one requires to include <tuple>
when using a language feature (as is technically required for
initializer_list) which is kinda awkward to be honest.

Matthew Woehlke

unread,
May 26, 2015, 2:50:59 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 14:11, Matthew Fioravante wrote:
> On Tuesday, May 26, 2015 at 12:09:27 PM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-20 21:14, Matthew Fioravante wrote:
>>> I've chosen to use [] instead of {} [...]
>>
>> Given the syntax comments, I would encourage you to read the previous
>> thread(s). That said, I have to say I'm actually in favor of the []
>> syntax; it "feels" least likely to have parsing issues, and [] normally
>> being indexing operators, it makes sense to me to use them for
>> unpacking. (It probably also helps that I write the odd bit of Python,
>> and Python uses [] to write lists :-).)
>
> But python also uses () to define tuples. The () syntax to me seems more
> natural but again I don't know it its possible with the current parsing
> rules.

True :-). I'm not wedded to []'s (I could see ()'s too), it's more that
they don't *bother* me, the way they seem to bother some folks. Also
that it "feels" like they would be the easiest to parse without
additional typing or syntax ambiguities.

>>> I've chosen to use ; instead of , to avoid confusion with the comma
>>> operator.
>>
>> I strongly agree with using ';' to separate elements, since multiple
>> declarations should be permitted, but '[auto x, auto y]' clearly makes
>> no syntactic sense. That said, is ',' allowed inside the []'s at all? If
>> yes, what does it mean?
>>
>> [auto x, y] = bar(); // forces decltype(x) == decltype(y)?
>
> It should be equivalent to:
>
> auto t = bar();
> auto x, y = std::get<0>(t);
>
> Which is currently an error because x is not initialized.

Riiiight. That seems weird. I was thinking:

auto&& __temp = bar();
auto x = get<0>(__temp), y = get<1>(__temp);

...which anyway I'm not sure if that's legal (at least not for 'auto'),
but seems like a reasonable way to parse the above input. I'd lean
toward either that, or just make ',' (or at least 'a declaration
declaring more than one variable') an error in this context.

> You could have some horrible thing like this, which would be allowed:
>
> int& operator,(int& a, int& b) { return a; }
>
> int x;
> int y;
> [ x,y; void; void] = foo(); //assignes std::get<0>(foo()) to x

Well, yes, but that's just abusing an lvalue expression :-). We'd have
to explicitly forbid that, which doesn't seem worthwhile.

>> I'd be somewhat strongly inclined to follow Python, and not discard any
>> values. So the last item in the list becomes a tuple of whatever is left
>> over.
>
> This feature conflicts with allowing this syntax to work with any std::get
> compatible type. What type should auto... z be?

I'd say 'tuple'. It makes sense for 'tuple', and it makes sense for
weird things (e.g. some struct with std::get to get each member by
index) which otherwise would require the compiler to synthesize a new
type on the spot (which... sounds like a tuple anyway).

If we implemented it as, say, a new std::get<size_t from, size_t to>,
users would be able to provide partial specializations to override that
default behavior (and STL could do so for e.g. std::array). The
"default" would return a tuple of the specified values.

> tuple<int,int> a();
> array<int,2> b();
> pair<int,int> c();
>
> [auto xa; auto xva] = a(); //decltype(xva) is tuple<int>?
> [auto xb; auto xvb] = b(); //decltype(xvb) is array<int,1>?
> [auto xc; auto xvc] = c(); //What is decltype(xvc)???

Hmm... this seems like a good reason to require '...'. Then, only '...'
may consume multiple values (presuming we're going with the 'must
explicitly consume everything' rule), and a '...' *always* has a
multi-valued type, as above, even if there is only a single remaining value.

> Here is another generic code use case which is trickier if we are only
> based on std::get().
> Imagine we want to extract the second parameter, and pass long a
> concatenated list of the first and the tail.
>
> [auto first; auto second; auto... tail] = foo();
> use(first);
> auto params = concat({ first }, tail)
> bar(params)
>
> Such a thing is possible for tuple and array but not pair.

If 'auto...' can be empty, why would this not work? (In fact, why would
it work differently for tuple<T1, T2> than pair<T1, T2>?)

>> The down side of course is that this (except the 'void...' case)
>> requires the compiler to know how many items are in the result. For
>> tuple this is probably easy. For "generic" types it may be harder, but I
>> imagine if necessary we could introduce a companion to std::get to
>> return the largest index that can be used with std::get, if we don't
>> already have ways to accomplish that.
>
> This language feature is built ontop of std::get<> which itself is designed
> for container types where we can dereference elements by index at compile
> time. Calling std::get with an invalid index is a compiler error and thus
> it should be possible to determine the valid index range at compile time
> using SFINAE tricks.

Okay... inelegant, but works :-). Considering that the compiler should
only need to inspect one more index than the number of assignments that
are being emitted, it may even be not inefficient.

>> This may be implied anyway, but I'll say it explicitly; let's please
>> also support this in range-based for:
>>
>> for ([auto key; auto value] : some_map) { ... }
>
> This is one major use case of such a feature and hopefully it should fall
> out automagically without any special provisions for it.

It might :-). It depends on how firm the "as-if" rule is that
range-based for involves an assignment. (Well, that may be more a case
of how compilers implement range-based for, but this feels like
something that could possibly be resolved in a DR if necessary, assuming
the proposal is accepted without explicitly acknowledging this case.
Also, I suspect compilers would tend to implement it even if the
standard didn't technically require it.)

Disclosure: I haven't looked up the standardese for how range-based for
is specified.

--
Matthew

hun.nem...@gmail.com

unread,
May 26, 2015, 3:04:14 PM5/26/15
to std-pr...@isocpp.org
I think we need a parameter struct like thing. A struct with a parameter list syntax, something like:

// return type is a parameter-struct
struct (int a,  int b, double) foo() {
   
return { 1, 2, 3.0 };
}

struct Test : decltype(foo()) // inherit from a parameter-struct
{
};

int main() {
 
int a = foo().a;
 
int b = foo().b;
 
int c = foo().member2; // unnamed member got a member + index name
 
int ai = foo().get<0>(); // auto generated getter
 
int bi = foo().get<1>();
 
double c = foo().get<2>();
  foo
().set<1>(2).get<1>(); // auto generated setter, returning *this
}


Peter

Matthew Woehlke

unread,
May 26, 2015, 3:07:27 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 14:29, Matthew Fioravante wrote:
> On Tuesday, May 26, 2015 at 2:24:22 PM UTC-4, Matthew Woehlke wrote:
>> Is there a reason this is not possible with returning std::tuple? Is
>> there a reason we should not support unpacking of e.g. std::pair,
>> std::tuple as return values?
>
> std::pair<X,Y> is convertible to std::tuple<X,Y> [...]

I think you missed the point :-). Nicol seemed to be implying a
(performance?) benefit to only supporting some new, special built-in
tuple-like type, as opposed to std::tuple (or more generally, classes
that support std::get). I'm not convinced that RVO can't happen with
this and e.g. std::tuple.

>> Put differently, is there a reason this needs to be part of the initial
>> proposal rather than a follow-on proposal? (That said, I *would* be in
>> favor of a simplified mechanism for returning multiple values;
>> std::tuple is... aesthetically challenged :-).)
>
> I agree std::tuple is ugly is somewhat non-obvious for beginners. Having a
> syntax for defining multiple return functions and also capturing the values
> would probably be the way to go.

Sure. But I agree with your comments; it seems to me also that this
should be syntactic sugar for std::tuple. This significantly lessens the
implementation burden (in particular, no new syntax for the return). And
that being the case, it may make sense to do it as a follow-on proposal.

(Hmm... should we allow '[int*3]' -> std::array<int, 3>? Again, though,
this feels like it should be a separate, follow-on proposal.)

--
Matthew

Matthew Fioravante

unread,
May 26, 2015, 3:18:36 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 2:50:59 PM UTC-4, Matthew Woehlke wrote:

True :-). I'm not wedded to []'s (I could see ()'s too), it's more that
they don't *bother* me, the way they seem to bother some folks. Also
that it "feels" like they would be the easiest to parse without
additional typing or syntax ambiguities.

Honestly I'm ok with either one. The question of () vs [] (assuming both are possible) can be considered a bikeshed question.
 

>>> I've chosen to use ; instead of , to avoid confusion with the comma
>>> operator.
>>
>> I strongly agree with using ';' to separate elements, since multiple
>> declarations should be permitted, but '[auto x, auto y]' clearly makes
>> no syntactic sense. That said, is ',' allowed inside the []'s at all? If
>> yes, what does it mean?
>>
>>   [auto x, y] = bar(); // forces decltype(x) == decltype(y)?
>
> It should be equivalent to:
>
> auto t = bar();
> auto x, y = std::get<0>(t);
>
> Which is currently an error because x is not initialized.

Riiiight. That seems weird. I was thinking:

  auto&& __temp = bar();
  auto x = get<0>(__temp), y = get<1>(__temp);

...which anyway I'm not sure if that's legal (at least not for 'auto'),
but seems like a reasonable way to parse the above input. I'd lean
toward either that, or just make ',' (or at least 'a declaration
declaring more than one variable') an error in this context.

I think this would be very confusing because now you can use , or ; to capture multiple values. I think the easiest way to specify this feature would be to specify the code that it "as-if" decomposes to, just like C++11 for each.

Since "auto x, y = something" is illegal C++, users already get a compiler warning when they accidentally try to use, instead of ;.
 

> You could have some horrible thing like this, which would be allowed:
>
> int& operator,(int& a, int& b) { return a; }
>
> int x;
> int y;
> [ x,y; void; void] = foo(); //assignes std::get<0>(foo()) to x

Well, yes, but that's just abusing an lvalue expression :-). We'd have
to explicitly forbid that, which doesn't seem worthwhile.

I don't think we should explicitly forbid anything. Either you specify a local variable declaration (which is compiled "as-if"), void, or an expression whose result is assigned to ("as-if").
 

>> I'd be somewhat strongly inclined to follow Python, and not discard any
>> values. So the last item in the list becomes a tuple of whatever is left
>> over.
>
> This feature conflicts with allowing this syntax to work with any std::get
> compatible type. What type should auto... z be?

I'd say 'tuple'. It makes sense for 'tuple', and it makes sense for
weird things (e.g. some struct with std::get to get each member by
index) which otherwise would require the compiler to synthesize a new
type on the spot (which... sounds like a tuple anyway).

If we implemented it as, say, a new std::get<size_t from, size_t to>,
users would be able to provide partial specializations to override that
default behavior (and STL could do so for e.g. std::array). The
"default" would return a tuple of the specified values.

> tuple<int,int> a();
> array<int,2> b();
> pair<int,int> c();
>
> [auto xa; auto xva] = a(); //decltype(xva) is tuple<int>?
> [auto xb; auto xvb] = b(); //decltype(xvb) is array<int,1>?
> [auto xc; auto xvc] = c(); //What is decltype(xvc)???

Hmm... this seems like a good reason to require '...'. Then, only '...'
may consume multiple values (presuming we're going with the 'must
explicitly consume everything' rule), and a '...' *always* has a
multi-valued type, as above, even if there is only a single remaining value.

Sorry, I actually meant to say ... there and made a typo. I agree that ... should be used to multi-capture, even in the size 1 or 0 case. The question I was trying to highlight is what the type should be if we are generically supporting any std::get() compatible type?

If the multi-captured value is to inherit the return type of the original function, this works fine for tuple and array but not for pair because pair has to be always size 2. We could add a "hack" for converting pair to tuple, but then we still have to figure out how to handle std::get compatible user defined types.

Allowing the syntax to capture from an array and then having the multi-capture variable be a tuple might be confusing.

array<int,4> foo();
[auto x, auto... yzw] = foo();
//yzw is tuple<int,int,int> but I expected array<int,3>!




> Here is another generic code use case which is trickier if we are only
> based on std::get().
> Imagine  we want to extract the second parameter, and pass long a
> concatenated list of the first and the tail.
>
> [auto first; auto second; auto... tail] = foo();
> use(first);
> auto params = concat({ first }, tail)
> bar(params)
>
> Such a thing is possible for tuple and array but not pair.

If 'auto...' can be empty, why would this not work? (In fact, why would
it work differently for tuple<T1, T2> than pair<T1, T2>?)

pair is always size 2, you cannot concatenate and chop up pairs like you can with tuples and arrays.
 

I believe range for is specified using "as-if" code substitution, so it should just be like 2 macro substitutions. The first one to expand the range for into an optimal iterator loop, and then another to expand the multi return value extraction syntax.

Matthew Fioravante

unread,
May 26, 2015, 3:34:18 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 3:07:27 PM UTC-4, Matthew Woehlke wrote:
 I'm not convinced that RVO can't happen with
this and e.g. std::tuple.

I'm not convinced either.

For example, this should be possible today:

tuple<int,float,double> foo();

//RVO constructs t in place
auto t = foo();
auto x = std::move(get<0>(t));
//use x from here, but never touch t again

There is no reason the compiler can't completely elide the move/copy construction of x and just keep referencing std::get<0>(t) which itself was RVO constructed.

 

>> Put differently, is there a reason this needs to be part of the initial
>> proposal rather than a follow-on proposal? (That said, I *would* be in
>> favor of a simplified mechanism for returning multiple values;
>> std::tuple is... aesthetically challenged :-).)
>
> I agree std::tuple is ugly is somewhat non-obvious for beginners. Having a
> syntax for defining multiple return functions and also capturing the values
> would probably be the way to go.

Sure. But I agree with your comments; it seems to me also that this
should be syntactic sugar for std::tuple. This significantly lessens the
implementation burden (in particular, no new syntax for the return). And
that being the case, it may make sense to do it as a follow-on proposal.

(Hmm... should we allow '[int*3]' -> std::array<int, 3>? Again, though,
this feels like it should be a separate, follow-on proposal.)

I was thinking maybe [int,3] but using * might be ok too, but yes there could be value for supporting arrays also. Arrays have advantages in that you can do for loops on arrays and pass them on directly to APIs which take an array_view, pointer pairs, etc.. Arrays also allow you to store and compute the "number of elements" as a compile time constant / constexpr expression.

If the extraction syntax works for array, then I think multi value extract should also be an array.

[int,3] foo();
[auto x, auto yz] = foo();
//yz is array<int,2>
[auto y, auto z] yz;

We could also combine them:

[[int,2],float,[double,3]] x; //tuple<array<int,2>,float,array<double,3>> x;

Also saying std::array<int,3> is verbose and ugly. Its probably one of the biggest reasons why people still use C arrays even though they really have no advantages and a lot of disadvantages over std::array.

Finally, one more step gives us the much desired "deduce the size from the initialization sequence" behavior of C arrays.

[auto,*] x = {1, 2, 3}; //x is array<int,3>


 

hun.nem...@gmail.com

unread,
May 26, 2015, 3:36:09 PM5/26/15
to std-pr...@isocpp.org, hun.nem...@gmail.com


On Tuesday, May 26, 2015 at 9:04:14 PM UTC+2, hun.nem...@gmail.com wrote:
I think we need a parameter struct like thing. A struct with a parameter list syntax, something like:

// return type is a parameter-struct
struct (int a,  int b, double) foo() {
   
return { 1, 2, 3.0 };
}

struct Test : decltype(foo()) // inherit from a parameter-struct
{
};

int main() {
 
int a = foo().a;
 
int b = foo().b;
 
int c = foo().member2; // unnamed member got a member + index name
 
int ai = foo().get<0>(); // auto generated getter
 
int bi = foo().get<1>();
 
double c = foo().get<2>();
  foo
().set<1>(2).get<1>(); // auto generated setter, returning *this
}


Peter



Maybe with an inline extension
inline struct (int a,  int b, double) foo() {
   
return { 1, 2, 3.0 };
}
int main() {
 
foo();
  std::cout << "Hello inline variables: " << a << b << member2 << std::end;
}

Or using a compile time code generator (not exit now) feature here.

struct (int a,  int b, double) foo() {
   
return { 1, 2, 3.0 };
}
int main() {
 
#!inlineVariables(foo());
  std::cout << "Hello inline variables: " << a << b << member2 << std::end;
}

Peter

hun.nem...@gmail.com

unread,
May 26, 2015, 3:39:56 PM5/26/15
to std-pr...@isocpp.org, hun.nem...@gmail.com


On Tuesday, May 26, 2015 at 9:04:14 PM UTC+2, hun.nem...@gmail.com wrote:
I think we need a parameter struct like thing. A struct with a parameter list syntax, something like:

// return type is a parameter-struct
struct (int a,  int b, double) foo() {
   
return { 1, 2, 3.0 };
}

struct Test : decltype(foo()) // inherit from a parameter-struct
{
};

int main() {
 
int a = foo().a;
 
int b = foo().b;
 
int c = foo().member2; // unnamed member got a member + index name
 
int ai = foo().get<0>(); // auto generated getter
 
int bi = foo().get<1>();
 
double c = foo().get<2>();
  foo
().set<1>(2).get<1>(); // auto generated setter, returning *this
}



Another addition can be a generated constexpr size() method
   int bi = foo().size(); // returns 3
   static_assert(foo().size() == 3);

Matthew Woehlke

unread,
May 26, 2015, 3:51:19 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 15:18, Matthew Fioravante wrote:
> On Tuesday, May 26, 2015 at 2:50:59 PM UTC-4, Matthew Woehlke wrote:
>>> tuple<int,int> a();
>>> array<int,2> b();
>>> pair<int,int> c();
>>>
>>> [auto xa; auto xva] = a(); //decltype(xva) is tuple<int>?
>>> [auto xb; auto xvb] = b(); //decltype(xvb) is array<int,1>?
>>> [auto xc; auto xvc] = c(); //What is decltype(xvc)???
>
> Sorry, I actually meant to say ... there and made a typo. [...] The question
> I was trying to highlight is what the type should be if we are generically
> supporting any std::get() compatible type?

I still think the best way to answer that is to use something other than
std::get<size_t> for '...', even if it's std::get<size_t, size_t>. That
way users can specialized, STL can specialize (e.g. for std::array), and
anything that doesn't can fall back on std::tuple.

>>> Here is another generic code use case which is trickier if we are only
>>> based on std::get().
>>> Imagine we want to extract the second parameter, and pass long a
>>> concatenated list of the first and the tail.
>>>
>>> [auto first; auto second; auto... tail] = foo();
>>> use(first);
>>> auto params = concat({ first }, tail)
>>> bar(params)
>>>
>>> Such a thing is possible for tuple and array but not pair.
>>
>> If 'auto...' can be empty, why would this not work? (In fact, why would
>> it work differently for tuple<T1, T2> than pair<T1, T2>?)
>
> pair is always size 2, you cannot concatenate and chop up pairs like you
> can with tuples and arrays.

...but why would you expect the result of the above example to be
different for (e.g.) std::pair<int, int> than for std::tuple<int, int>?

I wouldn't. I would expect them to be the same; decltype(tail) ==
std::tuple<>.

Actually, I think this illustrates *why* tails should normally decompose
to tuples; it's much easier to deal with them if they end up as a known
type.

--
Matthew

Matthew Woehlke

unread,
May 26, 2015, 3:57:51 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 15:34, Matthew Fioravante wrote:
> On Tuesday, May 26, 2015 at 3:07:27 PM UTC-4, Matthew Woehlke wrote:
>> (Hmm... should we allow '[int*3]' -> std::array<int, 3>? Again, though,
>> this feels like it should be a separate, follow-on proposal.)
>
> I was thinking maybe [int,3] but using * might be ok too,

It's Pythonic, but on second thought, '[int**3]' would be confusing :-).

Anyway, that's a bikeshed for another time; I still think we should get
unpacking first, and worry about syntax sugars later.

(Your other point is addressed in my other replies.)

> Also saying std::array<int,3> is verbose and ugly. Its probably one of the
> biggest reasons why people still use C arrays even though they really have
> no advantages and a lot of disadvantages over std::array.

This gives me ideas, but they're likely to be contentious (maybe not
between us, but to others). So instead I'll mention again this is one
reason I'd like to worry about unpacking first, and syntax sugar
separately :-).

> [auto,*] x = {1, 2, 3}; //x is array<int,3>

(Er... yes. Ideas along these lines. See previous comment.)

--
Matthew

Matthew Fioravante

unread,
May 26, 2015, 4:00:50 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
Converting a pair to a tuple is natural and probably won't result in too many surprises, converting array to tuple not so much.

If this feature is to support std::array, then I think it would be better for multi-capture to also construct a (sub) array as arrays have nice properties that tuples do not have. If multi-capture always constructs a tuple then it would probably be better to only define this proposal to operate with tuple, possibly doing conversions from pair and array.

Having all of the "type lists" be the same type will improve readability, generic code (I can pass the entire type list or a multi-captured sub type-list to the same function) and reduce surprises.

Is there any real reason to actually support anything other than tuple (with implicit conversion from pair) and array?

Nicol Bolas

unread,
May 26, 2015, 4:31:59 PM5/26/15
to std-pr...@isocpp.org
On Tuesday, May 26, 2015 at 2:49:38 PM UTC-4, Miro Knejp wrote:
The question here is really whether you want
1.  a syntax and language support for multiple return values
or
2. a destructuring syntax for tuple/pair (or anything compatible with
unqualified get<N>(x))

These are orthogonal features.

Sort of.

We already have a way to deconstruct tuples/pairs: `std::tie`. So what is being suggested is specialized syntax for it.

However, the principle justification for making that a language syntax (as stated here in the OP) is for multiple return values. So if you have multiple return values as legitimate syntax, you lose about 80% of the reason for having tuple unpacking syntax.

Oh sure, people can still make use of it. But when you're talking about adding new syntax for something, you need a really good justification for it. And if multiple return values were a first-class C++ feature, then tuple unpacking syntax would loose its major justifying purpose. And thus it would probably never happen.

So while they're not technically orthogonal, it's highly unlikely that we'd ever get both.

Matthew Woehlke

unread,
May 26, 2015, 4:33:24 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 16:00, Matthew Fioravante wrote:
> Is there any real reason to actually support anything other than tuple
> (with implicit conversion from pair) and array?

QPair. QSize. Other weird user-defined classes that "look like"
std::tuple, std::pair, or std::array. Existing API's that currently
return a struct because they predate std::tuple or considered it "too
ugly". (I have a range struct, i.e. a "pair" with lower/upper, that
might benefit.)

For that matter, tuple is *still* ugly in failing to name its elements.
I can readily imagine that even with unpacking, some folks will want to
return structs, but would like to be able to unpack them.

In general, yes, I think adds value if unpacking can be extended by the
user to work on new types, i.e. by providing std::get for such types.

> Converting a pair to a tuple is natural and probably won't result in too
> many surprises, converting array to tuple not so much.

This, and the above, is why I keep saying that the tail should use
something *like-but-not-exactly* std::get<size_t>, so that STL and the
user can specialize it if it makes sense for the tail to be something
other than std::tuple. (Note that in saying that, I am implying that STL
*would* specialize so that a tail of std::array is a std::array.)

Plus, if we miss STL edge cases, I'd expect there to be less resistance
in such case to vendors implementing non-conforming specializations in
their STL implementation and getting them standardized via DR's.

--
Matthew

Nicol Bolas

unread,
May 26, 2015, 4:36:23 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 3:34:18 PM UTC-4, Matthew Fioravante wrote:


On Tuesday, May 26, 2015 at 3:07:27 PM UTC-4, Matthew Woehlke wrote:
 I'm not convinced that RVO can't happen with
this and e.g. std::tuple.

I'm not convinced either.

For example, this should be possible today:

tuple<int,float,double> foo();

//RVO constructs t in place
auto t = foo();
auto x = std::move(get<0>(t));
//use x from here, but never touch t again

There is no reason the compiler can't completely elide the move/copy construction of x and just keep referencing std::get<0>(t) which itself was RVO constructed.

Yes there is. The Standard for Programming Language C++ explicitly forbids it.

This statement:

auto x = std::move(<<<insert any expression here>>>);

is required by the standard to construct `x` via an explicit call to a copy/move constructor (as appropriate to the type of the expression).

The only reason that this statement:

auto x = FunctionCall(...);

is allowed to elide the copy/move to `x` is because the standard has explicit language allowing the compiler to elide the copy/move. It can only happen through the return value from a function, directly.

What you're suggesting would be some form of generalized elision, which nobody has suggested or proposed, and would be... well, quite frankly rather scary to even contemplate.

Nicol Bolas

unread,
May 26, 2015, 4:40:03 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, May 26, 2015 at 3:07:27 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-26 14:29, Matthew Fioravante wrote:
> On Tuesday, May 26, 2015 at 2:24:22 PM UTC-4, Matthew Woehlke wrote:
>> Is there a reason this is not possible with returning std::tuple? Is
>> there a reason we should not support unpacking of e.g. std::pair,
>> std::tuple as return values?
>
> std::pair<X,Y> is convertible to std::tuple<X,Y> [...]

I think you missed the point :-). Nicol seemed to be implying a
(performance?) benefit to only supporting some new, special built-in
tuple-like type, as opposed to std::tuple (or more generally, classes
that support std::get). I'm not convinced that RVO can't happen with
this and e.g. std::tuple.

You can be convinced of whatever you want; the standard doesn't agree. Elision can only take place for the initialization of parameters to functions and the initialization of return values directly from functions. Therefore, if a function returns a tuple, then the tuple you store it in can elide the copy/move. However, if you then want to extract a member of this tuple, whether via `std::tie` or via `std::get<>` or whathaveyou, that's constructing a new value.

You can get a reference to the value in the tuple. But you can't copy/move to a new non-reference type without actually copying/moving.

Nicol Bolas

unread,
May 26, 2015, 4:42:29 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net

Also, I should make this clear: I'm not supporting the idea of returning a "tuple-like type". I'm saying that we should have multiple return values from functions. Just like functions can have multiple parameters.

You don't stick your parameters in a `tuple` just to call a function, so why should you pull them from a `tuple` when you return them? It's purely a language limitation, and one that is decidedly artificial.

Matthew Fioravante

unread,
May 26, 2015, 4:56:54 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 4:33:24 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-26 16:00, Matthew Fioravante wrote:
> Is there any real reason to actually support anything other than tuple
> (with implicit conversion from pair) and array?

QPair. QSize. Other weird user-defined classes that "look like"
std::tuple, std::pair, or std::array. Existing API's that currently
return a struct because they predate std::tuple or considered it "too
ugly". (I have a range struct, i.e. a "pair" with lower/upper, that
might benefit.)

Another solution for these could be asking user defined types to implement operator tuple<X,Y,Z>() or operator array<X,N>(); That might require copies though and be less efficient than std::get(). The thing about std::get() is that its actually very painful to implement because handling the max index requires a lot of ugly SFINAE hacks. Implementing a range based std::get would be even more complicated meta-programming.
 

For that matter, tuple is *still* ugly in failing to name its elements.
I can readily imagine that even with unpacking, some folks will want to
return structs, but would like to be able to unpack them.

For unpacking, having names does not matter but I agree sometimes I like to use structs. Still I'm not sure the two use cases mesh well.

If I'm returning a struct, that probably means I intend the client to use the struct as a distinct object by itself. If the user can use this proposals feature to capture the return values directly into nicely named local variables, that kind of defeats the purpose of returning a struct just for the named members.


In general, yes, I think adds value if unpacking can be extended by the
user to work on new types, i.e. by providing std::get for such types.

> Converting a pair to a tuple is natural and probably won't result in too
> many surprises, converting array to tuple not so much.

This, and the above, is why I keep saying that the tail should use
something *like-but-not-exactly* std::get<size_t>, so that STL and the
user can specialize it if it makes sense for the tail to be something
other than std::tuple. (Note that in saying that, I am implying that STL
*would* specialize so that a tail of std::array is a std::array.)

We should think more specifically about what such an API would look like and what it takes for a user to implement one for a simple case such as struct { float x; float y; float z; }. Not only do we need std::get<int> but also std::get<int,int> for the mutli-value capture and all of these need to correctly fail compilation for an invalid index while simultaneously supporting recursive iteration (SFINAE).

Matthew Fioravante

unread,
May 26, 2015, 4:58:36 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 4:56:54 PM UTC-4, Matthew Fioravante wrote:
We should think more specifically about what such an API would look like and what it takes for a user to implement one for a simple case such as struct { float x; float y; float z; }. Not only do we need std::get<int> but also std::get<int,int> for the mutli-value capture and all of these need to correctly fail compilation for an invalid index while simultaneously supporting recursive iteration (SFINAE).


Also don't forget about implementing std::get() correctly for lvalue and rvalues...

http://en.cppreference.com/w/cpp/utility/tuple/get
 

Matthew Woehlke

unread,
May 26, 2015, 5:04:43 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 16:36, Nicol Bolas wrote:
> On Tuesday, May 26, 2015 at 3:34:18 PM UTC-4, Matthew Fioravante wrote:
>> For example, this should be possible today:
>>
>> tuple<int,float,double> foo();
>>
>> //RVO constructs t in place
>> auto t = foo();
>> auto x = std::move(get<0>(t));
>> //use x from here, but never touch t again
>>
>> There is no reason the compiler can't completely elide the move/copy
>> construction of x and just keep referencing std::get<0>(t) which itself was
>> RVO constructed.
>
> Yes there is. The Standard for Programming Language C++ *explicitly forbids
> it*.
>
> What you're suggesting would be some form of generalized elision, which
> nobody has suggested or proposed, and would be... well, quite frankly
> rather scary to even contemplate.

Why generalized? Why can we not extend RVO to unpacking? IOW, implement
unpacking "as-if" but with a special allowance for RVO? (Why would that
be worse than what you were proposing?)

--
Matthew

Matthew Fioravante

unread,
May 26, 2015, 5:23:20 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 4:40:03 PM UTC-4, Nicol Bolas wrote:
You can be convinced of whatever you want; the standard doesn't agree. Elision can only take place for the initialization of parameters to functions and the initialization of return values directly from functions. Therefore, if a function returns a tuple, then the tuple you store it in can elide the copy/move. However, if you then want to extract a member of this tuple, whether via `std::tie` or via `std::get<>` or whathaveyou, that's constructing a new value.


If that's the case, the standard practice could be to capture by reference.

[const auto& x; auto& y] = foo();

//expands to:
auto t = foo();
const auto& x = std::get<0>(t);
auto& y = std::get<1>(t);

Since the unnamed tuple "t" in this example always has a longer lifetime than x or y, the references will always be valid. You're still forced to do a move/copy if you are assigning to a pre-existing variable, but you can't get RVO in that case anyway.

Capturing by reference also makes sense when you are extracting from a tuple object directly.

auto t = [int,float]();
[auto& i, auto& f] = t;


On Tuesday, May 26, 2015 at 4:42:29 PM UTC-4, Nicol Bolas wrote:
Also, I should make this clear: I'm not supporting the idea of returning a "tuple-like type". I'm saying that we should have multiple return values from functions. Just like functions can have multiple parameters.

You don't stick your parameters in a `tuple` just to call a function, so why should you pull them from a `tuple` when you return them? It's purely a language limitation, and one that is decidedly artificial.

Why not? Conceptually, there is little difference between a type list and list of function parameters. You could think of calling a function as "take these local variables, arrange them into a type list, and call the function with that list of parameters". Some languages like python let you expand a tuple in place to call a function with it as the parameters. Python also handles multiple return value capture using tuple syntax.

As was stated earlier, doing it this way would be a huge change that likely would never make it past the committee. Each platform will have to define a new calling convention for multiple return valued functions. We would never get support for interfaces which already return pair and tuple and they could never be changed because it would break old code. Other than potential performance concerns with RVO that might be alleviated by using references, what is the real benefit of doing it as a pure language feature like this?

How would you for example support a range for loop with std::unordered_map? The iterator dereference already returns a std::pair. There is no way we could change it return multiple values using the magic syntax and break old code. We wouldn't even want that as sometimes we want to pass along the pair, and other times we want to extract the key and value and work with those.

Using a type like tuple gives the callers the option of capturing all of the return values together in a container or extracting them out. They also can use generic tuple processing functions for free if it makes sense to do that.

Matthew Woehlke

unread,
May 26, 2015, 5:31:10 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 16:31, Nicol Bolas wrote:
> On Tuesday, May 26, 2015 at 2:49:38 PM UTC-4, Miro Knejp wrote:
>> The question here is really whether you want
>> 1. a syntax and language support for multiple return values
>> or
>> 2. a destructuring syntax for tuple/pair (or anything compatible with
>> unqualified get<N>(x))
>>
>> These are orthogonal features.
>
> Sort of.
>
> We already have a way to deconstruct tuples/pairs: `std::tie`. So what is
> being suggested is specialized syntax for it.
>
> However, the principle justification for making that a language syntax (as
> stated here in the OP) is for multiple return values. So if you have
> multiple return values as legitimate syntax, you lose about 80% of the
> reason for having tuple unpacking syntax.

If we had multiple return values as a first class language feature, why
should I not expect to be able to write the opposite of std::tie? :-)

--
Matthew

Magnus Fromreide

unread,
May 26, 2015, 5:47:42 PM5/26/15
to std-pr...@isocpp.org
One thing I am asking myself is if these should be composable.

[int, long, double] foo();
bar(int, long, double);

bar(foo()); // Should this be valid?

/MF

Matthew Woehlke

unread,
May 26, 2015, 5:51:25 PM5/26/15
to std-pr...@isocpp.org
On 2015-05-26 16:56, Matthew Fioravante wrote:
> We should think more specifically about what such an API would look like
> and what it takes for a user to implement one for a simple case such as
> struct { float x; float y; float z; }. Not only do we need std::get<int>
> but also std::get<int,int> for the mutli-value capture and all of these
> need to correctly fail compilation for an invalid index while
> simultaneously supporting recursive iteration (SFINAE).

*STL* would need the latter. Users would only need to specialize the
latter if they want something other than a tuple as the result. The
default would use the single-valued std::get to produce a tuple.

--
Matthew

Matthew Fioravante

unread,
May 26, 2015, 5:52:43 PM5/26/15
to std-pr...@isocpp.org

I would say no. One needs to differentiate between passing the whole collection of parameters as one object and unpacking the parameters and passing them separately.

Something like this if desired would probably require yet another unpacking syntax, similar to how python lets you unpack a tuple into a function call.

[int, long, double] foo();
[int, long, double] x;
void bar(int, long, double);
void car(tuple<int, long, double>);

bar
([*]foo());
bar
([*]x);
car
(foo());
car
(x);


 

Matthew Woehlke

unread,
May 26, 2015, 6:45:54 PM5/26/15
to std-pr...@isocpp.org
No. Not, anyway, without some additional syntax. Even in Python this is
not legal (you need a '*').

Now, I'd love to see the ability to unpack a std::tuple into multiple
parameters for a function, but that could be orthogonal. (Maybe not
orthogonal, if we decide to go Nicol's route and make arguments packs
some sort of first class entities that *aren't* std::tuple, though for
various reasons already expressed I remain unconvinced that's a good
idea...)

I could live with '[*]tuple' :-).

--
Matthew

Thiago Macieira

unread,
May 26, 2015, 7:15:38 PM5/26/15
to std-pr...@isocpp.org
On Tuesday 26 May 2015 13:00:50 Matthew Fioravante wrote:
> Is there any real reason to actually support anything other than tuple
> (with implicit conversion from pair) and array?

Are we talking about std::tuple or std::__1::tuple? And if it's std::tuple, do
you mean the one from GCC's libstdc++ or the STLport one, on Android?

Those types do not need to be layout compatible, especially when it comes to
empty member optimisations and optimising of padding.

std::initializer_list was specified as part of the initialiser list feature, so
ABIs defined it in those terms too; std::tuple did not benefit from that. So
this feature cannot rely on std::tuple, but must instead have its own ABI-
dependent type that std::tuple could construct constexprly from.
--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Nicol Bolas

unread,
May 26, 2015, 8:14:09 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


... std::tie takes a tuple and exports the members of that tuple into variables. The opposite of that would be to take variables and combine them into a tuple. That's called `make_tuple`; you don't need special syntax for that.

Also, I have no idea what that has to do with multiple return values.

Nicol Bolas

unread,
May 26, 2015, 9:23:50 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, May 26, 2015 at 5:23:20 PM UTC-4, Matthew Fioravante wrote:

On Tuesday, May 26, 2015 at 4:42:29 PM UTC-4, Nicol Bolas wrote:
Also, I should make this clear: I'm not supporting the idea of returning a "tuple-like type". I'm saying that we should have multiple return values from functions. Just like functions can have multiple parameters.

You don't stick your parameters in a `tuple` just to call a function, so why should you pull them from a `tuple` when you return them? It's purely a language limitation, and one that is decidedly artificial.

Why not?

Because it requires needless copy/moving. Because it looks like worse syntax than function parameters, even with your terse version. Because it provides absolutely zero descriptive information about what those parameters mean.

Because it's entirely pointless, and has been since we got variadic templates. Unless you happen to have a collection of values that are already in a tuple, there's no reason to pass a tuple to a function as a set of parameters.

Conceptually, there is little difference between a type list and list of function parameters. You could think of calling a function as "take these local variables, arrange them into a type list, and call the function with that list of parameters". Some languages like python let you expand a tuple in place to call a function with it as the parameters. Python also handles multiple return value capture using tuple syntax.

Yeah, and Python also handles multiple return values as a first-class language feature. It doesn't force you to package your multiple return values into a tuple, return them, and then unpack them. Nor does it force you to package parameters into a tuple.

So you're kind of proving my point for me.

It should also be noted that Python:

1) Is not a statically typed language.

2) Is not a strongly typed language.

Because of these, Python doesn't have "tuples" at all. It simply has arrays (and other data structures), which can contain values by index. It has a function that can take an array and return that array as though it were a sequence of values. And so forth. Because Python is dynamically typed, the distinction between "tuple" and "array" simply doesn't exist.

So Python doesn't have the issues that C++ does.

As was stated earlier, doing it this way would be a huge change that likely would never make it past the committee.

But it's the right way to do it. And quite frankly, we don't need multiple return values so badly that we should do it the wrong way just to have some convenient way to do it.

Some people use tuples a lot (some might say to the point of overuse). But you shouldn't steal potentially useful syntax from those C++ programmers who don't use them. Syntax is a precious commodity, which once apportioned, cannot be taken back.

It should also be noted that reflection, with the full ability to compile-time inspect and generate types with full syntax (including variable names) makes tuples... a dubious proposition for the future of C++. With reflection, you can index a struct by number. With reflection, you can take any typelist and build an honest-to-God struct out of it (which can be standard layout and such).

In short, it is entirely possible that tuples will become obsolete, thus also obsoleting this kind of syntax. So let's not have another std::bind happen here...
 
Each platform will have to define a new calling convention for multiple return valued functions. We would never get support for interfaces which already return pair and tuple and they could never be changed because it would break old code. Other than potential performance concerns with RVO that might be alleviated by using references, what is the real benefit of doing it as a pure language feature like this?

Well sure, if you ignore all of the advantages of something, then yes there's not much point to it.

How would you for example support a range for loop with std::unordered_map? The iterator dereference already returns a std::pair.

To extend it for multiple return values, all you need is an alternate iterator type that returns multiple values.

David Krauss

unread,
May 26, 2015, 9:41:26 PM5/26/15
to std-pr...@isocpp.org

On 2015–05–27, at 8:14 AM, Nicol Bolas <jmck...@gmail.com> wrote:

... std::tie takes a tuple and exports the members of that tuple into variables. The opposite of that would be to take variables and combine them into a tuple. That's called `make_tuple`; you don't need special syntax for that.

Also, I have no idea what that has to do with multiple return values.

The problem is that the variables need to be pre-existing, and support default-initialization.

I don’t see why the preprocessor can’t do it. Translate

UNTIE( (a) (b) (c), expression )

into

auto && TUPLE_abc_init = expression;
static_assert ( is_rvalue_reference_to_tuple_v< decltype (TUPLE_abc_init) >, “Only tuple values can be untied” );
auto && a = std::get<0>( std::move( TUPLE_abc_init ) );
auto && b = std::get<0+1>( std::move( TUPLE_abc_init ) );
auto && c = std::get<0+1+1>( std::move( TUPLE_abc_init ) );

It’s at least a starting point…

Matthew Fioravante

unread,
May 26, 2015, 10:53:05 PM5/26/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Tuesday, May 26, 2015 at 9:23:50 PM UTC-4, Nicol Bolas wrote:
On Tuesday, May 26, 2015 at 5:23:20 PM UTC-4, Matthew Fioravante wrote:

On Tuesday, May 26, 2015 at 4:42:29 PM UTC-4, Nicol Bolas wrote:
Also, I should make this clear: I'm not supporting the idea of returning a "tuple-like type". I'm saying that we should have multiple return values from functions. Just like functions can have multiple parameters.

You don't stick your parameters in a `tuple` just to call a function, so why should you pull them from a `tuple` when you return them? It's purely a language limitation, and one that is decidedly artificial.

Why not?

Because it requires needless copy/moving.

I don't see any needless copying happening below.

void foo(int x, const string& s);
tuple
<int,string> t;

foo
([*]t); //expands to foo(get<0>(t), get<1>(t));


 
Because it looks like worse syntax than function parameters, even with your terse version. Because it provides absolutely zero descriptive information about what those parameters mean.
 
You yourself suggested the following:

[int,float,double] foo() { return {1, 1.0f, 1.0 }; }

All that tells me is that foo() returns an int, float, and double. How is this more descriptive than a tuple?
 

Because it's entirely pointless, and has been since we got variadic templates. Unless you happen to have a collection of values that are already in a tuple, there's no reason to pass a tuple to a function as a set of parameters.

Magnus just showed a use case for this.. Composing multi-return functions together. Even if tuple is not used, something just like this would be need to say "unpack the multi-return values and forward them directly to the next function".
 

Conceptually, there is little difference between a type list and list of function parameters. You could think of calling a function as "take these local variables, arrange them into a type list, and call the function with that list of parameters". Some languages like python let you expand a tuple in place to call a function with it as the parameters. Python also handles multiple return value capture using tuple syntax.

Yeah, and Python also handles multiple return values as a first-class language feature. It doesn't force you to package your multiple return values into a tuple, return them, and then unpack them.

The multiple return values should be in some kind of container, whether its a tuple or something else. Sometimes I want to deal with the whole collection as one object, other times I want to unpack right away into local variables which could just be references to the original pack.
 
Nor does it force you to package parameters into a tuple.

But it does, at least at the syntax level

def foo():
   
return (1, 2, 3);

(x, y, z) = foo();

 

So you're kind of proving my point for me.

It should also be noted that Python:

1) Is not a statically typed language.

2) Is not a strongly typed language.

Because of these, Python doesn't have "tuples" at all. It simply has arrays (and other data structures), which can contain values by index. It has a function that can take an array and return that array as though it were a sequence of values. And so forth. Because Python is dynamically typed, the distinction between "tuple" and "array" simply doesn't exist.

Which is why we may need a tuple like syntax and an array like syntax.
 

So Python doesn't have the issues that C++ does.

As was stated earlier, doing it this way would be a huge change that likely would never make it past the committee.

But it's the right way to do it. And quite frankly, we don't need multiple return values so badly that we should do it the wrong way just to have some convenient way to do it.

Some people use tuples a lot (some might say to the point of overuse). But you shouldn't steal potentially useful syntax from those C++ programmers who don't use them.

Programmers who have no interest in tuple itself can just use the syntax to return multiple values and unpack them at the call site. They don't have to care that tuple (or something tuple like) is used to accomplish this.
 
Syntax is a precious commodity, which once apportioned, cannot be taken back.

It should also be noted that reflection, with the full ability to compile-time inspect and generate types with full syntax (including variable names) makes tuples... a dubious proposition for the future of C++. With reflection, you can index a struct by number. With reflection, you can take any typelist and build an honest-to-God struct out of it (which can be standard layout and such).

In short, it is entirely possible that tuples will become obsolete, thus also obsoleting this kind of syntax. So let's not have another std::bind happen here...

[int,long,double] foo();
auto x = foo();

Ok, if tuple is out then what is decltype(x) in the above example doing things your way? Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If not using tuple, can you explain in a bit more detail how your proposed solution would actually work?
 
 
Each platform will have to define a new calling convention for multiple return valued functions. We would never get support for interfaces which already return pair and tuple and they could never be changed because it would break old code. Other than potential performance concerns with RVO that might be alleviated by using references, what is the real benefit of doing it as a pure language feature like this?

Well sure, if you ignore all of the advantages of something, then yes there's not much point to it.

How would you for example support a range for loop with std::unordered_map? The iterator dereference already returns a std::pair.

To extend it for multiple return values, all you need is an alternate iterator type that returns multiple values.

So how do I get to use this alternate iterator type? That means I have to add some range wrapper because the default iterators with begin/end still use std::pair.

for([auto& k; auto& v] : make_multi_return_range(hashmap)){}

That would be pretty sad if in order to simplify the left side of the loop I have to add boilerplate to the right. Something like this should work naturally. Even if tuple is not the answer, I would hope some kind of automatic compatibility with tuple/pair is provided so we can write simple code with STL classes without awful legacy wrappers like make_multi_return_range().

I should be able to do both of these without any unnecessary additional noise.

for(auto& kv: hashmap);
for([auto& k; auto& v]: hashmap);


Nicol Bolas

unread,
May 27, 2015, 3:43:10 AM5/27/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Tuesday, May 26, 2015 at 10:53:05 PM UTC-4, Matthew Fioravante wrote:
On Tuesday, May 26, 2015 at 9:23:50 PM UTC-4, Nicol Bolas wrote:
On Tuesday, May 26, 2015 at 5:23:20 PM UTC-4, Matthew Fioravante wrote:
On Tuesday, May 26, 2015 at 4:42:29 PM UTC-4, Nicol Bolas wrote:
Also, I should make this clear: I'm not supporting the idea of returning a "tuple-like type". I'm saying that we should have multiple return values from functions. Just like functions can have multiple parameters.

You don't stick your parameters in a `tuple` just to call a function, so why should you pull them from a `tuple` when you return them? It's purely a language limitation, and one that is decidedly artificial.

Why not?

Because it requires needless copy/moving.

I don't see any needless copying happening below.

void foo(int x, const string& s);
tuple
<int,string> t;

foo
([*]t); //expands to foo(get<0>(t), get<1>(t));



Please re-read what I said: "You don't stick your parameters in a `tuple` just to call a function." I was analogizing return-values-as-tuple with parameters-as-tuple, which would require that `foo` in your example take a tuple, not a list of parameters. Passing parameters as a tuple would require copy/moving the parameters into the tuple. Hence the "needless copying" bit.

 
Because it looks like worse syntax than function parameters, even with your terse version. Because it provides absolutely zero descriptive information about what those parameters mean.
 
You yourself suggested the following:

[int,float,double] foo() { return {1, 1.0f, 1.0 }; }

All that tells me is that foo() returns an int, float, and double. How is this more descriptive than a tuple?

Ahem: "You don't stick your parameters in a `tuple` just to call a function." That's the context of that part of the discussion. That's what "looks like worse syntax than function parameters".

 

Because it's entirely pointless, and has been since we got variadic templates. Unless you happen to have a collection of values that are already in a tuple, there's no reason to pass a tuple to a function as a set of parameters.

Magnus just showed a use case for this.. Composing multi-return functions together.

Since multiple return values are not something that exists, they can be defined to be whatever we want. So if we want function composition to work with multiple return values, I see nothing standing in the way of declaring that it does. And most importantly of all, it would require zero syntax (outside of declaring that the inner function returns multiple values). See below.

Even if tuple is not used, something just like this would be need to say "unpack the multi-return values and forward them directly to the next function".

You're assuming that multiple return values are in any way "packed". They're not; they're multiple return values. At no time are MRV's a single value packed into anything.

Scripting languages (or at least, ones that don't make whitespace part of their grammar. See below) do this all the time; that's how they make multiple return values work.

  
 
Nor does it force you to package parameters into a tuple.

But it does, at least at the syntax level

def foo():
   
return (1, 2, 3);

(x, y, z) = foo();


Sorry, my knowledge of Python is limited; I just assumed it worked more or less like Lua.

In Lua, you don't need special syntax for returning or capturing multiple values. And quite frankly, I see no reason why you should:

function foo()
 
return 1, 2, 3 --Returns a sequence of 3 values.
end

function bar()
 
return {1, 2, 3} --Returns a single table, containing 3 values
end

local x, y, z = foo() --Declares 3 local variables, initializing them with 3 values from the function.

local w = {foo()} --Stores the sequence of values returned from `foo` in a table (aka: array)

local a = bar() --Stores the table

local comp = bar(foo()) --The sequence is passed to `bar` as 3 parameters.

local comp2 = bar(bar()) --A single parameter is passed to the outer `bar`.

local comp3 = bar(unpack(bar())) --Unpacks the array into the outer `bar`'s parameters

I find that much more reasonable and consistent than the Python syntax. There is a distinction between a sequence of values and a data structure. `foo` and `bar` may be "conceptually" the same, but functionally they're different. One returns a sequence of values, the other returns a table. If you need to convert a table into a sequence of values, you call `unpack`. If you need to convert a sequence of values into a table, you create a table with it.

Sequences of values in Lua are ephemeral; if you want to store the sequence and preserve it as a sequence, you stick it in a table. But if you just want to compose functions, store values, and the like, you use the sequence as is.

If C++ is going to pattern its functionality off of something, I'd much rather Lua's proper MRV syntax than Python's MRVs-as-arrays (I assume the () syntax is creating an array of some kind).

 


As was stated earlier, doing it this way would be a huge change that likely would never make it past the committee.

But it's the right way to do it. And quite frankly, we don't need multiple return values so badly that we should do it the wrong way just to have some convenient way to do it.

Some people use tuples a lot (some might say to the point of overuse). But you shouldn't steal potentially useful syntax from those C++ programmers who don't use them.

Programmers who have no interest in tuple itself can just use the syntax to return multiple values and unpack them at the call site. They don't have to care that tuple (or something tuple like) is used to accomplish this.

Except that they have to pay the cost for it. Consider something simple like this:

auto single_value()
{
  std
::string a = ...;
 
return a;
}

auto multi_value()
{
  std
::string a = ...;
  std
::vector b = ...;
 
return [a; b];
}

`single_value` is subject to copy elision, so you should feel no problem with using it. Even with a small-string-optimized std::string class, you will get the maximum possible performance if you're using the result to initialize an object.

With language-level multiple return values, `multi_value` would be able to operate under the same principle. `a` and `b` would be considered return values, and they would each be separately subject to elision.

With your way, that's just not possible. If that syntax creates a `tuple`, then it must copy or move initialize the elements of that `tuple`. So even though the `tuple` itself is subject to elision, the creation of it is not.

 
Syntax is a precious commodity, which once apportioned, cannot be taken back.

It should also be noted that reflection, with the full ability to compile-time inspect and generate types with full syntax (including variable names) makes tuples... a dubious proposition for the future of C++. With reflection, you can index a struct by number. With reflection, you can take any typelist and build an honest-to-God struct out of it (which can be standard layout and such).

In short, it is entirely possible that tuples will become obsolete, thus also obsoleting this kind of syntax. So let's not have another std::bind happen here...

[int,long,double] foo();
auto x = foo();

Ok, if tuple is out then what is decltype(x) in the above example doing things your way?

There are two defensible answers to this:

1) A compilation error. You called a function that returns 3 things, but you only captured one. That suggests you weren't paying attention.

2) `int`. The other two types are dropped on the floor.

#2 is probably the way to go. Any sort of aggregate object type is not even up for discussion.

Multiple return values means multiple return values, not "pack multiple values into some kind of object and unpack them later".
 
Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If you want to capture them all in an object, you just ask for it:

auto x = make_tuple(foo());

That is neither hard nor particularly verbose. You can even stick them in an array, if all of the returned values are of the same type:

auto x = make_array(foo());

Or in an arbitrary data structure:

auto x = std::vector<Type>{foo()};

But most people using multiple return values don't capture them in a big object, so we don't make the syntax optimal for that case.

If not using tuple, can you explain in a bit more detail how your proposed solution would actually work?

The details of how it works are more or less the way Lua works.

In C++ parlance, the notion of a "sequence of values" would be a construct. It would not necessarily be an "expression", in the same way that a braced-init-list is not an expression. Since it's a new construct, you can give it whatever properties you want.

Functions can return sequences of values, with the sequence as a full part of its return type. Using the sequence initialization syntax, you can initialize/assign multiple variables to a sequence of values. If a sequence of values is given to a function argument list, the values are placed in that list as arguments. If the sequence of values is used in any other kind of expression, then it resolves down to the first element of that sequence of values.

Initialization/assignment of variables through a sequence would work element by element, in order from left to right.

Miro Knejp

unread,
May 27, 2015, 4:51:58 AM5/27/15
to std-pr...@isocpp.org

On 27 May 2015, at 09:43 , Nicol Bolas <jmck...@gmail.com> wrote:


[int,long,double] foo();
auto x = foo();

Ok, if tuple is out then what is decltype(x) in the above example doing things your way?

There are two defensible answers to this:

1) A compilation error. You called a function that returns 3 things, but you only captured one. That suggests you weren't paying attention.

2) `int`. The other two types are dropped on the floor.

#2 is probably the way to go. Any sort of aggregate object type is not even up for discussion.

Multiple return values means multiple return values, not "pack multiple values into some kind of object and unpack them later".
 
Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If you want to capture them all in an object, you just ask for it:

auto x = make_tuple(foo());

That is neither hard nor particularly verbose. You can even stick them in an array, if all of the returned values are of the same type:

auto x = make_array(foo());

Or in an arbitrary data structure:

auto x = std::vector<Type>{foo()};

But most people using multiple return values don't capture them in a big object, so we don't make the syntax optimal for that case.


I assume Matthew’s point comes from dealing with template code.

template<class F>
??? contrived_example(F f) {
    return f();
}

The author would probably expect that ??? can be plain “auto”, implying “in some places auto can expand to multiple types”. Then there’s the point of what generic code would require to do to consistently capture the return value(s) of a Callable without knowing if there are one or multiple return values.

template<class F>
void contrived_example2(F f) {
    ??? x = f();
    ...
}

Should ??? be something that can potentially capture a single or multiple values depending on the initialization? If so what can be done with x down the pipeline? Or is a concept necessary to overload contrived_example2 for functions with single or multiple return values? Is template code forced into packing every Callable’s invocation into make_tuple() just in case it might return multiple values?

Nicol Bolas

unread,
May 27, 2015, 11:11:00 AM5/27/15
to std-pr...@isocpp.org


On Wednesday, May 27, 2015 at 4:51:58 AM UTC-4, Miro Knejp wrote:

On 27 May 2015, at 09:43 , Nicol Bolas <jmck...@gmail.com> wrote:


[int,long,double] foo();
auto x = foo();

Ok, if tuple is out then what is decltype(x) in the above example doing things your way?

There are two defensible answers to this:

1) A compilation error. You called a function that returns 3 things, but you only captured one. That suggests you weren't paying attention.

2) `int`. The other two types are dropped on the floor.

#2 is probably the way to go. Any sort of aggregate object type is not even up for discussion.

Multiple return values means multiple return values, not "pack multiple values into some kind of object and unpack them later".
 
Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If you want to capture them all in an object, you just ask for it:

auto x = make_tuple(foo());

That is neither hard nor particularly verbose. You can even stick them in an array, if all of the returned values are of the same type:

auto x = make_array(foo());

Or in an arbitrary data structure:

auto x = std::vector<Type>{foo()};

But most people using multiple return values don't capture them in a big object, so we don't make the syntax optimal for that case.


I assume Matthew’s point comes from dealing with template code.

template<class F>
??? contrived_example(F f) {
    return f();
}

The author would probably expect that ??? can be plain “auto”, implying “in some places auto can expand to multiple types”.

Well, `auto` as a "return type" means something very different from `auto` as a variable declaration. So it's perfectly reasonable for `auto` or `decltype(auto)` to deduce multiple return values. Just as it can deduce `void` (ie: zero return values).
 
Then there’s the point of what generic code would require to do to consistently capture the return value(s) of a Callable without knowing if there are one or multiple return values.

template<class F>
void contrived_example2(F f) {
    ??? x = f();
    ...
}

That one's easy. Since you're in generic code and could be getting multiple values, the only way to manipulate them is with a tuple. So that's what you stick them in:

auto x = make_tuple(f());

Matthew Fioravante

unread,
May 27, 2015, 11:11:34 AM5/27/15
to std-pr...@isocpp.org


On Wednesday, May 27, 2015 at 3:43:10 AM UTC-4, Nicol Bolas wrote:

In Lua, you don't need special syntax for returning or capturing multiple values. And quite frankly, I see no reason why you should:

function foo()
 
return 1, 2, 3 --Returns a sequence of 3 values.
end

function bar()
 
return {1, 2, 3} --Returns a single table, containing 3 values
end

local x, y, z = foo() --Declares 3 local variables, initializing them with 3 values from the function.

local w = {foo()} --Stores the sequence of values returned from `foo` in a table (aka: array)

local a = bar() --Stores the table

local comp = bar(foo()) --The sequence is passed to `bar` as 3 parameters.

local comp2 = bar(bar()) --A single parameter is passed to the outer `bar`.

local comp3 = bar(unpack(bar())) --Unpacks the array into the outer `bar`'s parameters

I find that much more reasonable and consistent than the Python syntax. There is a distinction between a sequence of values and a data structure. `foo` and `bar` may be "conceptually" the same, but functionally they're different. One returns a sequence of values, the other returns a table. If you need to convert a table into a sequence of values, you call `unpack`. If you need to convert a sequence of values into a table, you create a table with it.

Sequences of values in Lua are ephemeral; if you want to store the sequence and preserve it as a sequence, you stick it in a table. But if you just want to compose functions, store values, and the like, you use the sequence as is.

If C++ is going to pattern its functionality off of something, I'd much rather Lua's proper MRV syntax than Python's MRVs-as-arrays (I assume the () syntax is creating an array of some kind).

We still need a special "unpacking" syntax to bind the multi return values to variables because the comma operator is already taken and not sufficient for this task. The following looks very confusing and it doesn't even account for multiple return values of different types. It also doesn't let us assign return values directly to pre-existing variables.

[int,int,int] foo();
int bar();
//Initializes x, y, z using foo's return values??
int x, y, z = foo();
//Initializes only c using bar's() return value and leaves a and b uninitialized
int a, b, c = bar();
 

We could allow using auto here:
[int,long,double] foo();
auto x, y, z = foo(); //x is int, y is long, z is double

But this still doesn't allow us to specify cv and ref qualifiers for the individual variables.
 

 


As was stated earlier, doing it this way would be a huge change that likely would never make it past the committee.

But it's the right way to do it. And quite frankly, we don't need multiple return values so badly that we should do it the wrong way just to have some convenient way to do it.

Some people use tuples a lot (some might say to the point of overuse). But you shouldn't steal potentially useful syntax from those C++ programmers who don't use them.

Programmers who have no interest in tuple itself can just use the syntax to return multiple values and unpack them at the call site. They don't have to care that tuple (or something tuple like) is used to accomplish this.

Except that they have to pay the cost for it. Consider something simple like this:

auto single_value()
{
  std
::string a = ...;
 
return a;
}

auto multi_value()
{
  std
::string a = ...;
  std
::vector b = ...;
 
return [a; b];
}

`single_value` is subject to copy elision, so you should feel no problem with using it. Even with a small-string-optimized std::string class, you will get the maximum possible performance if you're using the result to initialize an object.

With language-level multiple return values, `multi_value` would be able to operate under the same principle. `a` and `b` would be considered return values, and they would each be separately subject to elision.

With your way, that's just not possible. If that syntax creates a `tuple`, then it must copy or move initialize the elements of that `tuple`. So even though the `tuple` itself is subject to elision, the creation of it is not.

So whats stopping someone from writing a proposal to allow copy/move elision in your multi_value() example? Just because it can't happen now doesn't mean we can't change the standard. Multi return values would be a primary use case for such a proposal.


 
Syntax is a precious commodity, which once apportioned, cannot be taken back.

It should also be noted that reflection, with the full ability to compile-time inspect and generate types with full syntax (including variable names) makes tuples... a dubious proposition for the future of C++. With reflection, you can index a struct by number. With reflection, you can take any typelist and build an honest-to-God struct out of it (which can be standard layout and such).

In short, it is entirely possible that tuples will become obsolete, thus also obsoleting this kind of syntax. So let's not have another std::bind happen here...

[int,long,double] foo();
auto x = foo();

Ok, if tuple is out then what is decltype(x) in the above example doing things your way?

There are two defensible answers to this:

1) A compilation error. You called a function that returns 3 things, but you only captured one. That suggests you weren't paying attention.

2) `int`. The other two types are dropped on the floor.

#2 is probably the way to go. Any sort of aggregate object type is not even up for discussion.

Then I should also ask you how do you plan on capturing the multiple values? Using a syntax similar to the one I proposed?

This looks bad:
[int,float] foo();
[auto x; auto y] = foo(); //Ok captures into x and y
[auto x; void] = foo(); //Ok captures first into x, throws away second
auto x = foo(); //Captures only first??

The last line there looks like an easy recipe for bugs, especially in generic code. I would suggest making it a compiler error.

Also what if I want to save the sequence so that I can forward it multiple times?
Using your approach, I might suggest allowing auto... to capture all of the return values so that they can be forwarded along. This could work just like a variadic template parameter pack.

void foo(int, float);
[int, float] bar();

auto... x = bar();
foo
(x...);
foo
(x...);

But we still have the generic code problems that Miro brought up.
 

Multiple return values means multiple return values, not "pack multiple values into some kind of object and unpack them later".
 
Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If you want to capture them all in an object, you just ask for it:

auto x = make_tuple(foo());

That is neither hard nor particularly verbose. You can even stick them in an array, if all of the returned values are of the same type:

auto x = make_array(foo());


These require copy/move. In your example, unpacking is free (no-op) and packing requires a copy/move. In my example unpacking is free (using references) and packing is also free, assuming you want to continue using the pack type returned by the function.
 
Or in an arbitrary data structure:

auto x = std::vector<Type>{foo()};

But most people using multiple return values don't capture them in a big object, so we don't make the syntax optimal for that case.

Its not uncommon. Every class that has multiple "values" (e.g unordered_map)  will be returning them in a pack using its default iterators so that one can write a simple range for loop.
 

If not using tuple, can you explain in a bit more detail how your proposed solution would actually work?

The details of how it works are more or less the way Lua works.

In C++ parlance, the notion of a "sequence of values" would be a construct. It would not necessarily be an "expression", in the same way that a braced-init-list is not an expression. Since it's a new construct, you can give it whatever properties you want.

Functions can return sequences of values, with the sequence as a full part of its return type. Using the sequence initialization syntax, you can initialize/assign multiple variables to a sequence of values. If a sequence of values is given to a function argument list, the values are placed in that list as arguments. If the sequence of values is used in any other kind of expression, then it resolves down to the first element of that sequence of values.

Initialization/assignment of variables through a sequence would work element by element, in order from left to right.

I would suggest maybe piggybacking off of varadic template parameter packs (If they can fit into this feature) instead of inventing a yet another brand new thing which is only used for multiple return values which itself does not show up very often.

You mentioned reflection before. I believe with reflection, we can achieve even better interfaces if we designed this MRV feature around unpacking collections of objects.
For example, imagine if we could unpack a pod struct like this (implemented using reflection):

struct kv {
string key;
int value;
};

kv kv
;
[auto k; auto v] = kv;

Why is this so great? Here is an example:

template <typename K, typename V>
class HashMap {
public:
 
struct kv { K key; V value; };
 
class iterator : public std::iterator<std::bidirectional_iterator_tag,kv> { /* ... */ };
 
//...
};

HashMap hashmap = /* something */;

//Iterate using kv struct (more descriptive than pair)
for(auto& kv: hashmap) {
  doSomething
(kv.key, kv.value);
}

//Iterate by unpacking the kv struct first
for([auto& key; auto& value]: hashmap) {
  doSomething
(key, value);
}

//Iterate over only the keys
for([auto& key; void]: hashmap) {}

//Iterate over only the values
for([void; auto& value]: hashmap) {}

If the MRV feature is designed around unpacking collections of objects, then we can get interfaces like this for free from reflection. All 4 example loops are enabled without any additional work by the author of HashMap.

If MRV is designed using sequences, then the class author has to provide separate iterators for returning an MRV sequence and also returning a pack in the default iterators. Class designers are already overburdened with boilerplate for writing type trait tags, iterator types, copy/move constructors, comparison operators, swap operator, etc..

The Lua way of doing things might be elegant, but lua is also a very simple language. It doesn't have the same features and restrictions of C++ and I'm not sure yet if that approach would be the best way.


On Wednesday, May 27, 2015 at 4:51:58 AM UTC-4, Miro Knejp wrote:
I assume Matthew’s point comes from dealing with template code.

template<class F>
??? contrived_example(F f) {
    return f();
}

The author would probably expect that ??? can be plain “auto”, implying “in some places auto can expand to multiple types”. Then there’s the point of what generic code would require to do to consistently capture the return value(s) of a Callable without knowing if there are one or multiple return values.

template<class F>
void contrived_example2(F f) {
    ??? x = f();
    ...
}

Should ??? be something that can potentially capture a single or multiple values depending on the initialization? If so what can be done with x down the pipeline? Or is a concept necessary to overload contrived_example2 for functions with single or multiple return values? Is template code forced into packing every Callable’s invocation into make_tuple() just in case it might return multiple values?


These are very good questions. If multi return is just sugar for returning a pack type, then generic code works just fine without any issue. If not, then we have a lot of questions to answer.

Morwenn

unread,
May 27, 2015, 11:18:40 AM5/27/15
to std-pr...@isocpp.org
Just a thought, but wouldn't it be possible to add a "shortcut" syntax along with the proposed one:

int [a, b, c] = foo();

Or:

auto&& [x, y, z] = bar();

Which would be equivalent to:

[auto&& x; auto&& y; auto&& z] = bar();

It would allow to to have a shorthand syntax while preserving the proposed flexibility.

Matthew Woehlke

unread,
May 27, 2015, 11:23:16 AM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 03:43, Nicol Bolas wrote:
> On Tuesday, May 26, 2015 at 10:53:05 PM UTC-4, Matthew Fioravante wrote:
>> On Tuesday, May 26, 2015 at 9:23:50 PM UTC-4, Nicol Bolas wrote:
>>> Nor does it force you to package parameters into a tuple.
>>
>> But it does, at least at the syntax level
>>
>> def foo():
>> return (1, 2, 3);
>>
>> (x, y, z) = foo();

Actually it doesn't:

>>> def foo():
>>> return 1, 2
>>> a, b = foo()

...but the return is a 'tuple' nevertheless:

>>> type(foo())
<type 'tuple'>

(Basically, the ()'s are optional, and implied if absent.)

> Sorry, my knowledge of Python is limited; I just assumed it worked more or
> less like Lua.

My knowledge of Lua is limited ("effectively nonexistent" is probably
more accurate). Python, however, only supports single return values. It
just has syntax sugar for returning tuples and unpacking tuples.

The latter, btw, works on any iterable:

>>> a, b = (1, 2)
>>> c, d = "cd"

This has significant advantages:

- Unpacking can be used on lots of things, not just some "multiple
return values" concept that doesn't actually exist in Python. For
example, std::pair returned by existing std::map::iterator. This means
that existing code would benefit from the addition of an unpacking
feature as proposed by Matthew F. Your proposal only helps new code and
requires ugly syntax to work in a range-based for over existing
dictionaries.

- The programmer has the option to unpack immediately, or to capture the
entire return value as a container to unpack later, or to use directly
as a container type. Moreover, container operations are possible (e.g.
concatenating two sets of return values).

- It makes slicing operations on the return value set possible. For
example, consuming one value and passing the others as a set along to
some other function. (With '...' unpacking, it allows metaprogramming
tricks like invoking some function for each return value.)

- It simplifies templates (Miro's point); templates code does not
necessarily need to be specialized depending on whether a function used
in a template returns a single or multiple values.

- It doesn't require massive changes to the standard or creation of a
new ABI.

The second in particular, plus unpacking into parameter lists, makes
something like this possible:

[int, int] foo();
[double, int] bar();
void something_different(int, int, double, int)'

auto&& x = foo();
auto&& y = bar();
something_different([*](concat(x, y));

(Digression: Python actually does strictly enforces that unpacking
matches the element count of the tuple being unpacked; no 'tail tuple'.
Possibly we should think about only providing that mode to start with;
we can always add tail unpacking via '...' later.)

> In Lua, you don't need special syntax for returning or capturing multiple
> values. And quite frankly, I see no reason why you should:
>
> function foo()
> return 1, 2, 3 --Returns a sequence of 3 values.
> end

Like Python, I don't even see a return type specified there, so I'm not
sure what you mean by "don't need special syntax".

In Python, "return x, y" is syntactic sugar for "return (x, y)". I
suppose we could allow similar sugar in C++, possibly with the
requirement that the function was declared with the tuple sugar (as in
the declarations in the above example) in order to avoid changing the
meaning of existing code w.r.t. 'operator,'.

> I find that much more reasonable and consistent than the Python syntax.
> There is a distinction between a sequence of values and a data structure.

I'm not convinced this is really *that* different from the Pythonic
distinction between 'tuple' and 'list'. Moreover, C++ has no concept of
"a sequence of values" existing outside of some containing type such as
std::tuple. I remain unconvinced that the significant effort to add such
a concept is worthwhile.

> auto multi_value()
> {
> std::string a = ...;
> std::vector b = ...;
> return [a; b];
> }

Is there a reason we can't introduce a new type, probably along the
lines of initializer_list, that would allow in-place construction but
would otherwise behave like std::tuple?

I don't think either Matthew F. or myself are wedded to the return type
*actually being* std::tuple, just that it should behave like std::tuple
(i.e. a single 'object' subject to sequence operations, get<N>, etc.).

For that matter, I at least am content not saying *anything* about
multiple return values. I still consider it an orthogonal issue. We just
need to coordinate so that immediate unpacking is allowed to leverage
RVO. (And I still think unpacking - as in Python - should be permitted
on anything that provides get<N>, be that "value sequences", std::tuple,
std::pair, std::array, QSize, my_funky_class, etc.) Multiple return
values might be the most common use case, but it not the *only* use
case. And there is enormous value on unpacking being useful with
existing API's.

>> [int,long,double] foo();
>> auto x = foo();
>>
>> Ok, if tuple is out then what is decltype(x) in the above example doing
>> things your way?
>
> There are two defensible answers to this:
>
> 1) A compilation error. You called a function that returns 3 things, but
> you only captured one. That suggests you weren't paying attention.
>
> 2) `int`. The other two types are dropped on the floor.
>
> #2 is probably the way to go. Any sort of aggregate object type is not even
> up for discussion.

I disagree on both counts. There are advantages to having an aggregate
object, as explained above. And silently dropping return values should
definitely not be allowed.

Maybe you and Matthew F. can write a paper together that presents both
proposals along with a comparison of the advantages and disadvantages of
both?

>> If not using tuple, can you explain in a bit more detail how your proposed
>> solution would actually work?

Nicol, I'd still like to see your equivalent of:

for([auto&& k; auto&& v] : some_map)

--
Matthew

Edward Catmur

unread,
May 27, 2015, 11:25:15 AM5/27/15
to std-pr...@isocpp.org
I really don't like the idea of having reflection invoked by a core language feature; remember that reflection can (by design) bypass access control. Instead, why not allow MRV to invoke a conversion function? It's minimal boilerplate for the library author:

  struct kv { K key; V value; operator[K, V] { return [key, value]; } };

MRV conversion functions could be added to pair and tuple in the library, meaning there's no need to invoke reflection or privilege std::get<N>.

Matthew Woehlke

unread,
May 27, 2015, 11:31:12 AM5/27/15
to std-pr...@isocpp.org
Okay, so composition in the face of generic code, where I also need to
store the return value(s) at some point for whatever reason, *requires*
the ability to unpack a std::tuple into an argument list.

That's true either way... but just sayin'...

--
Matthew

Thiago Macieira

unread,
May 27, 2015, 11:33:06 AM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 11:21:47 Matthew Woehlke wrote:
> In Python, "return x, y" is syntactic sugar for "return (x, y)". I
> suppose we could allow similar sugar in C++, possibly with the
> requirement that the function was declared with the tuple sugar (as in
> the declarations in the above example) in order to avoid changing the
> meaning of existing code w.r.t. 'operator,'.

Please don't, because operator, can be overloaded. Using it is one of the
tricks to handling void returns.

Matthew Fioravante

unread,
May 27, 2015, 11:34:00 AM5/27/15
to std-pr...@isocpp.org

That's why I suggested it working by default for only POD structs which are all public and standard layout where compiler generating unpacking makes sense without surprises.
 
Instead, why not allow MRV to invoke a conversion function? It's minimal boilerplate for the library author:

  struct kv { K key; V value; operator[K, V] { return [key, value]; } };

MRV conversion functions could be added to pair and tuple in the library, meaning there's no need to invoke reflection or privilege std::get<N>.

Sure, and this could be how one adds support for unpacking to any arbitrary type and its much easier than implementing std::get. One has to be careful with copies however.

Matthew Fioravante

unread,
May 27, 2015, 11:43:36 AM5/27/15
to std-pr...@isocpp.org


On Wednesday, May 27, 2015 at 11:33:06 AM UTC-4, Thiago Macieira wrote:
On Wednesday 27 May 2015 11:21:47 Matthew Woehlke wrote:
> In Python, "return x, y" is syntactic sugar for "return (x, y)". I
> suppose we could allow similar sugar in C++, possibly with the
> requirement that the function was declared with the tuple sugar (as in
> the declarations in the above example) in order to avoid changing the
> meaning of existing code w.r.t. 'operator,'.

Please don't, because operator, can be overloaded. Using it is one of the
tricks to handling void returns.


I'm happy with just using good old braced initialization, or other forms of initialization that already exist if you so choose.

[int,float] foo() { return { 1, 2.0f }; }
[int,float] bar() { return [int,float](1, 2.0f); }

There's nothing ambiguous or overly verbose about that and it doesn't ask the standard to invent more rules and syntax. I don't need additional sugar to avoid typing {}. If MRV is implemented using a packed type, this syntax is natural because we are constructing the pack object to be returned.


Matthew Woehlke

unread,
May 27, 2015, 11:45:55 AM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 11:33, Thiago Macieira wrote:
> On Wednesday 27 May 2015 11:21:47 Matthew Woehlke wrote:
>> In Python, "return x, y" is syntactic sugar for "return (x, y)". I
>> suppose we could allow similar sugar in C++, possibly with the
>> requirement that the function was declared with the tuple sugar (as in
>> the declarations in the above example) in order to avoid changing the
>> meaning of existing code w.r.t. 'operator,'.
>
> Please don't, because operator, can be overloaded. Using it is one of the
> tricks to handling void returns.

Yes, thank you, that's sort of my point :-).

IMO (and IYIO¹ apparently) it's dangerous to allow MRV with no more
syntax than 'return a, b'. *At minimum* I would restrict this to
functions that have explicitly declared MRV's (which is what I was
suggesting).

Personally, I'm not convinced (as Nicol is asserting) that we need it at
all. *I* don't have a problem requiring an MRV return to be wrapped in
either {}'s or []'s, and in fact I'd rather prefer that.

(¹ In Your Informed Opinion, as in, I'm just talking, but you (Thiago)
know what you're talking about :-).)

--
Matthew

Miro Knejp

unread,
May 27, 2015, 11:55:20 AM5/27/15
to std-pr...@isocpp.org
My point was specifically that the author of the template function doesn’t necessarily know whether f has multiple return values without type_traiting the hell out of it. I understand this would require basically almost every template function in existence with Callables to wrap every invocation in make_tuple(), just in case. Unless it is suggested to use concept shenanigans to overload the template function with concepts for all combinations of MRV/SRV Callable types accepted by the template function. Seems a bit excessive.

Nicol Bolas

unread,
May 27, 2015, 11:59:36 AM5/27/15
to std-pr...@isocpp.org
On Wednesday, May 27, 2015 at 11:11:34 AM UTC-4, Matthew Fioravante wrote:
On Wednesday, May 27, 2015 at 3:43:10 AM UTC-4, Nicol Bolas wrote:
Except that they have to pay the cost for it. Consider something simple like this:

auto single_value()
{
  std
::string a = ...;
 
return a;
}

auto multi_value()
{
  std
::string a = ...;
  std
::vector b = ...;
 
return [a; b];
}

`single_value` is subject to copy elision, so you should feel no problem with using it. Even with a small-string-optimized std::string class, you will get the maximum possible performance if you're using the result to initialize an object.

With language-level multiple return values, `multi_value` would be able to operate under the same principle. `a` and `b` would be considered return values, and they would each be separately subject to elision.

With your way, that's just not possible. If that syntax creates a `tuple`, then it must copy or move initialize the elements of that `tuple`. So even though the `tuple` itself is subject to elision, the creation of it is not.

So whats stopping someone from writing a proposal to allow copy/move elision in your multi_value() example?

The fact that C++ can't do that. Remember: your [values...] syntax equates to the construction of a `std::tuple` from the given values. Well, the memory for those values has already been allocated, and if those are variables, then the memory there must have been allocated elsewhere. And because `std::tuple` is not special, because it must follow the regular rules of C++, there's no way for a compiler to know (without very non-local optimizations) exactly what that constructor is doing.

Furthermore, even if a compiler did know... what of it? Are you suggesting that the compiler can, or should be allowed to, initialize the memory space in a `tuple` before the object is even constructed, then ignore the tuple's constructor?

I'd hate to have to implement that in a compiler.
 
Just because it can't happen now doesn't mean we can't change the standard. Multi return values would be a primary use case for such a proposal.

If the standard were changed, it couldn't be changed in anything remotely like a generic way. It would have to be based on a basic assumption that `[values..,]` syntax doesn't generate a `std::tuple`, but some other compiler-generated object type. And this type would be treated specially by the language, being allowed to skip constructors.

Otherwise, you'd have to define some general way for the compiler to know whether a constructor parameter would be stored in a tuple exactly as-is or not.

Also what if I want to save the sequence so that I can forward it multiple times?
Using your approach, I might suggest allowing auto... to capture all of the return values so that they can be forwarded along. This could work just like a variadic template parameter pack.

void foo(int, float);
[int, float] bar();

auto... x = bar();
foo
(x...);
foo
(x...);


Now that... that sounds like an idea. I've always wanted a way to build a template parameter pack.

I think that it would be possible to consider the "sequence of values" concept to be a template parameter pack. Thus, functions can create and return parameter packs, which can be unpacked by the user, or not unpacked by the user, as the case may be.

So if we consider [values...] to create a parameter pack, then the assignment syntax would look something like this:

[] = pack...;

The specific syntax can be worked out (I don't think any of these things can move forward with `[]` notation, due to various parsing issues). But that would be the general idea.

It wouldn't be as good as the Lua syntax. But I think this way regularizes parameter packs as "sequences of values" in a useful way.
 
But we still have the generic code problems that Miro brought up.
 

Multiple return values means multiple return values, not "pack multiple values into some kind of object and unpack them later".
 
Another kind of typelist which is standard layout compatible? Or are we no longer able to capture all of the return values into one object?

If you want to capture them all in an object, you just ask for it:

auto x = make_tuple(foo());

That is neither hard nor particularly verbose. You can even stick them in an array, if all of the returned values are of the same type:

auto x = make_array(foo());


These require copy/move. In your example, unpacking is free (no-op) and packing requires a copy/move. In my example unpacking is free (using references) and packing is also free, assuming you want to continue using the pack type returned by the function.

But as previously stated, packing is not free in your system. Packing requires the function doing the packing (the one returning the tuple) to either put the values in the tuple itself or copy/move the values into the tuple. Until you actually propose a way to fix that problem (rather than simply declaring that it can be fixed... somehow), your proposal must be evaluated as though such a solution doesn't exist.

 
Or in an arbitrary data structure:

auto x = std::vector<Type>{foo()};

But most people using multiple return values don't capture them in a big object, so we don't make the syntax optimal for that case.

Its not uncommon. Every class that has multiple "values" (e.g unordered_map)  will be returning them in a pack using its default iterators so that one can write a simple range for loop.

And how often do people iterate through maps? The main purpose of a map is to efficiently look up items by a key.

So I'd say it's still relatively uncommon. It's certainly not common enough to make a special syntax for tuples.

... that being said, I wouldn't mind `...` being able to unpack a tuple or other tuple-like construct in addition to a proper parameter pack (note: this is barring any syntactic issues, like breaking existing code). Granted, it wouldn't resolve that problem, but it would normalize tuple as the way to store a parameter pack as a free-standing object, rather than a compiler construct.

Thiago Macieira

unread,
May 27, 2015, 12:06:07 PM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 11:45:43 Matthew Woehlke wrote:
> IMO (and IYIO¹ apparently) it's dangerous to allow MRV with no more
> syntax than 'return a, b'. *At minimum* I would restrict this to
> functions that have explicitly declared MRV's (which is what I was
> suggesting).

My point is that

return a, b;

Already has a meaning. Imagine (contrived example):

template <typename T> auto void do(T &&t)
{
return SomeAction(), t();
}

[int, float] void f();

Does do<f> return [int, float]? Or does it return [SomeAction, [int, float]]?

"Why would you do such a thing?"

template <typename T> T operator,(SomeAction, const T &t)
{
debug(t);
return t;
};

Not sure this compiles and won't produce "void value not ignored as it
should". This is a contrived and untested example. But I have used operator,
to debug return values before in generic code. Operator, is the only one that
can "take" a void type as the second parameter.

http://code.qt.io/cgit/qt/qtbase.git/commit/?id=1991647af1de2d0b3812d57363dddfeac6179a68

Matthew Woehlke

unread,
May 27, 2015, 12:10:58 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-26 20:14, Nicol Bolas wrote:
> On Tuesday, May 26, 2015 at 5:31:10 PM UTC-4, Matthew Woehlke wrote:
>> If we had multiple return values as a first class language feature, why
>> should I not expect to be able to write the opposite of std::tie? :-)
>
> ... std::tie takes a tuple and exports the members of that tuple into
> variables.

Er... okay, I don't use std::tie. Ugh. That looks like a wart that
should die :-); proper unpacking syntax would make it superfluous:

auto t = foo(); // decltype(t) == tuple<int, int, int>

// Old
int x, y, z;
std::tie(x, y, z) = t;

// New (v1)
int x, y, z;
[x; y; z] = t; // Much more concise

// New (v2)
[auto x; auto y; auto z] = t; // Concise, and can declare with 'auto'


What I meant was that MRV by value sequence (your proposal) should make
it possible for me to write 'unpack', as in this example:

int foo(int, int, int);

std::tuple<int, int, int> params = { ... };
foo(unpack(params));

And...

>> On 2015-05-26 16:31, Nicol Bolas wrote:
>>> So while they're not technically orthogonal, it's highly unlikely that we'd
>>> ever get both.

...if we also have a way to unpack MRV's (which I assume we do, since
you object so strongly to anything container-like for MRV's), doesn't
that mean we *do* have the ability to unpack a std::tuple with your
proposal? (Without resorting to the ugly std::tie?)

That's what I was trying to say.

I still don't see why these shouldn't be orthogonal, especially if we
make some new quasi-container a la initializer_list for MRV's. Unpacking
can be something we introduce *now*, which immediately fixes at least
the map iteration problem, and allows MRV's via std::tuple with no other
major changes. Separately/later we can add "proper" MRV support in a way
that's compatible with the existing unpacking mechanism (and can be
stored / has implicit conversion to something container-like for the
reasons mentioned elsewhere). This makes both changes much more modest,
has no major drawbacks that I can see, and lets us tackle the problem in
more bite-sized chunks.

--
Matthew

Matthew Woehlke

unread,
May 27, 2015, 12:28:57 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 11:59, Nicol Bolas wrote:
> On Wednesday, May 27, 2015 at 11:11:34 AM UTC-4, Matthew Fioravante wrote:
>> So whats stopping someone from writing a proposal to allow copy/move
>> elision in your multi_value() example?
>
> The fact that C++ can't do that. Remember: your [values...] syntax equates
> to the construction of a `std::tuple` from the given values. [...]
>
> If the standard were changed, it couldn't be changed in anything remotely
> like a generic way. It would have to be based on a basic assumption that
> `[values..,]` syntax doesn't generate a `std::tuple`, but some other
> compiler-generated object type. And this type would be treated specially by
> the language, being allowed to skip constructors.

Great! Let's go with that. And lets make this "CGO" behave like
std::tuple, at least as far as being deterministic (i.e. as far as the
decltype() for a distinct type sequence) and implementing get<N>. Then
it will work with the unpacking proposal, which can be orthogonal.

>> auto... x = bar();
>> foo(x...);
>> foo(x...);
>
> Now that... that sounds like an idea. I've always wanted a way to build a
> template parameter pack.

This can be a third orthogonal proposal, e.g. '[*]' or '...' to accept
an MRV "object" as some sort of special "value sequence". Or if an MRV
"object" is implicitly unpacked as parameters, '[*]' can be written as a
template function.

I'd prefer option 1, '[*]' to convert a "container" to a parameter list.
(More versatile, if nothing else...)

>> Its not uncommon. Every class that has multiple "values" (e.g
>> unordered_map) will be returning them in a pack using its default
>> iterators so that one can write a simple range for loop.
>
> And how often do people iterate through maps? The main purpose of a map is
> to efficiently look up items by a key.

...or to store data that logically follows a "key"/"value" structure for
which there already exists a "key" and "value" type, but not a type that
combines the two. (And/or where sometimes look-up is needed, and
sometimes doing something with all items is needed.)

I run into this often enough that I have helper macros associated with
it, and a helper class to perform key/value iteration over Qt
associative containers (whose iterator's operator* only gives you the
value, such that the key is not accessible to foreach() / range-based
for). I've even proposed the latter for addition to Qt (though I haven't
yet gone back and done anything about submitting it).

> So I'd say it's still relatively uncommon. It's certainly not common enough
> to make a special syntax for tuples.

By itself, maybe not, but it's one use case of many for the same
feature. I think it's common enough that a solution to MRV unpacking
that doesn't also cover these other use cases when they are so clearly
related, is questionable.

--
Matthew

Matthew Fioravante

unread,
May 27, 2015, 12:30:13 PM5/27/15
to std-pr...@isocpp.org


On Wednesday, May 27, 2015 at 11:59:36 AM UTC-4, Nicol Bolas wrote:
On Wednesday, May 27, 2015 at 11:11:34 AM UTC-4, Matthew Fioravante wrote:
On Wednesday, May 27, 2015 at 3:43:10 AM UTC-4, Nicol Bolas wrote:
Except that they have to pay the cost for it. Consider something simple like this:

auto single_value()
{
  std
::string a = ...;
 
return a;
}

auto multi_value()
{
  std
::string a = ...;
  std
::vector b = ...;
 
return [a; b];
}

`single_value` is subject to copy elision, so you should feel no problem with using it. Even with a small-string-optimized std::string class, you will get the maximum possible performance if you're using the result to initialize an object.

With language-level multiple return values, `multi_value` would be able to operate under the same principle. `a` and `b` would be considered return values, and they would each be separately subject to elision.

With your way, that's just not possible. If that syntax creates a `tuple`, then it must copy or move initialize the elements of that `tuple`. So even though the `tuple` itself is subject to elision, the creation of it is not.

So whats stopping someone from writing a proposal to allow copy/move elision in your multi_value() example?

The fact that C++ can't do that. Remember: your [values...] syntax equates to the construction of a `std::tuple` from the given values. Well, the memory for those values has already been allocated, and if those are variables, then the memory there must have been allocated elsewhere. And because `std::tuple` is not special, because it must follow the regular rules of C++, there's no way for a compiler to know (without very non-local optimizations) exactly what that constructor is doing.

If tuple (or whatever type we use, lets call it tuple) constructor is inline and just trivially constructs the members, as it should be then the compiler can do whatever it wants. I don't see why the constructor cannot construct the string directly in the memory to be used by the returned tuple.


Furthermore, even if a compiler did know... what of it? Are you suggesting that the compiler can, or should be allowed to, initialize the memory space in a `tuple` before the object is even constructed, then ignore the tuple's constructor?

Sure, if the constructor is inline and we can prove "as-if" compatible behavior why not?
 

Your performance concerns are valid and I agree that any proposal would need to address them directly and prove that they can either be mitigated or that they are worth the cost.
 

Matthew Woehlke

unread,
May 27, 2015, 12:44:22 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 12:06, Thiago Macieira wrote:
> On Wednesday 27 May 2015 11:45:43 Matthew Woehlke wrote:
>> IMO (and IYIO¹ apparently) it's dangerous to allow MRV with no more
>> syntax than 'return a, b'. *At minimum* I would restrict this to
>> functions that have explicitly declared MRV's (which is what I was
>> suggesting).
>
> My point is that
>
> return a, b;
>
> Already has a meaning.

*Exactly*. You must be misinterpreting something I said, because we are
violently agreeing with each other :-). (Maybe because I said "suppose
we could", which doesn't do a great job conveying "eew... um... do we
really *have* to?".)

This is what happened:

Nicol: I don't want to have to type extra symbols for MRV.
Me: Um... well... I *guess* I could see that, but it seems dangerous.
You: Absolutely it is dangerous!
Me: Yes!

...and then I thanked you for putting my uneasy mutterings into a clear
example of why said mutterings are fully justified.

TL;DR: We both agree that changing the meaning of 'return a, b' is bad
and both of us would rather not do that.

> Imagine (contrived example):
>
> template <typename T> auto void do(T &&t)
> {
> return SomeAction(), t();
> }
>
> [int, float] void f();
>
> Does do<f> return [int, float]? Or does it return [SomeAction, [int, float]]?

The former; 'do' wasn't explicitly declared to have MRV. (This is even
if we allow MRV without new syntax, which as stated, I'm not comfortable
with, for the same reasons you're objecting to it.)

--
Matthew

Edward Catmur

unread,
May 27, 2015, 1:25:48 PM5/27/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
That's a library addition; we can even do it with Python syntax, since std::tuple doesn't provide unary operator* yet:

template<class... T> [T...] tuple<T...>::operator*() { return [get<0...sizeof...(T...)>(*this)...]; }

allowing:

auto x = make_tuple(f());
g
(*x);

Thiago Macieira

unread,
May 27, 2015, 1:44:50 PM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 12:44:11 Matthew Woehlke wrote:
> > template <typename T> auto void do(T &&t)
> > {
> > return SomeAction(), t();
> > }
> >
> >
> >
> > [int, float] void f();
> >
> >
> >
> > Does do<f> return [int, float]? Or does it return [SomeAction, [int,
> > float]]?
> The former; 'do' wasn't explicitly declared to have MRV. (This is even
> if we allow MRV without new syntax, which as stated, I'm not comfortable
> with, for the same reasons you're objecting to it.)

Ok, so

template <typename T> decltype(t()) do(T &&);

Is that an explicit MRV?

Matthew Woehlke

unread,
May 27, 2015, 2:01:33 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 13:44, Thiago Macieira wrote:
> On Wednesday 27 May 2015 12:44:11 Matthew Woehlke wrote:
>> On 2015-05-27 12:06, Thiago Macieira wrote:
>>> template <typename T> auto void do(T &&t)
>>> {
>>> return SomeAction(), t();
>>> }
>>>
>>> [int, float] void f();
>>>
>>> Does do<f> return [int, float]? Or does it return [SomeAction, [int,
>>> float]]?
>>
>> The former; 'do' wasn't explicitly declared to have MRV. (This is even
>> if we allow MRV without new syntax, which as stated, I'm not comfortable
>> with, for the same reasons you're objecting to it.)
>
> Ok, so
>
> template <typename T> decltype(t()) do(T &&);
>
> Is that an explicit MRV?

I *want* to say "yes"... and... eep, yeah, I think you're right, that
would at best result in a compile error for code that used to work :-).
So, there is no way to allow MRV without some syntax to denote the MRV
list. I wasn't fond of it anyway, but that's the proverbial last nail in
the coffin.

Great analysis. Thanks again!

(That said, if we had MRV, I think your example should compile as-is...
i.e. no additional syntax required to 'pass through' an MRV object as an
MRV. But that's an aside.)

--
Matthew

Matthew Woehlke

unread,
May 27, 2015, 2:09:07 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 13:25, Edward Catmur wrote:
> On Wednesday, 27 May 2015 16:31:12 UTC+1, Matthew Woehlke wrote:
>> Okay, so composition in the face of generic code, where I also need to
>> store the return value(s) at some point for whatever reason, *requires*
>> the ability to unpack a std::tuple into an argument list.
>
> That's a library addition; we can even do it with Python syntax, since
> std::tuple doesn't provide unary operator* yet [...] allowing:
>
> auto x = make_tuple(f());
> g(*x);

...that assumes that a std::value_sequence<...> (or whatever) is
implicitly expanded when it appears in an argument list, which is *not*
a library addition. Also, that only works if it is not possible to treat
a value sequence as a single object, to which I (pardon the homograph)
object.

Plus, I'd rather have a unary "operator" '[*]' (or whatever) that
generically unpacks something that supports get<N> into a sequence of
values. This would work with e.g. QMap::iterator which *does* already
have an operator*.

--
Matthew

Nicol Bolas

unread,
May 27, 2015, 4:06:23 PM5/27/15
to std-pr...@isocpp.org
On Wednesday, May 27, 2015 at 12:30:13 PM UTC-4, Matthew Fioravante wrote:
On Wednesday, May 27, 2015 at 11:59:36 AM UTC-4, Nicol Bolas wrote:
On Wednesday, May 27, 2015 at 11:11:34 AM UTC-4, Matthew Fioravante wrote:
On Wednesday, May 27, 2015 at 3:43:10 AM UTC-4, Nicol Bolas wrote:
Except that they have to pay the cost for it. Consider something simple like this:

auto single_value()
{
  std
::string a = ...;
 
return a;
}

auto multi_value()
{
  std
::string a = ...;
  std
::vector b = ...;
 
return [a; b];
}

`single_value` is subject to copy elision, so you should feel no problem with using it. Even with a small-string-optimized std::string class, you will get the maximum possible performance if you're using the result to initialize an object.

With language-level multiple return values, `multi_value` would be able to operate under the same principle. `a` and `b` would be considered return values, and they would each be separately subject to elision.

With your way, that's just not possible. If that syntax creates a `tuple`, then it must copy or move initialize the elements of that `tuple`. So even though the `tuple` itself is subject to elision, the creation of it is not.

So whats stopping someone from writing a proposal to allow copy/move elision in your multi_value() example?

The fact that C++ can't do that. Remember: your [values...] syntax equates to the construction of a `std::tuple` from the given values. Well, the memory for those values has already been allocated, and if those are variables, then the memory there must have been allocated elsewhere. And because `std::tuple` is not special, because it must follow the regular rules of C++, there's no way for a compiler to know (without very non-local optimizations) exactly what that constructor is doing.

If tuple (or whatever type we use, lets call it tuple) constructor is inline and just trivially constructs the members, as it should be then the compiler can do whatever it wants. I don't see why the constructor cannot construct the string directly in the memory to be used by the returned tuple.

OK, let's stop using `tuple`, because `tuple` is not able to do anything even remotely like what you're talking about. `tuple`'s constructor is a highly recursive beast. Many implementations of `tuple` don't even store the values as their particular types; they use std::aligned_storage or something internally. Lastly, we don't want compilers to rely on the implementation details of `tuple`, much like `{}` syntax doesn't return an array, but a special type supplied by the compiler.

So let's just say you have a simple struct:

struct Simple
{
  std
::string str;
 
int foo;
};

No tricks, no internal gimmicks; it is the simplest possible case. Given that:

std::string in = ...;

return Simple{in, 2};

Right now, the standard requires that `in` be copied into `Simple`. You want the standard to allow the compiler to elide the copy, and thus construct `in` in the place where `Simple` will be constructed.

There's one really big hurdle here. You are literally talking about eliding part of a constructor. After all, you're not eliding the entire constructor here; you still need to copy that 2 in there. So... how would "partial constructor elision" work? Would it only be restricted to aggregates? If not, then the compiler has to somehow cull out parts of a constructor.

Even if you limit it to only constructors that have no function body and are "inline", you're still requiring the compiler to break down a constructor into separate steps. And if you allow constructor bodies, how exactly does that work?



Furthermore, even if a compiler did know... what of it? Are you suggesting that the compiler can, or should be allowed to, initialize the memory space in a `tuple` before the object is even constructed, then ignore the tuple's constructor?

Sure, if the constructor is inline and we can prove "as-if" compatible behavior why not?

Because elision is not "'as-if' compatible behavior". Elision changes visible behavior; that's why the standard has to explicitly allow it under certain circumstances.

The questions you need to answer are:

1) How do you specify it? Oh, I know how you say it in English. But what is it in standardese? The language for return value elision is very simple. It's one sentence in 12.8 section 31. Your "partial constructor elision" is much more complex.

2) Can you actually implement it? This requires some significant knowledge of compilers. Can compilers adequately detect when they can employ elision? Can compilers break constructors down into pieces that they can choose to execute or not, based on which parts are elided? This is very much non-trivial, and you should not assume that they can simply work it out.

Thiago Macieira

unread,
May 27, 2015, 4:13:59 PM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 12:10:32 Matthew Woehlke wrote:
> What I meant was that MRV by value sequence (your proposal) should make
> it possible for me to write 'unpack', as in this example:
>
> int foo(int, int, int);
>
> std::tuple<int, int, int> params = { ... };
> foo(unpack(params));

This is unlikely.

Far more likely:

unpack(foo, params);

This is already achievable today

template <typename F, typename... Args, size_t... N> auto
unpack(F &&f, std::tuple<Args...> &&args,
std::index_sequence<N...> = std::index_sequence_for<Args...>())
{
using namespace std;
return f(std::forward<Args>(get<N>(args))...);

Cleiton Santoia

unread,
May 27, 2015, 4:16:10 PM5/27/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net

Let me give my 2 cents : 

A "variadic initializer list" or some analogue concept may solve part of this problem.

template<typename ...T> class initializer_list {}

or even a different class 

template<typename ...T> class var_initializer_list {}

This also allow "implicit" construction of tuples and pairs via uniform initialization.

So instead of 
template<typename ...Types> 
explicit tuple (const Types&... elems);

We might use 

template<typename ...Types> 
tuple (const initializer_list<Types...>& elems);


BR
Cleiton

Thiago Macieira

unread,
May 27, 2015, 4:26:57 PM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 13:16:10 Cleiton Santoia wrote:
> template<typename ...Types>
>
> tuple (const initializer_list<Types...>& elems);

I like this, since it also allows me to do heterogeneous maps without having
to create a std::initializer_list<Entry<Typesafe, Typesafe>>. I can't make
that "Typesafe" inner type work for types unknown at build time without heavy
type erasure.

make_map({{"foo", 1}, {"bar", "hello"}, {0, false}});

maybe without the outer { } too

Matthew Woehlke

unread,
May 27, 2015, 4:42:24 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 16:06, Nicol Bolas wrote:
> struct Simple
> {
> std::string str;
> int foo;
> };
>
> std::string in = ...;
>
> return Simple{in, 2};
>
> [...] So... how would "partial constructor elision" work? Would it
> only be restricted to aggregates?

At least restricted to only aggregates, yes. Anything else, as you note,
is... "fun".

You are *also* proposing something almost, if not exactly, equivalent
for your "value sequence" concept. If we define "value sequence" as some
tuple-like-but-not-tuple "object" for which this sort of in-place
construction is allowed, then I don't see why this needs to be tightly
coupled to unpacking.

> The questions you need to answer are [...] Can you actually implement
> it?

Can you actually implement your MRV concept? Seems like it has most of
the same issues...

Aside from determining when to employ elision, at least, but then, if
the return type supports unpacking (which it obviously should), I don't
see why we need to get so worked up about it.

Really, what we need is a) generalized unpacking, and b) a special
tuple-like type that can only be "constructed" in certain ways, which
includes an appropriate form of RVO in-place construction. I don't see
why these need to be conjoined.

--
Matthew

Matthew Woehlke

unread,
May 27, 2015, 5:05:45 PM5/27/15
to std-pr...@isocpp.org
On 2015-05-27 16:13, Thiago Macieira wrote:
> On Wednesday 27 May 2015 12:10:32 Matthew Woehlke wrote:
>> What I meant was that MRV by value sequence (your proposal) should make
>> it possible for me to write 'unpack', as in this example:
>>
>> int foo(int, int, int);
>>
>> std::tuple<int, int, int> params = { ... };
>> foo(unpack(params));
>
> This is unlikely.

Why? If Nicol's "value sequence" idea was accepted, I mean.

My understanding of that idea is that a "value sequence" is NOT a
container, and can be used where multiple values are expected. I.e. this
should work:

[int, int] foo();
void bar(int, int);

bar(foo());

If that works, then I should also be able to write a template 'unpack'
as in the above example.

That said, this notion of a single "entity" that is really multiple
values... bugs me. It would be one thing if we had syntax that said
'hey, this here is multiple values that I would like passed as separate
parameters', but my understanding is that that *isn't* what Nicol is
suggesting. Given that we don't have any similar concept in the language
currently, I'm not particularly fond of introducing it.

This is one reason why I think that MRV's, however they happen under the
hood, should still be "container-like". Which in turn strongly suggests
that unpacking should be explicit and not too tightly coupled to MRV's.
(Which in turn is what I've been saying all along.)

I still think we have three *orthogonal* ideas:

1. Unpacking via assignment/declaration (original proposal).
2. MRV optimization, especially w.r.t. RVO.
3. Unpacking into an argument list (i.e. function calls).

I'm not convinced that *any* of these are dependent on each other, not
even 2 -> 1. Yes, it's obnoxious right now to write 'auto&& a = foo();
auto& x = get<0>(a);', but why shouldn't RVO (given that we have 2) work
for that?

--
Matthew

Nicol Bolas

unread,
May 27, 2015, 6:05:15 PM5/27/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Wednesday, May 27, 2015 at 4:42:24 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-27 16:06, Nicol Bolas wrote:
> struct Simple
> {
>   std::string str;
>   int foo;
> };
>
> std::string in = ...;
>
> return Simple{in, 2};
>
> [...] So... how would "partial constructor elision" work? Would it
> only be restricted to aggregates?

At least restricted to only aggregates, yes. Anything else, as you note,
is... "fun".

You are *also* proposing something almost, if not exactly, equivalent
for your "value sequence" concept. If we define "value sequence" as some
tuple-like-but-not-tuple "object" for which this sort of in-place
construction is allowed, then I don't see why this needs to be tightly
coupled to unpacking.

This is the thing you don't seem to fully understand.

I do not want a "value sequence" to be any kind of C++ object. It should not be a type with a value. It should not be memory stored in some location. Remember: template parameter packs are not objects either. You can't copy them, move them, reference them, or use them in anything other than a parameter pack expression. Similarly, braced-init-lists are not elements of an expression; the construct `{4.3, "foo"}` is not a legal expression. It is merely a construct which can be used in certain locations to initialize values in C++.

Value sequences should be like those. Such constructs can be arbitrarily assigned behavior that we find useful, rather than having to fit into existing paradigms.

I don't want them to be objects of any sort.

> The questions you need to answer are [...] Can you actually implement
> it?

Can you actually implement your MRV concept? Seems like it has most of
the same issues...

That's a fair question, but I fail to see the implementation problems. Certainly, they're nothing like those for defining and implementing elision out of some type.

Right now, as I understand it, output values at the compiled code level are returned as either registers (for small objects), or the caller provides a piece of stack space to store the returned object (for larger objects). Since the caller knows the function's MRV signature, it knows how much space to allocate. And you can even still only pass a single "value" for the return value, knowing exactly how the function will fill in the multiple objects in the value.

Indeed, that's how elision works. The caller is given the output value as a pointer to stack memory. Internally, the C++ code says to create some value and then return it. The compiler sees the opportunity for elision and simply decides to allocate that value in the return value space, thus making the return statement a noop. The space the caller provides is simply the object that the caller is initializing, if the caller is initializing an object.

If the caller is not initializing the object, then the caller creates some temporary stack space, does the copy/move into the final location, and then destroys the temporary. No initialization, no elision.

Oh sure, it would require something of an ABI change. But since it's part of the return value, you wouldn't even need to mess with name mangling. You'd just use the list of input types to determine how big of a buffer you need to pass in.

Aside from determining when to employ elision, at least, but then, if
the return type supports unpacking (which it obviously should), I don't
see why we need to get so worked up about it.

Because you're asking for general syntax, rather than a library feature or a contained bit of syntax. And once assigned, that syntax will be permanently embedded in C++. Forever. It can not be used for anything else in the language.

Because syntax is rare and precious, it is not cheap. And valuable syntax should not be created just because it makes the lives of generic programmers who work with tuples easier. Syntax should be something we can all, or most all, use.

Uniform initialization, lambda expressions, move semantics, these are all highly useful elements of basic C++ syntax that can be used by programmers regardless of discipline. Even terse template syntax has substantial value, particularly in light of many other changes coming down the pipe. Multiple return values would be a similarly broad concept, usable by many people.

But doing MRVs as some kind of hack, based on building tuples (or tuple-like objects) and the like? No.

Nicol Bolas

unread,
May 27, 2015, 6:09:33 PM5/27/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Wednesday, May 27, 2015 at 5:05:45 PM UTC-4, Matthew Woehlke wrote:
That said, this notion of a single "entity" that is really multiple
values... bugs me. It would be one thing if we had syntax that said
'hey, this here is multiple values that I would like passed as separate
parameters', but my understanding is that that *isn't* what Nicol is
suggesting. Given that we don't have any similar concept in the language
currently, I'm not particularly fond of introducing it.

We do have a similar concept: template parameter packs. It is one entity which represents multiple objects. You can unpack them in arbitrary expressions, which makes them much more useful than your limited `unpack` tuple suggestion.

I'm not saying that MRV functions should effectively return TPPs. But my point is that we already have similar constructs that are based on similar elements. So there's nothing strange about the idea of such a construct.

Thiago Macieira

unread,
May 27, 2015, 6:55:56 PM5/27/15
to std-pr...@isocpp.org
On Wednesday 27 May 2015 15:09:33 Nicol Bolas wrote:
> We do have a similar concept: template parameter packs. It is one entity
> which represents multiple objects. You can unpack them in arbitrary
> expressions, which makes them much more useful than your limited `unpack`
> tuple suggestion.

Actually, thinking about it and fold expressions allows for very easy
unpacking. The unpack-tuple-and-call function would be:

template <typename F, typename... Args> auto
unpack(F &&f, std::tuple<Args...> &&args)
{
using namespace std;
return f(std::forward<Args>(get<N>(args)), ...);
}

So all we need is for the MRV pack to be treated like a template parameter
pack and we can use all the expansion tricks.

std::cout << ... << args << endl;
size_t sum = 0 + ... + args; // assuming they cast somehow to integer

tuple<decltype(args)...> t = make_tuple(..., args);

If you have a template pack containing an MRV pack, you'll need the .......
operator...

Farid Mehrabi

unread,
May 28, 2015, 4:09:50 AM5/28/15
to std-proposals

​​
​​

2015-05-21 5:44 GMT+04:30 Matthew Fioravante <fmatth...@gmail.com>:
So this idea came out of the number parsing thread. Many languages like python have support for functions returning multiple values.

In C++, one option we have currently is this:
​​

std::tuple<int,float,double> foo();

int x;
double d;
std
::tie(x, std::ignore, d) = foo();

This works but it has a lot of problems:
- We have to declare the receiving variables beforehand. This means they have to be default constructed and then assigned and we cannot use auto.
- Its overly verbose and clumsy to use. First we have to declare the variables, then we need 8 character std::tie and 11 character std::ignore.
- tuple and tie are not as well known to beginners. A "standard practice" for capturing multiple values would be better as a core language feature than a library feature.

So one solution would be to invent a new syntax:
int x;
[x; void; auto d] = foo();


Which is the equivalent of:
​​
int x;
auto _t = foo();
​​
x
= std::get<0>(std::move(_t));
auto d = std::get<2>(std::move(_t));

Each element in this "assignment list" corresponds to a element of a tuple of the right hand side of the expression. Each element can be one of:
- An lvalue expression: the resulting lvalue is assigned with operator=() to the corresponding tuple element.
- void: the corresponding tuple element is ignored
- A local variable declaration. A local variable is created and constructed with the corresponding tuple element.

I've chosen to use [] instead of {} because so far braces always introduce a scope and in this example, d which is created inside the braces actually lives in the outer scope.
I've chosen to use ; instead of , to avoid confusion with the comma operator.

We can also allow the user to specify a shorter list, with default values of void for the trailing elements not mentioned:

[
​​
auto
x] = foo(); //same as [auto x, void, void] = foo()

What do you think about such a feature?

-- 
​What is wrong with using references?
​​​​
auto 
& bar
 
= foo();
int
 
​​
= std::get<0>(
bar
);
auto
​​
 
= std::get<2>(
​​
bar
);

​regards,
FM.​


--
how am I supposed to end the twisted road of  your hair in such a dark night??
unless the candle of your face does shed some light upon my way!!!

Miro Knejp

unread,
May 28, 2015, 6:13:43 AM5/28/15
to std-pr...@isocpp.org
On 28 May 2015, at 00:55 , Thiago Macieira <thi...@macieira.org> wrote:

On Wednesday 27 May 2015 15:09:33 Nicol Bolas wrote:
We do have a similar concept: template parameter packs. It is one entity
which represents multiple objects. You can unpack them in arbitrary
expressions, which makes them much more useful than your limited `unpack`
tuple suggestion.

Actually, thinking about it and fold expressions allows for very easy
unpacking. The unpack-tuple-and-call function would be:

template <typename F, typename... Args> auto
unpack(F &&f, std::tuple<Args...> &&args)
{
       using namespace std;
       return f(std::forward<Args>(get<N>(args)), ...);
}

This is already part of the Library Fundamentals TS in [tuple.apply]

Matthew Woehlke

unread,
May 28, 2015, 12:14:01 PM5/28/15
to std-pr...@isocpp.org
On 2015-05-27 18:05, Nicol Bolas wrote:
> On Wednesday, May 27, 2015 at 4:42:24 PM UTC-4, Matthew Woehlke wrote:
>> If we define "value sequence" as some tuple-like-but-not-tuple
>> "object" for which this sort of in-place construction is allowed,
>> then I don't see why this needs to be tightly coupled to
>> unpacking.
>
> This is the thing you don't seem to fully understand.
>
> I do not *want* a "value sequence" to be *any* kind of C++ object. It
> should not be a type with a value.

I think I do understand. It's just that I strongly disagree.

At some point, there *has* to be an object representing a value
sequence. Even if it's not a "C++ object", the compiler needs some sort
of internal notion that it has so many values and they are stored in
such-and-such manner.

If it has this, I fail to see why there is a problem giving that
construct a C++ name such that one can write get<N> against the object.

Whether or not one can "make copies" is an interesting question, though
again I object to declaring that you can't.

As stated elsewhere, I also object to the notion that there is no
(non-explicit) way to treat a value sequence as a single entity
(regardless of the underlying representation). C++ currently has no
concept at all like this. Python has no such concept. I am utterly
unconvinced that there is any genuine value in creating such a concept,
when the main objections you raised can be addressed by a new container
type.

> Remember: template parameter packs are not objects either. You
> can't copy them, move them, reference them, or use them in anything other
> than a parameter pack expression.

Template parameter packs *also* are not real C++ entities. They are
template meta-programming entities that are much more closely related to
template type names. Substitution occurs at compile time, at instantiation.

This is not the case with return values, multiple or otherwise. For one,
the types are not templated (or it they are, that is an orthogonal process).

I object to the idea of MRV's being unusable except to be unpacked
(which would also imply that unpacking is a concept that can only be
used for MRV's). I fail to see the justification for this approach
compared to a more traditional "MRV's are a real, single type" approach.

> Similarly, braced-init-lists are not
> elements of an expression; the construct `{4.3, "foo"}` is not a legal
> expression.

You're confusing the issue. That's *not* valid without context, no, but
we're not talking about a context-free case. And '{1, 2}' *is* valid; on
its own, it is a std::initializer_list<int>.

MRV's have context. Either the types are known a-priori, or else your
proposal requires that they be deduced. However I see no reason that
deduction can't deduce a std::value_sequence<Types...>.

> Value sequences should be like those.

They won't be. A brace expression such as the above is *always* a brace
expression. You can't get such a thing back from a function, or give it
a name. Again, I object to having functions return something that is
invisibly treated as multiple expressions.

>> Can you actually implement your MRV concept? Seems like it has most of
>> the same issues...
>
> That's a fair question, but I fail to see the implementation problems.
> Certainly, they're nothing like those for defining and implementing elision
> out of some type.

That is *exactly* a problem. In your proposal, you expect this:

std::vector<int> a = {1, 2, 3};
std::string b = "foo";
return [a; b];

...to deduce that the 'a' and 'b' objects should NOT be constructed on
the stack, but rather in some manner that allows RVO.

I fail to see why it should be any more difficult to do this if the MRV
is some new "object type".

> Right now, as I understand it, output values at the compiled code level are
> returned as either registers (for small objects), or the caller provides a
> piece of stack space to store the returned object (for larger objects).

So what's the problem giving std::value_sequence this behavior?

It seems to me that std::value_sequence can be treated as a struct with
members equal to the set of return values *that are not constructed by
the caller*. The caller provides this storage as it would for e.g. a
std::tuple. The callee constructs the "members" of this in place in the
usual manner of RVO. No ABI change is needed.

>> I don't see why we need to get so worked up about it.
>
> Because syntax is rare and precious, it is not cheap. And valuable syntax
> should not be created just because it makes the lives of generic
> programmers who work with tuples easier.

I *don't* work with tuples. The *reason* I don't work with tuples is
because they are a pain, exactly *because* we don't have syntax to make
them easier.

Nor is unpacking limited to tuples. Aside from the range-based for
example (which, btw, I consider as a "critical" use case for any form of
unpacking and/or MRV), unpacking has potential value for any
sequence-like structure. Think about arrays, points, ranges,
dimensions... all could potentially use unpacking.

Python obviously felt that *generalized* unpacking is sufficiently
valuable that it is not only a language feature, but one that uses
almost *no* syntax.

--
Matthew

Arthur O'Dwyer

unread,
May 28, 2015, 3:00:20 PM5/28/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Wednesday, May 27, 2015 at 8:23:16 AM UTC-7, Matthew Woehlke wrote:
On 2015-05-27 03:43, Nicol Bolas wrote:
> On Tuesday, May 26, 2015 at 10:53:05 PM UTC-4, Matthew Fioravante wrote:
>> On Tuesday, May 26, 2015 at 9:23:50 PM UTC-4, Nicol Bolas wrote:
>>> Nor does it force you to package parameters into a tuple.
>>
>> But it does, at least at the syntax level
>>
>> def foo():
>>     return (1, 2, 3);
>>
>> (x, y, z) = foo();

Actually it doesn't:

  >>> def foo():
  >>>  return 1, 2
  >>> a, b = foo()

...but the return is a 'tuple' nevertheless:

  >>> type(foo())
  <type 'tuple'>

(Basically, the ()'s are optional, and implied if absent.)

My knowledge of Lua is limited ("effectively nonexistent" is probably
more accurate). Python, however, only supports single return values. It
just has syntax sugar for returning tuples and unpacking tuples.

[...]

In Python, "return x, y" is syntactic sugar for "return (x, y)".

I'm glad someone finally pointed that out!  This thread has been relatively full of misconceptions about non-C++ languages. I even saw somebody claiming that Python doesn't have tuples. :P

FWIW, I'm strongly opposed to "filling in" any holes in the lambda syntax [x](...){...} with completely unrelated functionality [x]=(...).
(And yes, I was initially opposed to the crazy lambda syntax "filling in" holes in the array-indexing syntax. This is the line-noise-must-compile philosophy of Perl: "a[i] works, i[a] works, now let's make [a]{i;} do something too!")

However, let's go back to analyzing use-cases. I hear three coming up over and over:

(1) Initializing a set of named local variables from a tuple, without using std::tie, and ideally defining those variables with 'auto'.

    std::tuple<float,float,float> getcoords();
    int main() {
        auto x, y, z = getcoords();
    }

This syntax would work out of the box, if you defined "auto list-of-idents = expression" to be a single syntactic unit that meant "auto&& __e = expression, ident0 = std::get<0>(__e), ident1 = std::get<1>(__e), ...".  There would be some difficulty in specifying how std::get was looked up; but we have already solved that problem for ranged for-loop syntax (which relies on std::begin and std::end), so we'd solve it the same way.

Notice that "auto" here is magic; "int x, y, z = getcoords();" would remain a compile error (due to tuple<float,float,float>'s not being convertible to int in the initializer of z). The reason "auto" works is that "auto x, y, z = ..." with no initializers for x and y would ordinarily (e.g. in C++14) be a compile error.

(2) Passing the "multiple return values" of one function as the "multiple arguments" to another. This is solved already in C++17: http://en.cppreference.com/w/cpp/utility/functional/invoke, inherited from Boost Fusion http://www.boost.org/doc/libs/1_58_0/libs/fusion/doc/html/fusion/functional/invocation/functions/invoke.html.

(3) Creating a local "parameter pack" variable, suitable for expanding into subsequent expressions.

    std::tuple<float,float,float> getcoords();
    bool enemy_at(float x, float y, float z)
    int main() {
        auto... xyz = std::unpack(getcoords());  // hypothetical magical "std::unpack" for turning a tuple into a parameter-pack
        if (enemy_at(xyz...)) {
            auto sum_of_coords = 0 +...+ xyz;  // C++17 fold-expression
        }
    }

This requires more thought (from me, at least).  It seems to want some notion of functions "returning" parameter-packs, which doesn't exist at the moment.

Anyway, I see no reason to start introducing square brackets and stuff into what ideally would be a straightforward and newbie-friendly syntax feature.

–Arthur

Nicol Bolas

unread,
May 28, 2015, 3:11:27 PM5/28/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Thursday, May 28, 2015 at 12:14:01 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-27 18:05, Nicol Bolas wrote:
> On Wednesday, May 27, 2015 at 4:42:24 PM UTC-4, Matthew Woehlke wrote:
>> If we define "value sequence" as some tuple-like-but-not-tuple
>> "object" for which this sort of in-place construction is allowed,
>> then I don't see why this needs to be tightly coupled to
>> unpacking.
>
> This is the thing you don't seem to fully understand.
>
> I do not *want* a "value sequence" to be *any* kind of C++ object. It
> should not be a type with a value.

I think I do understand. It's just that I strongly disagree.

At some point, there *has* to be an object representing a value
sequence. Even if it's not a "C++ object", the compiler needs some sort
of internal notion that it has so many values and they are stored in
such-and-such manner.

Yes, if the compiler is generating an AST from parsing C++, then it must have some AST construct that represents a multiple return value return statement.

So what? If it is not a "C++ object", then it is not an "object" in any way that matters to the standard. Any such "object" is merely an implementation detail, not something the standard has to deal with. It does not work in the way that a "C++ object" works.

So there does not "have to be" any object associated with a value sequence.

If it has this, I fail to see why there is a problem giving that
construct a C++ name such that one can write get<N> against the object.

Because then it would have to be an object, which would have to work in accord with the rules for objects. I'm using the term "object" as used in the standard. Objects have types. Objects have values. Objects have storage. Objects have all kinds of things.

Template parameter packs are not objects. They do not have a type. They do not have a value. They do not have memory storage. They do not need any of these things.

Why does a value sequence need a type? If you want to assign them a type, if you want to make them an object, stick them in a `tuple`; I've already shown how.

But why does it by default need to be shoved into some object?
 
> Remember: template parameter packs are not objects either. You
> can't copy them, move them, reference them, or use them in anything other
> than a parameter pack expression.

Template parameter packs *also* are not real C++ entities. They are
template meta-programming entities that are much more closely related to
template type names. Substitution occurs at compile time, at instantiation.

Template metaprogramming is C++. Thus, "meta-programming entities" are by definition C++ entities.

What they are not is C++ objects.
 
This is not the case with return values, multiple or otherwise. For one,
the types are not templated (or it they are, that is an orthogonal process).

... what does it matter if it's not in a tempate? The types of the members of the sequence are all known.

> Similarly, braced-init-lists are not
> elements of an expression; the construct `{4.3, "foo"}` is not a legal
> expression.

You're confusing the issue. That's *not* valid without context, no, but
we're not talking about a context-free case. And '{1, 2}' *is* valid; on
its own, it is a std::initializer_list<int>.

No, it is not. The standard is very clear on this. It only becomes a std::initializer_list in a context in which that transformation is a possibility. Auto/template type deduction is such a context, as is initialization via constructor. In aggregate initialization, `{1, 2}` is not a std::initializer_list of any sort. It is instead a group of constructs used for aggregate initialization.

After all:

int a[4] = {1, 2, 3, 4};
int b[4];
b
= {1, 2, 3, 4};

`a` is legal; `b` is not. If `{1, 2, 3, 4}` genuinely was a value of `std::initializer_list<int>` type, then the standard would have no room to say that `a` is legal and `b` is not. Not without a bunch of special case rules that say, "if the object is not a initializer_list, then do this..."

This is precisely why braced-init-lists are not types or expressions.
 
> Value sequences should be like those.

They won't be. A brace expression such as the above is *always* a brace
expression.

There is no such thing as a "brace expression". The standard is very clear about this: it is not an expression.

Please use the proper terminology for standardized constructs, not the way you think they work.

>> Can you actually implement your MRV concept? Seems like it has most of
>> the same issues...
>
> That's a fair question, but I fail to see the implementation problems.
> Certainly, they're nothing like those for defining and implementing elision
> out of some type.

That is *exactly* a problem. In your proposal, you expect this:

  std::vector<int> a = {1, 2, 3};
  std::string b = "foo";
  return [a; b];

...to deduce that the 'a' and 'b' objects should NOT be constructed on
the stack, but rather in some manner that allows RVO.

I fail to see why it should be any more difficult to do this if the MRV
is some new "object type".

... OK, I see your point. Assuming that the type's construction is handled purely in compiler logic, that's reasonable.

> Right now, as I understand it, output values at the compiled code level are
> returned as either registers (for small objects), or the caller provides a
> piece of stack space to store the returned object (for larger objects).

So what's the problem giving std::value_sequence this behavior?

It seems to me that std::value_sequence can be treated as a struct with
members equal to the set of return values *that are not constructed by
the caller*. The caller provides this storage as it would for e.g. a
std::tuple. The callee constructs the "members" of this in place in the
usual manner of RVO. No ABI change is needed.

Well, the ABI now has to include the "std::value_sequence" type, which again is a "magic type", not a normal type. I admittedly know very little about ABIs, so I have no idea how any particular "magic type" of this sort would be handled at the ABI level. But it would depend on how much "magic" is in this magic type.

That being said, and given that I don't know much about ABIs, I don't really understand why MRVs would be problematic for ABIs. Or more to the point, how the solution of returning a "magic value" would differ from the MRV solution. In both cases, the compiler needs to know how much space to allocate, which will be based on some aggregation of the types involved. In both cases, the compiler and it's ABI will have to define that aggregation, whether it's in the "magic type" (and therefore, the ABI will have to define the ordering of the items in the type as well as their layout) or purely internally (with the ABI defining the offsets/alignment for each type in the returned data).

std::initializer_list poses the same ABI "change", in the sense that the ABI has to explain what std::initializer_list looks like internally. Otherwise, one compiler could not compile code that's ABI compatible with another, if someone tries to pass an `initializer_list` across ABI boundaries.

>> I don't see why we need to get so worked up about it.
>
> Because syntax is rare and precious, it is not cheap. And valuable syntax
> should not be created just because it makes the lives of generic
> programmers who work with tuples easier.

I *don't* work with tuples. The *reason* I don't work with tuples is
because they are a pain, exactly *because* we don't have syntax to make
them easier.

And that brings up yet another reason not to regularize tuple usage by giving them special syntax: tuples are not good.

The nice thing about a struct is that its members are named. This means that every member has some semantic information associated with it.

When you have something like map::insert that returns a pair, you lose any semantic information. What does the boolean value mean? A nice name like "did_insert" would be handy to have. But a `pair` can't do that; it'd have to have returned a proper struct.

Similarly, when using your preferred example of iterating through a `map`, you have to know that the key comes first. Sure, that's obvious to those who are used to the type, but if a function were just handed a std::pair, and had no idea where it came from, how do you know how to use it? By all rights, std::map should have used a struct type with real members, not a generic `pair`, since any function that takes it will have to know what the members mean.

Tuples are not a construct that should see frequent use. Not just because there's no easy syntax for working with them, but because frequent use of them destroys semantic information.

That's not to say that they aren't useful. But we shouldn't enshrine them just because dynamically typed languages like the construct.

Nor is unpacking limited to tuples. Aside from the range-based for
example (which, btw, I consider as a "critical" use case for any form of
unpacking and/or MRV),

We can't change the past. If C++ had MRVs from day 1, std::map's iterator would not return a `pair`. For `map`, "pair" would merely be an implementation detail; anytime we see a "pair" in a return value, that's nothing more than a syntactic necessity due to lacking MRVs.

We shouldn't make features the wrong way just to support an API that's was poorly designed for the new paradigm. We should support having a proper API that works with the new method. So `map` would simply have a `mrv_range` member functions that returns an MRV-style range.

It should also be noted that your magical type syntax doesn't support this, while MRV does:

auto x = Typename{FuncThatReturnsMultipleValues()};

This function's return values can be used to initialize a variable who's type is determined by the caller. A `value_sequence` return type could only be able to do so if `Typename` specifically allowed it.

I would consider this, and indeed any sort of chaining of this kind, to be a prime use case for MRVs. It would be just as common if not substantially moreso than iterating through a `std::map`.

I imagine that next, you're going to say that, since `value_sequence` is a magic type, we can just make this syntax magically work somehow. At which point, you're basically just taking a sledgehammer to the C++ specification, breaking it wherever you have to in order to make the syntax work with an object. You're saying that objects mostly work like X, except when magic objects are used which instead work like Y.

And that is one of the main reasons why braced-init-lists are not objects. Because to make their syntax work where it currently does, yet still requiring them to follow normal object rules, would require so many special cases that it'd be basically impossible to specify it in the language.

unpacking has potential value for any
sequence-like structure. Think about arrays, points, ranges,
dimensions... all could potentially use unpacking.

I cannot recall a single instance where I've had the desire to "unpack" members of an array, point, or arbitrary ranges into a function call or anything of the like. And even if I did, I would consider it to be an outlier case, for which specialized syntax would not be needed.

With MRVs, it could just be a `std::unpack` function, that could work on anything (either through reflection or through user-defined specialization).

Python obviously felt that *generalized* unpacking is sufficiently
valuable that it is not only a language feature, but one that uses
almost *no* syntax.

Lua obviously felt that value sequences were sufficiently valuable that they are not only a language feature, but one that uses almost *no* syntax.

I can play the "some other language does it" game too. That fact alone is not a justification for C++ to do it. You could try to claim that Python is better because it's more widely used. I could counter that by claiming that Lua is better because it's more efficient (value sequences in Lua are just values on its stack. Thus, they require zero memory allocation, unlike Python tuple packing/unpacking).

Nicol Bolas

unread,
May 28, 2015, 3:23:33 PM5/28/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net

So... you want to limit this automatic unpacking just to `auto`? That's a pretty terrible limitation. Not everyone is crazy about using `auto` everywhere. They shouldn't have to do 10x the syntactic work just to do that.

It's one thing to make that syntax an option; it's quite another to say that it's the only possible way to automatically unpack a tuple or use MRVs.
 
(2) Passing the "multiple return values" of one function as the "multiple arguments" to another. This is solved already in C++17: http://en.cppreference.com/w/cpp/utility/functional/invoke, inherited from Boost Fusion http://www.boost.org/doc/libs/1_58_0/libs/fusion/doc/html/fusion/functional/invocation/functions/invoke.html.

So.. if you have a `tuple`... how do you use `invoke` on it? `fusion::invoke` takes a FUsion sequence; `std::invoke` just takes parameters, not a `tuple` or anything else that's remotely sequence-like.

Equally importantly, it's not nearly as simple as real MRVs. Nor is it as composable:

some_func(func1(), func2());

If `some_func` takes three values, func1 could return 1 value with func2 returning 2. Or vice-versa. That is much more "straightforward and newbie-friendly syntax" than any form of `invoke`.

Not that even `fusion::invoke` could handle the above.

Matthew Woehlke

unread,
May 28, 2015, 4:34:44 PM5/28/15
to std-pr...@isocpp.org
On 2015-05-28 15:11, Nicol Bolas wrote:
> When you have something like map::insert that returns a pair, you lose any
> semantic information. What does the boolean value mean? A nice name like
> "did_insert" would be handy to have. But a `pair` can't do that; it'd have
> to have returned a proper struct.

(OT)

I still dream about one day having something that is transparently
equivalent to std::pair (meaning, we could change e.g. std::map without
SIC/BIC) but has meaningful names for the members :-).

--
Matthew

Matthew Woehlke

unread,
May 28, 2015, 5:22:42 PM5/28/15
to std-pr...@isocpp.org
(Sigh... I had a reply and TB ate it. Going to try to cover the high
points; sorry if this ends up being a bit terse.)

On 2015-05-28 15:11, Nicol Bolas wrote:
> On Thursday, May 28, 2015 at 12:14:01 PM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-27 18:05, Nicol Bolas wrote:
>>> Similarly, braced-init-lists are not
>>> elements of an expression; the construct `{4.3, "foo"}` is not a legal
>>> expression. Value sequences should be like those.
>>
>> They won't be. A brace expression such as the above is *always* a brace
>> expression.
>
> There is no such thing as a "brace expression". The standard is *very clear*
> about this: it is not an expression.

Please don't language lawyer :-).

What I mean is, the symbols '{a, b}' show me that there are two items.
In no way can I, at present, "pass" those two items in a way that I lose
the immediately obvious fact that there are two items.

What I object to is:

> auto x = Typename{FuncThatReturnsMultipleValues()};

...this. Here, I have to inspect FuncThatReturnsMultipleValues to know
that it returns multiple values. I object to that. I strongly would
prefer to require some syntax to indicate unpacking, e.g. '[*]' or '...'
or whatever.

>> That is *exactly* a problem. In your proposal, you expect this:
>>
>> std::vector<int> a = {1, 2, 3};
>> std::string b = "foo";
>> return [a; b];
>>
>> ...to deduce that the 'a' and 'b' objects should NOT be constructed on
>> the stack, but rather in some manner that allows RVO.
>>
>> I fail to see why it should be any more difficult to do this if the MRV
>> is some new "object type".
>
> ... OK, I see your point. Assuming that the type's construction is handled
> purely in compiler logic, that's reasonable.

Right. (Well, I would say that there "is" no construction, at least at
the call site. Construction would be of the "member items", in-place, in
the callee.)

> Well, the ABI now has to include the "std::value_sequence" type, which
> again is a "magic type", not a normal type. I admittedly know very little
> about ABIs, so I have no idea how any particular "magic type" of this sort
> would be handled at the ABI level. But it would depend on how much "magic"
> is in this magic type.
>
> That being said, and given that I don't know much about ABIs, I don't
> really understand why MRVs would be problematic for ABIs. Or more to the
> point, how the solution of returning a "magic value" would differ from the
> MRV solution.

I don't think there is much magic, and I think we are more or less
wanting the same thing. The only real exception that I am seeing is that
unpacking and MRV's (and expansion) are orthogonal, and that an MRV has
a type.

Note: the more I think about it, the more I'm inclined to say that it is
an unnameable type, like lambdas. That is, 'auto x = mrv_func()' is
legal, but doing anything besides unpacking 'x' is not. (Potentially
even passing it via a template type.)

> In both cases, the compiler needs to know how much space to
> allocate, which will be based on some aggregation of the types involved. In
> both cases, the compiler and it's ABI will have to define that aggregation,
> whether it's in the "magic type" (and therefore, the ABI will have to
> define the ordering of the items in the type as well as their layout) or
> purely internally (with the ABI defining the offsets/alignment for each
> type in the returned data).

Right, though I don't think there is a real difference here.
Particularly if the type is unnameable, the order isn't necessarily
consistent for a given type list, but would depend on the callee. (In
neither case, AFAIK, can it vary at run-time, or by who is the caller.)

> std::initializer_list poses the same ABI "change", in the sense that the
> ABI has to explain what std::initializer_list looks like internally.
> Otherwise, one compiler could not compile code that's ABI compatible with
> another, if someone tries to pass an `initializer_list` across ABI
> boundaries.

Isn't that already the case in general? (Compiler 1's ABI is not
compatible with Compiler 2? Even for different versions of the same
vendor's compiler?)

>>> Because syntax is rare and precious, it is not cheap. And valuable
>>> syntax should not be created just because it makes the lives of
>>> generic programmers who work with tuples easier.

Incidentally, I'm not sure this is a *valid* reason. Depending on the
unpacking syntax, knowing that you are unpacking without a lot of
look-ahead might be difficult. Compiler vendors tend to not like that;
as a result, it may be that your unpacking syntax would not be any more
context sensitive than generic unpacking.

> The nice thing about a struct is that its members are named. This means
> that every member has some semantic information associated with it.

The nice thing about generic unpacking is that I can unpack the MRV's
from a function that returned them as a struct with pretty names :-).

> We shouldn't make features the wrong way just to support an API that's was
> poorly designed for the new paradigm. We should support having a proper API
> that works with the new method. So `map` would simply have a `mrv_range`
> member functions that returns an MRV-style range.

Generic unpacking would make std::map, and any user classes with similar
API, Just Work.

> It should also be noted that your magical type syntax doesn't support this,
> while MRV does:
>
> auto x = Typename{FuncThatReturnsMultipleValues()};

Per above, I object to that :-).

My "magic type" *would* work like:

auto x = Typename{[*]FuncThatReturnsMultipleValues()};

...and so would this:

auto params = make_tuple(5, 12, 42.0);
auto x = Typename{[*]params};

> I would consider this, and indeed any sort of chaining of this kind, to be
> a prime use case for MRVs. It would be just as common if not substantially
> moreso than iterating through a `std::map`.

Agreed, but I also consider parameter expansion an orthogonal problem.
And one that works with value sequences *and more*, if get<N> can be
used on a value sequence.

Oh, and, I'm pretty sure I would at some point write code like:

auto qm = QMatrix4x4{[*]m.elements};

(Yes, that's right, there are some obnoxious matrix classes that take a
list of values, but not an array of values, for initialization. Generic
expansion lets me not have to write out that junk.)

>> unpacking has potential value for any
>> sequence-like structure. Think about arrays, points, ranges,
>> dimensions... all could potentially use unpacking.
>
> I cannot recall a single instance where I've had the desire to "unpack"
> members of an array, point, or arbitrary ranges into a function call or
> anything of the like.

I can. Here's the example (slightly simplified, and with names changed)
that I know of offhand:

inline void foo(point2d const& in, double result[2])
{
// Method 1 (shorter)
[result[0]; result[1]] = foo([*]in);

// Method 2 (more generic)
std::tie([*]result) = std::tie([*](foo[*]in));

// C++98 (not generic)
const point2d p = foo(in.X, in.Y);
result[0] = p.X;
result[1] = p.Y;
}

This is in real, production code, and yes I would write it like that if
I could. (Note: I'm not sure if assignment like that actually works, but
that's only an issue because my 'point2d' is not assignable from a tuple.)

Unpacking objects (usually geometry types) into individual local
variables is not unusual for me.

--
Matthew

Nicol Bolas

unread,
May 28, 2015, 6:51:11 PM5/28/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Thursday, May 28, 2015 at 5:22:42 PM UTC-4, Matthew Woehlke wrote:
(Sigh... I had a reply and TB ate it. Going to try to cover the high
points; sorry if this ends up being a bit terse.)

On 2015-05-28 15:11, Nicol Bolas wrote:
> On Thursday, May 28, 2015 at 12:14:01 PM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-27 18:05, Nicol Bolas wrote:
>>> Similarly, braced-init-lists are not
>>> elements of an expression; the construct `{4.3, "foo"}` is not a legal
>>> expression. Value sequences should be like those.
>>
>> They won't be. A brace expression such as the above is *always* a brace
>> expression.
>
> There is no such thing as a "brace expression". The standard is *very clear*
> about this: it is not an expression.

Please don't language lawyer :-).

You want to modify The Standard for Programming Language C++. That requires a degree of precision about what things are called.
 
What I mean is, the symbols '{a, b}' show me that there are two items.
In no way can I, at present, "pass" those two items in a way that I lose
the immediately obvious fact that there are two items.

What I object to is:

> auto x = Typename{FuncThatReturnsMultipleValues()};

...this. Here, I have to inspect FuncThatReturnsMultipleValues to know
that it returns multiple values. I object to that. I strongly would
prefer to require some syntax to indicate unpacking, e.g. '[*]' or '...'
or whatever.

On what basis do you object to that? You say that you have to inspect it to know that it returns multiple values. Well... why do you care? All you care about is what you need to; namely, that `Typename` has a constructor that matches the return value(s) of the function you're using. Whether it's one return value or 20, as long as there's a matching constructor, you're syntactically fine.

Why should using `FuncThatReturnsOneValue` require less syntax than `FuncThatReturnsMultipleValues`? Furthermore, what happens if someone changes the return type; why should it cause this (particular) code to break? The statement would be no less legal, so long as Typename has a valid constructor.

I just don't see what the point of the added syntax would be. I understand why you need template parameter pack unpacking syntax (because you're able to distribute unpacking across an expression). But what do you gain from having to unpack the results, rather than having the results not be packed to begin with?


> Well, the ABI now has to include the "std::value_sequence" type, which
> again is a "magic type", not a normal type. I admittedly know very little
> about ABIs, so I have no idea how any particular "magic type" of this sort
> would be handled at the ABI level. But it would depend on how much "magic"
> is in this magic type.
>
> That being said, and given that I don't know much about ABIs, I don't
> really understand why MRVs would be problematic for ABIs. Or more to the
> point, how the solution of returning a "magic value" would differ from the
> MRV solution.

I don't think there is much magic, and I think we are more or less
wanting the same thing. The only real exception that I am seeing is that
unpacking and MRV's (and expansion) are orthogonal, and that an MRV has
a type.

Note: the more I think about it, the more I'm inclined to say that it is
an unnameable type, like lambdas. That is, 'auto x = mrv_func()' is
legal, but doing anything besides unpacking 'x' is not. (Potentially
even passing it via a template type.)

Then what's the point of being able to store it? Why make it an object at all if the only thing it can do is be unpacked?
 

> In both cases, the compiler needs to know how much space to
> allocate, which will be based on some aggregation of the types involved. In
> both cases, the compiler and it's ABI will have to define that aggregation,
> whether it's in the "magic type" (and therefore, the ABI will have to
> define the ordering of the items in the type as well as their layout) or
> purely internally (with the ABI defining the offsets/alignment for each
> type in the returned data).

Right, though I don't think there is a real difference here.
Particularly if the type is unnameable, the order isn't necessarily
consistent for a given type list, but would depend on the callee. (In
neither case, AFAIK, can it vary at run-time, or by who is the caller.)

I don't know much about ABIs, but I'm pretty sure that "consistency" is a very important element of them. Otherwise, you can't have different code that links to each other.

If the caller doesn't know that the first item in the typelist is actually the first in the data structure, how can the caller access it to unpack it? The whole point of an ABI is for both the caller and callee to know what's going on in an interface.
 
>>> Because syntax is rare and precious, it is not cheap. And valuable
>>> syntax should not be created just because it makes the lives of
>>> generic programmers who work with tuples easier.

Incidentally, I'm not sure this is a *valid* reason. Depending on the
unpacking syntax, knowing that you are unpacking without a lot of
look-ahead might be difficult. Compiler vendors tend to not like that;
as a result, it may be that your unpacking syntax would not be any more
context sensitive than generic unpacking.

Um, I'm not talking about "unpacking syntax." Or rather, more to the point, I don't want "unpacking syntax". Or packing syntax.

I want MRV syntax. Packing and unpacking would not be syntax; they'd just be function calls.

Agreed, but I also consider parameter expansion an orthogonal problem.

That's because you're focused on syntax for packing and unpacking tuples. If you simply have MRVs as a first-class feature rather than an unpacking hack, then packing and unpacking is nothing more than a function call.
 
>> unpacking has potential value for any
>> sequence-like structure. Think about arrays, points, ranges,
>> dimensions... all could potentially use unpacking.
>
> I cannot recall a single instance where I've had the desire to "unpack"
> members of an array, point, or arbitrary ranges into a function call or
> anything of the like.

I can. Here's the example (slightly simplified, and with names changed)
that I know of offhand:

  inline void foo(point2d const& in, double result[2])
  {
    // Method 1 (shorter)
    [result[0]; result[1]] = foo([*]in);

    // Method 2 (more generic)
    std::tie([*]result) = std::tie([*](foo[*]in));

    // C++98 (not generic)
    const point2d p = foo(in.X, in.Y);
    result[0] = p.X;
    result[1] = p.Y;
  }

This is in real, production code, and yes I would write it like that if
I could. (Note: I'm not sure if assignment like that actually works, but
that's only an issue because my 'point2d' is not assignable from a tuple.)

Unpacking objects (usually geometry types) into individual local
variables is not unusual for me.

So your use case for this syntax is translating values of structs between different APIs? Isn't that something that should happen at the leaf parts of your code, with the internals of your code actually handling things in a consistent way?

In your example above, a user who wants the result as a `double[2]` should call `foo` like this:

double result[2];
to_double2
(foo(data), result);

That way, you don't have to write 20 different variations of `foo`.

Matthew Fioravante

unread,
May 28, 2015, 10:38:09 PM5/28/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
Even if MRV is implemented by returning a kind of non-type sequence / parameter pack, I think there still is value in a generalized unpacking syntax. We will need an unpacking syntax for MRV itself.

//Unpack pair
std
::pair<int,int> p;
[const auto& p0, const auto& p1] = p;

//Unpack tuple
std
::tuple<int, int, int> t;
int t2;
[const auto& t0; void; t2] = t;

//Unpack C array
int a[2];
[auto a0, auto a1] = a;

//Unpack std::array
std
::array<int,4> ax;
[auto ax0; auto ax1, void, void] = ax;

//Unpack string literal
std
::string_literal<3> s = "bar";
[auto sb; auto sa; auto sr] = s;

The syntax can support braced initializers
//Braced initialization can be unpacked
[auto i; auto l; auto d] = { 1, 1L, 1.0 };

This syntax would also support unpacking sequences from MRV.
The ... syntax is more verbose and maybe redundant, but it clearly tells us at usage whether we are dealing with an MRV sequence or an expression.
[int,float,double] foo();
auto... x = foo();
//More verbose, obvious where parameter packs are used
[auto x, auto y, auto z] = { x... }; //implemented using braced initializer support
[auto x1, void, void] = { foo()... };

It also gives us flexibility in how to chain templates and function calls.
std::max_element({ mrvs... });
std
::forward<decltype(mrvs)>(mrvs)...;

This version is more terse.
[int,float,double] foo();
auto... x = foo();

//More terse, possible confusion is x a type or
[auto x, auto y, auto z] = x;
[auto x1, void, void] = foo();

It would seem odd to me that to expand a variadic template parameter pack I need ... but for an MRV pack I don't. They look too similar that it could be confusing if the rules are very different.
[int,float,double] foo();
void bar(int, float, double);
template <typename... Args> void run(Args&&... tpp) {
 
auto... mrvs = foo();
  bar
(mrvs);
  bar
(tpp...);
}

Classes can define custom operator[auto&&...] to return MRV packs which can then use the unpack syntax
//Operator returns an MRV sequence
class udt0 {
public:
 
constexpr operator[auto&&...]() = default;
  //constexpr operator[auto...]() { return {i, f, d}; }
  //constexpr operator[auto...]() { return {f, d, i}; }
 
//constexpr operator[int,float,double]() { return {i,f,d}; };
 
//constexpr operator[double,float,int]() { return {d, f, i}; };
 
//constexpr operator[double,auto]() { return {d - i, f + i}; };
 
private:
 
int i; float f; double d;
} udt;
[auto di; auto fi] = udt;

POD struct types (maybe can relax constraints) have compiler generated default operator[auto&&...]
//POD types have compiler generated operator[auto...]
struct kv { string key; vector<int> value; } kv;


Matthew Woehlke

unread,
May 29, 2015, 11:16:56 AM5/29/15
to std-pr...@isocpp.org
On 2015-05-28 18:51, Nicol Bolas wrote:
> On Thursday, May 28, 2015 at 5:22:42 PM UTC-4, Matthew Woehlke wrote:
>> What I object to is:
>>
>>> auto x = Typename{FuncThatReturnsMultipleValues()};
>>
>> ...this. Here, I have to inspect FuncThatReturnsMultipleValues to know
>> that it returns multiple values. I object to that. I strongly would
>> prefer to require some syntax to indicate unpacking, e.g. '[*]' or '...'
>> or whatever.
>
> Why should using `FuncThatReturnsOneValue` require less syntax than
> `FuncThatReturnsMultipleValues`?

Because in no existing case can a single 'token' represent multiple
parameters without that being made obvious.

To ask the opposite question, should we allow this?

template <Typename Args...>
void foo(Args... args)
{
bar(args);
}

Also because separating expansion from unpacking from MRV's gives
greater consistency and greater general usefulness, rather than limiting
those concepts to a single tightly-coupled use case.

> Then what's the point of being able to store it? Why make it an object at
> all if the only thing it can do is be unpacked?

auto r = foo();
auto x = get<0>(params);
auto params = get<1,-1>(params)
for (auto i : x)
i->bar([*]params);

Storing allows it to be used more than once, allows accepting the values
and using the values to be separated, ...

One could use a tuple, but this whole discussion started because you
objected (loudly) to the extraneous copies that would incur. In the
above example, I might even hope that the compiler makes no copies
(excluding when calling 'bar', and possibly with some '&'s added). I
would *especially* expect no copies if I wasn't slicing the value
sequence, but merely reusing it.

>> Right, though I don't think there is a real difference here.
>> Particularly if the type is unnameable, the order isn't necessarily
>> consistent for a given type list, but would depend on the callee. (In
>> neither case, AFAIK, can it vary at run-time, or by who is the caller.)
>
> I don't know much about ABIs, but I'm pretty sure that "consistency" is a
> *very* important element of them. Otherwise, you can't have different code
> that links to each other.

Hmm... I was thinking that, depending on the internal implementation of
a function, the compiler might make a different choice how to pass
parameters (e.g. registers or not). But on further consideration, you're
right; I don't see how the compiler would then know how to call that
function. So, yeah, it would have to be consistent for a given type list.

> Um, I'm not talking about "unpacking syntax." Or rather, more to the point,
> I don't *want* "unpacking syntax". Or packing syntax.
>
> I want MRV syntax. Packing and unpacking would not be syntax; they'd just
> be function calls.

What, then, is your equivalent to this?

[auto x; auto y] = some_mrv_function();

(Note that you cannot use std::tie here, as 'x' and 'y' are not declared
yet.)

It seems you are entirely ignoring this case and asserting that "real
MRV's" make it go away. I disagree with that assertion. (And will point
out, again, that I still think Matthew F's proposal and yours are, or
should be, orthogonal. They are complementary, and yours needs to be
implemented in a way to be compatible with unpacking, but otherwise
orthogonal.)

>> Agreed, but I also consider parameter expansion an orthogonal problem.
>
> That's because you're focused on syntax for packing and unpacking tuples.

Right. Because I think it's a problem worth solving (see also previous
comment), and because it was the original proposal.

I think you may be confusing unpacking and expansion. Unpacking, as I am
using it, is the ability to write assignments like the above example.
Expansion is the ability to tell the compiler to... well, do this:

{[*]tuple_like};
foo([*]tuple_like);

// ...becomes
{get<0>(tuple_like), get<1>(tuple_like), ...};
foo(get<0>(tuple_like), get<1>(tuple_like), ...);

They have some conceptual similarities, but are very different operations.

> So your use case for this syntax is translating values of structs between
> different APIs?

That's *a* use case.

Here's another:

[auto x; auto y] = [*]p;
x = sin(2*x) + cos(2*y);
y = sqrt(y);
return {x, y};

Invented and somewhat simplified, but this is another case I run into
with moderate frequency; given a "tuple-like" class (often one that does
not have public data members), I want to "crack" it into local variables
for various reasons; terseness, to avoid multiple invocations of
accessor methods, etc.

There is also, of course, the associative container iteration example.
I'm confident there are others I'm not thinking of offhand.

> In your example above, a user who wants the result as a `double[2]` should
> call `foo` like this:
>
> double result[2];
> to_double2(foo(data), result);
>
> That way, you don't have to write 20 different variations of `foo`.

I find it more convenient to write utility overloads to accept data in a
variety of formats, and I'm not the only API to do so. The difference is
I can write those overloads *once* and use them many times, compared to
having to write call-site conversions many times.

--
Matthew

Matthew Woehlke

unread,
May 29, 2015, 11:23:41 AM5/29/15
to std-pr...@isocpp.org
On 2015-05-28 22:38, Matthew Fioravante wrote:
> //More verbose, obvious where parameter packs are used
> [auto x, auto y, auto z] = { x... }; //implemented using braced initializer
> support

I've been writing std::tie here (make_tuple would be equally
sufficient), but... yeah, I like being able to unpack a literal '{...}'
better :-). (More efficient, for sure...)

What's a little weird, though, is that this can't work under the 'as-if'
rule, since as Nicol pointed out, a general braced list can't be
assigned to a variable. I think that's a useful special case, but it
*is* a special case.

--
Matthew

Matthew Fioravante

unread,
May 29, 2015, 11:44:48 AM5/29/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
Implicit expansion might be a way to make braced initializers work with perfect forwarding. This might require the terse syntax.
 
struct Foo { Foo(int, double); };
struct Bar { Bar(const Foo&, const Foo&); }
auto... x = { 1, 1.0 };
auto f = Foo{x};
auto b = Bar{{x}, {x}}

//Expands to 2 mrv packs which are then forwarded to BAR(const Foo&, const Foo&)
std
::make_unique<Bar>({1, 1.0}, {2, 2.0});



Nicol Bolas

unread,
May 29, 2015, 7:57:00 PM5/29/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Friday, May 29, 2015 at 11:16:56 AM UTC-4, Matthew Woehlke wrote:
On 2015-05-28 18:51, Nicol Bolas wrote:
> On Thursday, May 28, 2015 at 5:22:42 PM UTC-4, Matthew Woehlke wrote:
>> What I object to is:
>>
>>> auto x = Typename{FuncThatReturnsMultipleValues()};
>>
>> ...this. Here, I have to inspect FuncThatReturnsMultipleValues to know
>> that it returns multiple values. I object to that. I strongly would
>> prefer to require some syntax to indicate unpacking, e.g. '[*]' or '...'
>> or whatever.
>
> Why should using `FuncThatReturnsOneValue` require less syntax than
> `FuncThatReturnsMultipleValues`?

Because in no existing case can a single 'token' represent multiple
parameters without that being made obvious.

... what?

`FuncThatReturnsMultipleValues()` is not a "token"; it's three tokens. It's an identifier, followed by an open paren, followed by a close paren.

You can consider this pedantic, but it's very hard to have a discussion about the standard if you use misuse standard terminology this badly.

If by "token", you meant "expression", then you're correct. The result of an expression is, without multiple return values, a single value with a single type. Therefore, in order for MRVs to work, that will have to change.

Obviously.

The standard would have to be changed to allow the evaluation of a function call expression to be a value sequence, as defined by the function's return value sequence syntax. Value sequences would only be able to be used in certain locations, such as in the initialization of a variable(s) or in the argument list of an enclosing function call expression.

There is no need for special expansion syntax at the site of the use of a value sequence for this to work.
 
To ask the opposite question, should we allow this?

  template <Typename Args...>
  void foo(Args... args)
  {
    bar(args);
  }


I understand why they didn't. It's for two reasons:

1) You already need an explicit unpacking operation on packs, so that you can do cool expression expansion tricks.

2) Because of #1, it's really important that the user understands what they're asking for (the difference between `func(args...)` and `func(args)...`). Otherwise, they might expect `args` to be able to be used in places or ways it can't be.

Since value sequences are not parameter packs, these don't have to apply.

One could use a tuple, but this whole discussion started because you
objected (loudly) to the extraneous copies that would incur.

Then you've misunderstood the point.

The primary justification for this feature was having multiple return values. It's the title of the thread. My point was that, by doing it as proposed (through std::tuple), it interferes with elision. Which it does.

Remember: the elision problem was that the callee had some variable, which was being packed into a tuple, then unpacked into the caller's variable. The elision in question was the elision between the callee's variable and the caller's variable.

Well, if the caller is storing the tuple as a tuple, rather than broadcasting the tuple elements into variables, this argument no longer applies.
 
In the
above example, I might even hope that the compiler makes no copies
(excluding when calling 'bar', and possibly with some '&'s added). I
would *especially* expect no copies if I wasn't slicing the value
sequence, but merely reusing it.

As previously stated, the standard requires this statement:


auto x = get<0>(params);

to call a constructor. Elision works only in cases where elision is expressly permitted. And this is in no way one of them.

Not only that, elision in this case breaks all kinds of C++ guarantees. In your case, if the copy/move was elided, `x` and `params` would be sharing data. Changing `x` would implicitly change `params`. I'm no authority on this matter, but I'm pretty sure that violates strict aliasing or some other C++ guarantee.
 
> Um, I'm not talking about "unpacking syntax." Or rather, more to the point,
> I don't *want* "unpacking syntax". Or packing syntax.
>
> I want MRV syntax. Packing and unpacking would not be syntax; they'd just
> be function calls.

What, then, is your equivalent to this?

  [auto x; auto y] = some_mrv_function();

Under MRV rules, that is not "unpacking" or "expansion", because `some_mrv_function` does not return a "packed" value that needs to be "unpacked" or "expanded". It returns multiple values.

Under MRV, this is merely the syntax for storing multiple values.

> So your use case for this syntax is translating values of structs between
> different APIs?

That's *a* use case.

Here's another:

  [auto x; auto y] = [*]p;
  x = sin(2*x) + cos(2*y);
  y = sqrt(y);
  return {x, y};

Invented and somewhat simplified, but this is another case I run into
with moderate frequency ; given a "tuple-like" class (often one that does
not have public data members), I want to "crack" it into local variables
for various reasons; terseness, to avoid multiple invocations of
accessor methods, etc.

There's a word for that use case:

Perfidy.

If you violate a class's interface, you are committing fraud. You had an agreement with the class, that you would follow its public interface. And you are knowingly and deliberately breaking that agreement.

If you don't like a class's interface; that's your right. You can choose not to use it for that reason; that's also your right. It's not your right to back-door the class, break in, and do whatever you want with what you find.

I know that C++ is not supposed to prevent perfidy. But we should not make perfidy literally three keystrokes away. What the hell good is public/private if anyone can bypass it all trivially?

"We should have this because it makes it trivial for me to break encapsulation" is not a good argument for any feature. That's not to say that being able to use a feature to break the language makes the feature a priori wrong. But such uses should be necessary evils, not a prime motivator for having it.

There is also, of course, the associative container iteration example.
I'm confident there are others I'm not thinking of offhand.

> In your example above, a user who wants the result as a `double[2]` should
> call `foo` like this:
>
> double result[2];
> to_double2(foo(data), result);
>
> That way, you don't have to write 20 different variations of `foo`.

I find it more convenient to write utility overloads to accept data in a
variety of formats, and I'm not the only API to do so. The difference is
I can write those overloads *once* and use them many times, compared to
having to write call-site conversions many times.

That's your prerogative. But you should accept the burden of wanting to do so: having to write all of those overloads.

The fact that one possible method of implementing something requires more effort than you'd like to expend is not a sufficient reason for requesting a syntactic change.

Matthew Fioravante

unread,
May 29, 2015, 10:59:07 PM5/29/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
But it seems to me that you might actually want to have this flexibility even with MRV's. I might want to call a function, passing all of the return values to it, or I might want to call a function separately for each return value.

[int,int] f();
[int,int] g(int, int);
int h(int);
void i(int, int, int, int);

//Compose f and g and g again
g
(g(f()...)...);

//Call h on each of f()'s return values and then forward the resulting sequence to g().
g
(h(f())...);

//f() produces 2 values, call g() on each of them separately to produce 4 values which is then forwarded to i().
i
(g(f())...);




One could use a tuple, but this whole discussion started because you
objected (loudly) to the extraneous copies that would incur.

Then you've misunderstood the point.

The primary justification for this feature was having multiple return values. It's the title of the thread. My point was that, by doing it as proposed (through std::tuple), it interferes with elision. Which it does.

Remember: the elision problem was that the callee had some variable, which was being packed into a tuple, then unpacked into the caller's variable. The elision in question was the elision between the callee's variable and the caller's variable.

Well, if the caller is storing the tuple as a tuple, rather than broadcasting the tuple elements into variables, this argument no longer applies.
 
In the
above example, I might even hope that the compiler makes no copies
(excluding when calling 'bar', and possibly with some '&'s added). I
would *especially* expect no copies if I wasn't slicing the value
sequence, but merely reusing it.

As previously stated, the standard requires this statement:

auto x = get<0>(params);

to call a constructor. Elision works only in cases where elision is expressly permitted. And this is in no way one of them.

Not only that, elision in this case breaks all kinds of C++ guarantees. In your case, if the copy/move was elided, `x` and `params` would be sharing data. 

If you wrote something like this:

[int,float] f();
[auto x; auto y] = f();

The tuple like thing returned by f() is a hidden object never directly accessible by the programmer. The optimizer therefore should be able to elide the copies and make x and y effectively aliases to the data members of the unnamed tuple return object thing on the stack.

If you do this however, all bets are off unless you explicitly use references
auto xy = f();
[auto x; auto y]=xy;


Obviously compiler generated unpacking would only be allowed for standard layout types with all public data members. If you want to support unpacking for a class with private data members you'll have to write the operator yourself.

Matthew Woehlke

unread,
May 30, 2015, 12:26:23 PM5/30/15
to std-pr...@isocpp.org
On 2015-05-29 22:59, Matthew Fioravante wrote:
> On Friday, May 29, 2015 at 7:57:00 PM UTC-4, Nicol Bolas wrote:
>> As previously stated, the standard *requires* this statement:
>>
>> auto x = get<0>(params);
>>
>> to call a constructor. Elision works *only* in cases where elision is
>> expressly permitted. And this is in no way one of them.
>>
>> Not only that, elision in this case breaks all kinds of C++ guarantees. In
>> your case, if the copy/move was elided, `x` and `params` would be sharing
>> data.
>
> If you wrote something like this:
>
> [int,float] f();
> [auto x; auto y] = f();
>
> The tuple like thing returned by f() is a hidden object never directly
> accessible by the programmer. The optimizer therefore should be able to
> elide the copies and make x and y effectively aliases to the data members
> of the unnamed tuple return object thing on the stack.
>
> If you do this however, all bets are off unless you explicitly use
> references
> auto xy = f();
> [auto x; auto y]=xy;

Right:

// Full elision
[auto x; auto y] = foo();

// Elision for 'x', copy for 'y'
Bar y;
[auto x; y] = foo();

// Elision for 'xy', copy for 'x' and 'y'
auto xy = foo();
[auto x; auto y] = xy;

>> On Friday, May 29, 2015 at 11:16:56 AM UTC-4, Matthew Woehlke wrote:
>>> Here's another [use case]:
>>>
>>> [auto x; auto y] = [*]p;
>>> x = sin(2*x) + cos(2*y);
>>> y = sqrt(y);
>>> return {x, y};
>>
>> If you violate a class's interface, you are *committing fraud*. You had
>> an agreement with the class, that you would follow its public interface.
>> And you are knowingly and deliberately breaking that agreement.
>
> Obviously compiler generated unpacking would only be allowed for standard
> layout types with all public data members. If you want to support unpacking
> for a class with private data members you'll have to write the operator
> yourself.

Er... right. Nicol, you're adding scary things to my proposal that *are
not part of it*. Unpacking is not magic. For the above to work requires
that get<N> is implemented (somewhere) for the type (or the type has a
conversion-to-tuple-like operator as has been mentioned a few times).
The mechanism for extracting "values" therefore either *is¹* part of the
type's public API, or must be written against the type's public API.

(¹ Unless you implement get<N> with nasty protection-bypassing code, but
then, that's not unpacking's fault that you did that.)

In particular, this additional code was implied:

int get<0>(QPoint const& p) { return p.x(); }
int get<1>(QPoint const& p) { return p.y(); }

--
Matthew

Matthew Woehlke

unread,
May 30, 2015, 12:35:06 PM5/30/15
to std-pr...@isocpp.org
On 2015-05-29 19:57, Nicol Bolas wrote:
> On Friday, May 29, 2015 at 11:16:56 AM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-28 18:51, Nicol Bolas wrote:
>>> Why should using `FuncThatReturnsOneValue` require less syntax than
>>> `FuncThatReturnsMultipleValues`?
>>
>> Because in no existing case can a single 'token' represent multiple
>> parameters without that being made obvious.
>
> ... what?
>
> `FuncThatReturnsMultipleValues()` is not a "token"; it's *three* tokens.
> It's an identifier, followed by an open paren, followed by a close paren.

(OT: Do you have *any idea* how annoying it is when you do this? I'm
trying to talk in abstract concepts, okay? It's frustratingly difficult
to do that when a) all the terminology that naturally lends itself to
doing so has already been "coopted" to have some specific meaning in
standardese, which b) you must insist on applying.)

s/token/conceptual unit in the mind of the developer/

...is that better?

> If by "token", you meant "expression", then you're correct.

...and last time I used "expression" you complained at me for that too.

/me sighs

> The result of an expression is, without multiple return values, a
> single value with a single type. Therefore, in order for MRVs to
> work, that will have to change.

Yes, it would have to change. That is a big part of my objection.

> There is no need for special expansion syntax at the site of the use of a
> value sequence for this to work.

From a technical perspective, no. It's the cognative perspective that
bothers me.

>> One could use a tuple, but this whole discussion started because you
>> objected (loudly) to the extraneous copies that would incur.
>
> Then you've misunderstood the point.
>
> The primary justification for this feature was having multiple return
> values. It's *the title of the thread*.

Yes, but the *feature* was *multiple assignments*. (That's *also* in the
title of the thread. First, in fact.) AFAICT you are trying to kill, or
needlessly cripple, that.

> My point was that, by doing it as proposed (through std::tuple), it
> interferes with elision. Which it does.

We agree that std::tuple is not optimal for MRV's. Please stop beating
on the point.

The proposal is to do unpacking for *anything that can be unpacked*, for
whatever that means (e.g. get<N>, conversion operator, something
else...). That would include std::tuple, std::pair, std::array, any user
types that provide support for it, and whatever we use as the 'optimal
mechanism for MRV's'.

>> What, then, is your equivalent to this?
>>
>> [auto x; auto y] = some_mrv_function();
>
> Under MRV rules, that is not "unpacking" or "expansion", because
> `some_mrv_function` does not return a "packed" value that needs to be
> "unpacked" or "expanded". It returns *multiple values*.
>
> Under MRV, this is merely the syntax for storing multiple values.

So, you are saying you *still* want unpacking syntax¹, you just only
want it to work for MRV's? Why? What is the benefit to limiting it in
such manner? (As previously explained, I don't buy the "syntax is
precious" argument.)

(¹ Syntax which performs multiple declarations/assignments from some
conceptual construction of multiple values, e.g. MRV functions.)

--
Matthew

Nicol Bolas

unread,
Jun 1, 2015, 12:01:53 PM6/1/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net


On Saturday, May 30, 2015 at 12:26:23 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-29 22:59, Matthew Fioravante wrote:
>
>> On Friday, May 29, 2015 at 11:16:56 AM UTC-4, Matthew Woehlke wrote:
>>> Here's another [use case]:
>>>
>>>   [auto x; auto y] = [*]p;
>>>   x = sin(2*x) + cos(2*y);
>>>   y = sqrt(y);
>>>   return {x, y};
>>
>> If you violate a class's interface, you are *committing fraud*. You had
>> an agreement with the class, that you would follow its public interface.
>> And you are knowingly and deliberately breaking that agreement.
>
> Obviously compiler generated unpacking would only be allowed for standard
> layout types with all public data members. If you want to support unpacking
> for a class with private data members you'll have to write the operator
> yourself.

Er... right. Nicol, you're adding scary things to my proposal that *are
not part of it*.

Matthew's the one wanting to use unpacking to break encapsulationHe was the one I was responding to. He specifically stated that in his post: "I want to "crack" it into local variables
for various reasons; terseness, to avoid multiple invocations of
accessor methods
, etc" (emphasis added). You can't avoid accessor invocations without breaking encapsulation.

Nicol Bolas

unread,
Jun 1, 2015, 12:15:37 PM6/1/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Saturday, May 30, 2015 at 12:35:06 PM UTC-4, Matthew Woehlke wrote:
On 2015-05-29 19:57, Nicol Bolas wrote:
> On Friday, May 29, 2015 at 11:16:56 AM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-28 18:51, Nicol Bolas wrote:
>>> Why should using `FuncThatReturnsOneValue` require less syntax than
>>> `FuncThatReturnsMultipleValues`?
>>
>> Because in no existing case can a single 'token' represent multiple
>> parameters without that being made obvious.
>
> ... what?
>
> `FuncThatReturnsMultipleValues()` is not a "token"; it's *three* tokens.
> It's an identifier, followed by an open paren, followed by a close paren.

(OT: Do you have *any idea* how annoying it is when you do this? I'm
trying to talk in abstract concepts, okay? It's frustratingly difficult
to do that when a) all the terminology that naturally lends itself to
doing so has already been "coopted" to have some specific meaning in
standardese, which b) you must insist on applying.)

Do you know how annoying it is when people are having a detailed discussion of the standard, yet use standardese incorrectly? I can't know what you mean by "token" means if you're using some colloquial definition of the term.

> If by "token", you meant "expression", then you're correct.

...and last time I used "expression" you complained at me for that too.

Because it wasn't an expression! I never said "never use the word 'expression'". I said "braced-init-lists are not an expression."


> The result of an expression is, without multiple return values, a
> single value with a single type. Therefore, in order for MRVs to
> work, that will have to change.

Yes, it would have to change. That is a big part of my objection.

... why?

>> One could use a tuple, but this whole discussion started because you
>> objected (loudly) to the extraneous copies that would incur.
>
> Then you've misunderstood the point.
>
> The primary justification for this feature was having multiple return
> values. It's *the title of the thread*.

Yes, but the *feature* was *multiple assignments*. (That's *also* in the
title of the thread. First, in fact.) AFAICT you are trying to kill, or
needlessly cripple, that.

No, I'm trying to kill the "via tuple" part. I want "Multiple assignments from multiple return values, as a first-class language feature."

>> What, then, is your equivalent to this?
>>
>>   [auto x; auto y] = some_mrv_function();
>
> Under MRV rules, that is not "unpacking" or "expansion", because
> `some_mrv_function` does not return a "packed" value that needs to be
> "unpacked" or "expanded". It returns *multiple values*.
>
> Under MRV, this is merely the syntax for storing multiple values.

So, you are saying you *still* want unpacking syntax¹, you just only
want it to work for MRV's? Why? What is the benefit to limiting it in
such manner? (As previously explained, I don't buy the "syntax is
precious" argument.)

You can choose not to buy it, but that doesn't make it false.

If the syntax gets thrown away on "unpacking packed objects" (as opposed to "storing multiple values"), then there will be no syntax left to have actual MRVs as a first-class language feature. So they're not "orthogonal" in any way; the syntactic presence of one precludes the possibility, viability, and utility of the other.

If there can be only one, I want real MRVs, not "unpacking packed objects". If we need the latter, we can create a function that returns multiple values, representing the members of the object. Unpacking packed objects is not a useful enough to promote to the level of syntax. Just like `tuple` is not important enough to make it a language feature rather than just a useful class.

Matthew Woehlke

unread,
Jun 1, 2015, 12:26:05 PM6/1/15
to std-pr...@isocpp.org
On 2015-06-01 12:01, Nicol Bolas wrote:
> Matthew's the one wanting to use unpacking to break encapsulation. He was the
> one I was responding to. He specifically stated that in his post:
>
>> I want to "crack" it into local variables for various reasons; terseness,
>> *to avoid multiple invocations ofaccessor methods*, etc.
>
> (Emphasis added.) You can't avoid accessor invocations without breaking encapsulation.

No, you're still misinterpreting. Also, by presuming to tell me what *I*
want, you're deliberately putting words in my mouth. In, I would add,
direct contradiction to my previous post. Stop that.

The key word there *is not* "invocation", it is *"multiple"*.

As in this (eliding the second variable for brevity):

auto x = p.x();
x = x * x - sin(x);
return {x}

...vs. this:

return { p.x() * p.x() - sin(p.x()) };

Please go back and re-read my previous post which explains how my
proposed unpacking is merely shorthand for the above form and does not
in any way violate encapsulation.

--
Matthew

Matthew Woehlke

unread,
Jun 1, 2015, 12:51:41 PM6/1/15
to std-pr...@isocpp.org
On 2015-06-01 12:15, Nicol Bolas wrote:
> On Saturday, May 30, 2015 at 12:35:06 PM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-29 19:57, Nicol Bolas wrote:
>>> The result of an expression is, without multiple return values, a
>>> single value with a single type. Therefore, in order for MRVs to
>>> work, that will have to change.
>>
>> Yes, it would have to change. That is a big part of my objection.
>
> ... why?

Because 'foo()' "looks like" a single semantic "thing". And right now it
always *means* a single semantic thing. You propose to make it possible
for it to suddenly mean *multiple* semantic things, with no indication
to the reader.

>>> The primary justification for this feature was having multiple return
>>> values. It's *the title of the thread*.
>>
>> Yes, but the *feature* was *multiple assignments*. (That's *also* in the
>> title of the thread. First, in fact.) AFAICT you are trying to kill, or
>> needlessly cripple, that.
>
> No, I'm trying to kill the "via tuple" part.

Then why do you object to MRV's being tuple-like? Or to generic
unpacking? Neither of these relate to "trying to kill the 'via tuple' part".

I want three things, all of which IMO can, and probably should, be
separate proposals:

1. Generic unpacking, i.e. multiple assignment from a "container".

2. A better (i.e. RVO-friendly) way to return multiple values without
the need to declare a struct for the purpose.

3. The ability to expand a "container" into multiple "values", e.g. as
function call arguments, or within a brace initializer list.

Each of these is made more valuable by the others, but each is also
useful by itself. None has an absolute dependency on another. (Not even
syntactically, I assert; see below.)

>> So, you are saying you *still* want unpacking syntax¹, you just only
>> want it to work for MRV's? Why? What is the benefit to limiting it in
>> such manner? (As previously explained, I don't buy the "syntax is
>> precious" argument.)
>
> You can choose not to buy it, but that doesn't make it false.

You are asking for this:

auto t = make_tuple(...);
[auto x; void; y] = foo(); // Legal
[auto a; void; b] = t; // Illegal / reserved for future use

Really? Leaving aside the compiler complexity arguments, I'd be inclined
to reject that just on the basis of the confusion it is likely to cause.
Context-sensitive syntax has a high cognitive burden. That is much less
an issue for context-sensitive keywords. (For one, most people will
pretend that they are just keywords, and not use them otherwise.)

As a code writer, I look at the above and want to know why the second
one doesn't work.

> If the syntax gets thrown away on "unpacking packed objects" (as opposed to
> "storing multiple values"), then there will be no syntax left to have
> actual MRVs as a first-class language feature. So they're not "orthogonal"
> in any way; the syntactic presence of one precludes the possibility,
> viability, and utility of the other.

If you said that [expr; ...] is only multiple assignment *if followed by
'='*, and means something else otherwise, I'd be okay with that. And in
particular, 'something else' can be MRV return specification. (In either
case, the restriction 'at the start of a LOC' can and probably should
apply.)

...But that also means there is no reason to limit the RHS of the
assignment to MRV's.

Are we *really* having this conversation because I neglected to mandate
the '='?

(Even more pedantic: the full specification of the syntax in question is
"[" <lvalue-expression or declaration> [ ";" ... ] "]" "=". So the
syntax "[" <type specifier> [ ";" ... ] "]" is still available.)

It should be obvious that it was never the intent to coopt the '['
symbol as the first symbol without limitation. At least, there is the
implied restriction that the meaning of "[a]0" - abomination though it
may be :-) - is not changed.

--
Matthew

Nicol Bolas

unread,
Jun 1, 2015, 9:06:02 PM6/1/15
to std-pr...@isocpp.org, mw_t...@users.sourceforge.net
On Monday, June 1, 2015 at 12:51:41 PM UTC-4, Matthew Woehlke wrote:
On 2015-06-01 12:15, Nicol Bolas wrote:
> On Saturday, May 30, 2015 at 12:35:06 PM UTC-4, Matthew Woehlke wrote:
>> On 2015-05-29 19:57, Nicol Bolas wrote:
>>> The result of an expression is, without multiple return values, a
>>> single value with a single type. Therefore, in order for MRVs to
>>> work, that will have to change.
>>
>> Yes, it would have to change. That is a big part of my objection.
>
> ... why?

Because 'foo()' "looks like" a single semantic "thing". And right now it
always *means* a single semantic thing. You propose to make it possible
for it to suddenly mean *multiple* semantic things, with no indication
to the reader.

I've been using Lua for years, and it never bothered me. Indeed, the only things that bother me about Lua's MRV syntax are certain places where it's not orthogonal (these are complex to explain, and none of them are necessary for C++).

So it seems to me that it's more a matter of what you're used to/expect than a general problem with the concept.

And it should be noted that you're trying to adopt this view as well. You want to treat a tuple as semantically indistinguishable from its component parts; to you, the tuple is an implementation detail of dealing with value sequences. That is the very core of the title of your thread: multiple assignments, multiple semantic values, via one syntactic value.

All I'm saying is that the implementation detail needs to go away: multiple assignments, multiple syntactic values.

I want three things, all of which IMO can, and probably should, be
separate proposals:

1. Generic unpacking, i.e. multiple assignment from a "container". 

2. A better (i.e. RVO-friendly) way to return multiple values without
the need to declare a struct for the purpose.

3. The ability to expand a "container" into multiple "values", e.g. as
function call arguments, or within a brace initializer list.

Each of these is made more valuable by the others, but each is also
useful by itself. None has an absolute dependency on another. (Not even
syntactically, I assert; see below.)

Here's the problem with the notion that these are all separate things.

If you start with solving #1, you will come up with a different solution to #2 than if you started with #2. Instead of introducing value sequences as a first-class construct, you'll likely do what was suggested earlier. You make some language hack type that mimics tuple, but has some arbitrary rules that allow elision through it. Similarly, for #3, you would introduce some syntax that will implicitly use the generic unpacking you used in #1, which is probably based directly on `std::get<>`.

However, if you start from #2, then you start from a position of needing the concept of value sequences. So your solution to #1 is based on calling an MRV function to do the generic unpacking. And thus, your solution to #3 will similarly use this same MRV function to do the generic unpacking, coupled with expanding where value sequences can be used.

So these are not terribly orthogonal concepts. Each may have uses on their own, but the overall solutions to these problems will be different. If you already have #2, the solution to #1 is going to look very different than if you don't.

>> So, you are saying you *still* want unpacking syntax¹, you just only
>> want it to work for MRV's? Why? What is the benefit to limiting it in
>> such manner? (As previously explained, I don't buy the "syntax is
>> precious" argument.)
>
> You can choose not to buy it, but that doesn't make it false.

You are asking for this:

  auto t = make_tuple(...);
  [auto x; void; y] = foo(); // Legal
  [auto a; void; b] = t; // Illegal / reserved for future use

Really? Leaving aside the compiler complexity arguments, I'd be inclined
to reject that just on the basis of the confusion it is likely to cause.
Context-sensitive syntax has a high cognitive burden. That is much less
an issue for context-sensitive keywords. (For one, most people will
pretend that they are just keywords, and not use them otherwise.)

As a code writer, I look at the above and want to know why the second
one doesn't work.

As a code writer, I would look at the above and realize, "hey, that's a tuple, not a value sequence. I shouldn't expect to be able to transform one thing into multiple things without an explicit conversion."

A tuple is a value; a single value. It is only a "collection of values" semantically; syntactically, a tuple is no more multiple values than an integer is.

And to be quite frank, from the perspective what "a code writer" thinks ought to happen, the fact that `vector<int>{3}` and `vector<int>{{3}}` do the same thing is far more pernicious than the notion that a tuple and a value sequence would not be directly interchangeable.

Miro Knejp

unread,
Jun 2, 2015, 8:17:53 AM6/2/15
to std-pr...@isocpp.org
I was thinking some more about how this could be implemented and just wanted to give my viewpoint on this.

Disclaimer: In all examples below I assume the comma operator does not exist and is more or less pseudocode to make it more readable. Syntax bikeshedding can be done once the semantic issues are solved. I also assume none of the functions are inlined.

Consider how RVO is typically done today. Given the function

T foo(A a, B b);

and assuming T is too big to be passed in registers. Now imagining the function performs RVO the compiler transforms this signature in something like

void foo(T* result, A a, B b);

Now imagine we have multiple return values:

T, U, V bar(A a, B b)

In the best-case scenario RVO is possible for all three return values, transforming the function into

struct __Result { T t; U u; V v; };
void bar(__Result* result, A a, B b);

This assumes all three types are copyable or at least movable to their final destination.
In the case where all three return values are used for initialization this is pretty trivial.

T t, U u, V v = bar(a, b);

transforms into

#1
__Result __result; // not initialized
bar(&__result, a, b);
// t, u, v are now aliases to __result.t, __result.u, __result.v

Now what if the initialization is different

#2
T t;
U u;
t, u, V v = bar(a, b);

This must be transformed to

__Result __result; // not initialized
bar(&__result, A a, B b);
// v is now an alias to result.v
t = std::move(__result.t);
u = std::move(__result.u);
__result.u.~U();
__result.t.~T();

This means there is wasted stack space for t and u (due to the fixed layout of __Result) __result.t and __result.u have to stay until v goes out of scope. It could be recycled for other variables, but just saying.

There is of course an alternative way how the signature can be transformed:

void baz(T* t, U* u, V* v, A a, B b);

Here RVO can be applied directly to each individual result value. This means the caller is much more flexible in where to store the results:

#3 (semantically equivalent to #1)
T t; // not initialized
U u; // not initialized
V v; // not initialized
baz(&t, &u, &v, a, b);

#4 (semantically equivalent to #2)
T t;
U u;
V v; // not initialized
{
T __t; // not initialized
U __u; // not initialized
baz(&__t, &__u, &v, a, b);
t = std::move(__t);
u = std::move(__u);
}

There is no difference in the number of operations but #4 is more efficient in stack space usage than #2, but it has to push 2 more pointers on the call stack, however that stack space for __t and __u is occupied only temporarily.

This is how I think the compiler might realize multiple return values with RVO support. People who actually write compilers for a living probably have some aces up their sleeves.

The first option with an implementation-defined and ABI-consistent out-structure only requires one additional argument to the function, however the caller is severely restricted in how it can layout code around the invocation. The second option requires more arguments to the call, meaning pushing more stuff on the stack, however it gives the caller more options to eliminate stack overhead.

This difference is important when it comes to argument passing:

void consume(T t, U u, V v);

Let’s examine the expressions "consume(foo(a, b))" if realized with either bar() or baz():

#5
{
__Result __result; // not initialized
bar(&__result, a, b);
consume(std::move(__result.t), std::move(__result.u), std::move(__result.v))
}

#6
{
T __t; // not initialized
U __u; // not initialized
V __v; // not initialized
baz(&__t, __u, __v, a, b);
consume(std::move(__t), std::move(__u), std::move(__v))
}

Again, same number of operations, *but* in #6 the compiler can arrange the storage for __t, __u, and __v in such a way that they can be passed directly into consume without move:

#7
{
T __t; // not initialized
U __u; // not initialized
V __v; // not initialized
baz(&__t, __u, __v, a, b);
call consume // __t, __u and __v are pre-allocated at the stack locations required for consume() call
}

This optimization gets rid of the moves and allows in-place construction of the parameters to consume(). This may not be possible with #5 if the layout of the structure is not exactly the same as the layout of the same argument sequence to a function (and obviously if no conversions are required).

Solutions #2, #4, #6 and #7 become much more difficult (if not impossible) if the structure used for transporting the return values is not compiler-generated but a user provided std::tuple, std::pair or anything else that is not “magic” or with the exact layout required by the ABI.

I guess my whole point here is that there are at least two ways to implement this that need to be hammered into the ABI and dictate how much flexibility the caller has and how much call overhead there is per return value (1 or n additional pointers pushed). Once there is a syntax for this compiler-generated unpacking it can surely be extended to user-defined types. I just don’t think that there is as much elision potential for user-defined types.

Matthew Woehlke

unread,
Jun 2, 2015, 10:03:58 AM6/2/15
to std-pr...@isocpp.org
On 2015-06-01 21:06, Nicol Bolas wrote:
> On Monday, June 1, 2015 at 12:51:41 PM UTC-4, Matthew Woehlke wrote:
>> 'foo()' "looks like" a single semantic "thing". And right now it
>> always *means* a single semantic thing. You propose to make it
>> possible for it to suddenly mean *multiple* semantic things, with
>> no indication to the reader.
>
> I've been using Lua for years, and it never bothered me. Indeed, the only
> things that bother me about Lua's MRV syntax are certain places where it's
> not orthogonal (these are complex to explain, and none of them are
> necessary for C++).
>
> So it seems to me that it's more a matter of what you're used to/expect
> than a general problem with the concept.

That may be true. C++ coders however are likely used to one "thing"
actually being one "thing".

> And it should be noted that you're trying to adopt this view as well. You
> want to treat a tuple as semantically indistinguishable from its component
> parts; to you, the tuple is an implementation detail of dealing with value
> sequences. That is the very core of the title of your thread: multiple
> assignments, multiple *semantic* values, via one *syntactic* value.

I'm not sure I'd say that (although I'm not exactly sure what you're
saying). Rather, I would like to be able to interchange more freely
between value sequence containers and the contained collection of
values. Regardless of the actual type of the container :-).

> All I'm saying is that the implementation detail needs to go away: multiple
> assignments, multiple *syntactic* values.

...yes? I *think* I am saying that also...

>> I want three things, all of which IMO can, and probably should, be
>> separate proposals:
>>
>> 1. Generic unpacking, i.e. multiple assignment from a "container".
>>
>> 2. A better (i.e. RVO-friendly) way to return multiple values without
>> the need to declare a struct for the purpose.
>>
>> 3. The ability to expand a "container" into multiple "values", e.g. as
>> function call arguments, or within a brace initializer list.
>>
>> Each of these is made more valuable by the others, but each is also
>> useful by itself. None has an absolute dependency on another. (Not even
>> syntactically, I assert; see below.)
>
> Here's the problem with the notion that these are all separate things.
>
> If you *start* with solving #1, you will come up with a different solution
> to #2 than if you started with #2. Instead of introducing value sequences
> as a first-class construct, you'll likely do what was suggested earlier.
> You make some language hack type that mimics tuple, but has some arbitrary
> rules that allow elision through it.

I guess I don't understand what difference this makes. What features
does my implementation lack that yours provides? (Besides implicit
expansion, which as stated above I don't want anyway.)

> Similarly, for #3, you would introduce some syntax that will
> implicitly use the generic unpacking you used in #1, which is
> probably based directly on `std::get<>`.

Yes, but again, what's wrong with that?

> However, if you start from #2, then you start from a position of needing
> the concept of value sequences. So your solution to #1 is based on calling
> an MRV function to do the generic unpacking.

...and why should I have to write additional code to unpack something?
(Moreover, how are you going to provide a non-ugly implementation of
that unpacking function? In particular, can it be written without
recursion? Unpacking generated by the compiler will create the best
possible code, and likely provide better diagnostics when there is an
error.)

> So these are not terribly orthogonal concepts. Each may have uses on their
> own, but the overall solutions to these problems will be different. If you
> already have #2, the solution to #1 is going to look very different than if
> you don't.

Okay, I can see that, but I share your opinion in reverse: I *prefer*
the solution that arises from providing #1 first and dislike the
solution that becomes the only option if #2 is addressed first without
considering #1. Also, I still want to be able to capture the entire MRV
set as a single "entity".

>> You are asking for this:
>>
>> auto t = make_tuple(...);
>> [auto x; void; y] = foo(); // Legal
>> [auto a; void; b] = t; // Illegal / reserved for future use
>
> As a code writer, I would look at the above and realize, "hey, that's a
> tuple, not a value sequence. I shouldn't expect to be able to transform one
> thing into multiple things without an explicit conversion."

Um... which is exactly my point at the start of this thread, in a
slightly different context.

In this context, I would say that unpacking *is* an explicit conversion.
That's sort of how I define unpacking, in fact; taking a single
syntactic "thing" that is multiple semantic "things", splitting it apart
into it's component semantic parts, and assigning (or discarding) the parts.

--
Matthew

Matthew Woehlke

unread,
Jun 2, 2015, 10:23:08 AM6/2/15
to std-pr...@isocpp.org
On 2015-06-02 08:17, Miro Knejp wrote:
> Now imagine we have multiple return values:
>
> T, U, V bar(A a, B b)
>
> In the best-case scenario RVO is possible for all three return values, transforming the function into
>
> struct __Result { T t; U u; V v; };
> void bar(__Result* result, A a, B b);
>
> This assumes all three types are copyable or at least movable to their final destination.

...no? With RVO you are *not* copying/moving, you are constructing in
place. (Well, IIRC the standard requires that they *be* copyable, but
the point is that they won't actually *be* copied.)

IOW, this:

auto answer = some_logic();
...
return answer;

...becomes this:

new (sizeof(ResultType), result) ResultType{some_logic()};
...
return;

> In the case where all three return values are used for initialization this is pretty trivial.
>
> T t, U u, V v = bar(a, b);
>
> transforms into
>
> #1
> __Result __result; // not initialized
> bar(&__result, a, b);
> // t, u, v are now aliases to __result.t, __result.u, __result.v

Correct.

> Now what if the initialization is different
>
> #2
> T t;
> U u;
> t, u, V v = bar(a, b);
>
> This must be transformed to
>
> __Result __result; // not initialized
> bar(&__result, A a, B b);
> // v is now an alias to result.v
> t = std::move(__result.t);
> u = std::move(__result.u);
> __result.u.~U();
> __result.t.~T();

Also correct (in particular, thanks for noting that the destructors need
to be called immediately!), and...

> This means there is wasted stack space for t and u (due to the fixed
> layout of __Result) __result.t and __result.u have to stay until v
> goes out of scope. It could be recycled for other variables, but just
> saying.

...unavoidable AFAICT. Well... the compiler should "know" that the
storage for __result.t and __result.u is now unused and could be reused,
but you are correct that the storage can't be *released* in this case.
(It could if it was at the end of the result list, but I assume you knew
that.)

> [snip lots]
> I guess my whole point here is that there are at least two ways to
> implement this that need to be hammered into the ABI and dictate how
> much flexibility the caller has and how much call overhead there is
> per return value (1 or n additional pointers pushed). Once there is a
> syntax for this compiler-generated unpacking it can surely be
> extended to user-defined types.

Two questions:

1. Is the overhead of passing more pointers to the called function worth
the benefits? I ask objectively. I think your argument is convincing,
but ultimately a qualified answer to this question is desired before
choosing an implementation. (This may not be a question to be answered
by the standard, either, but by compiler vendors.)

2. Can the second form allow for code such as the following example? Is
there a necessary performance penalty?

auto results = foo();
for (;;)
bar(results...);

(Doesn't need to be an infinite loop, so long as the compiler is
prevented from transforming the separate call and assignment into
'bar(foo()...)'.)

> I just don’t think that there is as much elision potential for
> user-defined types.

I think we all agree on that much. I'm not sure there is *any* elision
possible :-).

--
Matthew

Miro Knejp

unread,
Jun 2, 2015, 11:55:03 AM6/2/15
to std-pr...@isocpp.org

> On 02 Jun 2015, at 16:22 , Matthew Woehlke <mw_t...@users.sourceforge.net> wrote:
>
> On 2015-06-02 08:17, Miro Knejp wrote:
>> Now imagine we have multiple return values:
>>
>> T, U, V bar(A a, B b)
>>
>> In the best-case scenario RVO is possible for all three return values, transforming the function into
>>
>> struct __Result { T t; U u; V v; };
>> void bar(__Result* result, A a, B b);
>>
>> This assumes all three types are copyable or at least movable to their final destination.
>
> ...no? With RVO you are *not* copying/moving, you are constructing in
> place. (Well, IIRC the standard requires that they *be* copyable, but
> the point is that they won't actually *be* copied.)

Right, that statement referred to a previous version of the example and I just forgot to remove the sentence. Though it still does apply if the values must be moved out of __result to a *final destination*.
>
>
>
>> [snip lots]
>> I guess my whole point here is that there are at least two ways to
>> implement this that need to be hammered into the ABI and dictate how
>> much flexibility the caller has and how much call overhead there is
>> per return value (1 or n additional pointers pushed). Once there is a
>> syntax for this compiler-generated unpacking it can surely be
>> extended to user-defined types.
>
> Two questions:
>
> 1. Is the overhead of passing more pointers to the called function worth
> the benefits? I ask objectively. I think your argument is convincing,
> but ultimately a qualified answer to this question is desired before
> choosing an implementation. (This may not be a question to be answered
> by the standard, either, but by compiler vendors.)

I do not have any measurements so I don’t know. In the end this is for the ABI designers to decide. As I said though, if __Result has the *exact same* layout as is required for the same sequence of parameters for a function call then the single-pointer version only has the "potentially wasted” stack space of partial __Result structures if it cannot be re-used in the same scope. However in the real world there are also parameters passed in registers, so this all becomes a little more complicated. I guess the functions could instead be transformed as

__Result foo(A a, B b);

and let the already existing ABI rules for compound type returns apply.

>
> 2. Can the second form allow for code such as the following example? Is
> there a necessary performance penalty?
>
> auto results = foo();
> for (;;)
> bar(results...);
>
> (Doesn't need to be an infinite loop, so long as the compiler is
> prevented from transforming the separate call and assignment into
> 'bar(foo()...)'.)
>

This is basically equivalent to

auto a = …
auto b = …
auto c = ...
for(;;)
bar(a, b, c);

The only real difference is how the parameters are initialized but since the values captured in “results” are lvalues the usual procedure applies. The scenario bar(foo()) is somewhat a special case because foo() produces prvalues which are immediately consumed by bar().

It is loading more messages.
0 new messages