Going further with parameter packs: aliases, variables and return values

3,541 views
Skip to first unread message

Alex B

unread,
Nov 22, 2013, 2:36:47 PM11/22/13
to std-pr...@isocpp.org
Hi,
 
I have been working on several ideas that would fill several gaps and make things much easier when dealing with parameter packs. Before going in with a formal proposal, I first wanted to share my ideas. Feedback would be greatly appreciated.
 
Type pack alias
 
A pack alias (templated or not) can be defined with a "using..." directive. On the right hand side must be an unexpanded type pack. This unexpanded type pack can come from an unexpanded parameter pack of a variadic template or from another pack alias.
 

A pack alias can be expanded with the "..." operator in the same way as an unexanded parameter pack of a variadic template.

The most basic (and useful) templated pack alias would be the following, which allow creating packs on the fly:

template <class... T>
using... type_pack = T;

 

Usage example:

using... my_types = type_pack<int, char, double>;
using my_tuple = tuple<my_types...>;

 

Variable pack
 

Variable packs are the same as function parameter packs, except that they can be declared anywhere a variable can be declared. There are two ways to declare and initialize a variable pack.

The first one is by using an already defined type pack. If constructor arguments are specified as single values, all the constructors of the variables in the pack are called with those same arguments.

using... types = type_pack<int, char, double>;
types values1
;
type_pack
<int, char, double> values2;
types values3
= 10; // all 3 variables are initialized to 10
types values4
(10); // same
types values5
= values2;
type_pack
<double, double, double> values6 = values2;
//type_pack<int, char> values7 = values2; //error: mismatching pack sizes

template <class... T>
class C
{
 T
... values;
};

template <class... T>
void f(T... t)
{
 T
... t2 = t * 2;
}
 
 
A variable pack can also be declared without a defined type pack. In that case, it must be initialized from another variable pack.
 
//double... values8; //error: unknown pack size
//double... values9 = 10.0; //error: unknown pack size
double... values10 = values2; // pack of 3 variables of type double
double... values11(values2); // same
auto... values12 = values2; // pack of 3 variables of types int, char and double



template <class... T>
void g(T... t)
{
 
auto... t2 = t * 2;
}


Operations can be done on packs if all the unexpanded packs are of the same size.

values10 = values11 * 2.0;
values11
+= values10;
values11
= values10 + values11;


Single values can be used along with pack but only in operations in which those single values are const.

double single_value = 1.0;
values10
*= single_value; //ok: rhs is const single_value
single_value
*= values10; //error: lhs of operator*= cannot be const

void h(const double&, double&) {}

h
(values10, values11); //ok: 2 packs of the same size (h will be called 3 times)
h
(single_value, values10); //ok: first arg of h is const (h will be called 3 times, each time with the same single_value)
h
(values10, single_value); //error: trying to pass a non-const single value in an unexpanded context

 

Variable template pack

Variable packs should also work for variable templates.

The most basic (and useful) variable template packs would be an equivalent to std::integer_sequence that would allow working directly with an integer pack (instead of having to store that pack in a type).

template <class T, T... I>
constexpr T... integer_pack = I;

template <size_t... I>
constexpr size_t... index_pack = I;

template <class T, T N>
constexpr T... make_integer_pack = integer_pack<T, /* a sequence 0, 1, 2, ..., N-1 */>;

template <size_t N>
constexpr size_t... make_index_pack = make_integer_pack<size_t, N>;

template <class... T>
constexpr size_t... index_pack_for = make_index_pack<sizeof...(T)>;
 

With these variable template packs, additional utilies can be provided for tuple-like classes:

// tuple_indices<T> is equivalenent to 0, 1, ..., tuple_size<T>::value - 1
template <class T>
constexpr size_t... tuple_indices = make_index_pack<tuple_size<T>::value>;

// tuple_elements<T> is equivalent to tuple_element_t<0, T>, tuple_element_t<1, T>, ..., tuple_element_t<N-1, T>
template <class T>
using... tuple_elements = tuple_element_t<tuple_indices<T>, T>;


These are much more simpler to use than current workarounds requiring usage of std::integer_sequence. Example of passing all elements of tuple t as arguments to a function f:

template <class... Args>
void f(Args&&... args);

template <class Tuple>
void g(T&& t)
{
 f
(get<tuple_indices<T>>(forward<T>(t))...);
}

 

Returning a pack from a function

With pack variables comes the need for functions returning a pack of values.

The most basic function that could be defined is one returning a pack from several values:

template <class... T>
decltype(auto)... make_pack(T&&... t) { return forward<T>(t); }
 
Usage example:
 
void g(int a, int b, int c, int d, int e, int f, int g, int h);

// Following functions forward all arguments to g by first multiplying them by 2 and adding 3
void f(int a, int b, int c, int d, int e, int f, int g, int h)
{
 
// Old way
 g
(a*2 + 3, b*2 + 3, c*2 + 3, d*2 + 3, e*2 + 3, f*2 + 3, g*2 + 3, h*2 + 3);
 
// New way
 g
(make_pack(a, b, c, d, e, f, g, h)*2 + 3 ...);
}

Another usage would be a function returning all the values of a tuple-like class:

// get_all(t) is equivalent to get<0>(t), get<1>(t), ..., get<N-1>(t)
template <class T>
constexpr decltype(auto)... get_all(T&& t) { return get<tuple_indices<T>>(forward<T>(t)); }

Usage example (passing all elements of tuple t as arguments to a function f):

template <class... T>
void f(T&&... t);

template <class Tuple>
void g(T&& t)
{
 f
(get_all(forward<T>(t))...);
}

 

Expanding nested pack aliases

Considering the following:

using... original = type_pack<T0, T1, /*...*/, TN>;

template <class... T>
using... alias = /*...*/;

using... a = alias<original...>;
using... b = alias<original>...;


The pack a is the result of applying the alias on the entire original pack.
The pack b is the result of applying the alias on each individual elements of the original pack.

using... a = alias<T0, T1, /*...*/, TN>;
using... b = type_pack<alias<T0>..., alias<T1>..., /*...*/, alias<TN>...>;

When multiple aliases are used, the same rules can be applied successively by considering the inner-most pack first.
The following example illustrates how the rules are applied when 2 aliases are used on a pack.

using... orig = type_pack<T0, T1, /*...*/, TN>;

template <class... T>
using... a1 = /*...*/;

template <class... T>
using... a2 = /*...*/;

using... a = a1<a2<orig...>...>;
// same as: using... temp = a2<orig...>;
//          using... a = a1<temp...>;

using... b = a1<a2<orig>... ...>;
// same as: using... temp = a2<orig>...;
//          using... b = a1<temp...>;

using... c = a1<a2<orig...>>...;
// same as: using... temp = a2<orig...>;
//          using... c = a1<temp>...;

using... d = a1<a2<orig>...>...;
// same as: using... temp = a2<orig>...;
//          using... d = a1<temp>...;

//using... e = a1<a2<orig>>... ...;
// error

//using... f = a1<a2<orig... ...>>;
// error


Additional examples

struct A {};
struct B {};

template <class... T>
using... twice = type_pack<T..., T...>;

using... AB = type_pack<A, B>;             // A, B
using... t1 = twice<AB...>;                // A, B, A, B
using... t2 = type_pack<twice<AB...>...>;  // A, B, A, B
using... t3 = twice<AB>...;                // A, A, B, B
using... t4 = type_pack<twice<AB>... ...>; // A, A, B, B
using... t5 = twice<twice<AB...>...>;      // A, B, A, B, A, B, A, B
using... t6 = twice<twice<AB>... ...>;     // A, A, B, B, A, A, B, B
using... t7 = twice<twice<AB...>>...;      // A, A, B, B, A, A, B, B
using... t8 = twice<twice<AB>...>...;      // A, A, A, A, B, B, B, B

struct C {};

template <class... T>
using... add_c = type_pack<T..., C>;

using... u1 = add_c<AB...>;            // A, B, C
using... u2 = add_c<AB>...;            // A, C, B, C
using... u3 = add_c<add_c<AB...>...>;  // A, B, C, C
using... u4 = add_c<add_c<AB>... ...>; // A, C, B, C, C
using... u5 = add_c<add_c<AB...>>...;  // A, C, B, C, C, C
using... u6 = add_c<add_c<AB>...>...;  // A, C, C, C, B, C, C, C

using... v1 = add_c<twice<AB...>...>;  // A, B, A, B, C
using... v2 = add_c<twice<AB>... ...>; // A, A, B, B, C
using... v3 = add_c<twice<AB...>>...;  // A, C, B, C, A, C, B, C
using... v4 = add_c<twice<AB>...>...;  // A, C, A, C, B, C, B, C

using... w1 = twice<add_c<AB...>...>;  // A, B, C, A, B, C
using... w2 = twice<add_c<AB>... ...>; // A, C, B, C, A, C, B, C
using... w3 = twice<add_c<AB...>>...;  // A, A, B, B, C, C
using... w4 = twice<add_c<AB>...>...;  // A, A, C, C, B, B, C, C

 

Richard Smith

unread,
Nov 22, 2013, 3:33:32 PM11/22/13
to std-pr...@isocpp.org
On Fri, Nov 22, 2013 at 11:36 AM, Alex B <deva...@gmail.com> wrote:
Hi,
 
I have been working on several ideas that would fill several gaps and make things much easier when dealing with parameter packs. Before going in with a formal proposal, I first wanted to share my ideas. Feedback would be greatly appreciated.
 
Type pack alias
 
A pack alias (templated or not) can be defined with a "using..." directive. On the right hand side must be an unexpanded type pack. This unexpanded type pack can come from an unexpanded parameter pack of a variadic template or from another pack alias.
 

A pack alias can be expanded with the "..." operator in the same way as an unexanded parameter pack of a variadic template.

The most basic (and useful) templated pack alias would be the following, which allow creating packs on the fly:

template <class... T>
using... type_pack = T;

 

Usage example:

using... my_types = type_pack<int, char, double>;
using my_tuple = tuple<my_types...>;


This immediately creates a problem. One of the properties of packs today is that it is possible to syntactically determine whether some snippet of C++ syntax contains unexpanded parameter packs, and to determine which packs it contains. This is crucial to some implementation techniques.

It's also important in resolving some grammar ambiguities. Today, if I have:

  template<typename T> void f(typename T::type x ...);

I know this is a vararg function. With your proposal, I could presumably write:

  template<typename...Ts> struct X {
    using ...type = Ts;
  };
  void g() {
    f<X<int, int, int>>(1, 2, 3);
  }

and 'f' would mean something fundamentally different.

I had a similar-but-different proposal a while back (which I've not written up for committee consideration yet) which avoids this problem by using a slightly different syntax:

  template<typename...Ts> struct types {
    using ... = Ts; // "...types<Ts>" is the pack "Ts"
  };

  typedef std::types<int, char, double> icd; // just a normal type
  void f(...icd ...x) // "...icd" is a pack of types; this is "void f(int, char, double)" except that we have a pack of parameters

Variable pack
 

Variable packs are the same as function parameter packs, except that they can be declared anywhere a variable can be declared. There are two ways to declare and initialize a variable pack.

The first one is by using an already defined type pack. If constructor arguments are specified as single values, all the constructors of the variables in the pack are called with those same arguments.

using... types = type_pack<int, char, double>;
types values1
;

This syntax doesn't work. Consider:

  template<typename ...Ts>
  void f(Ts ...ts) {
    g(ts + []() {
      types values;
      return values;
    }() ...);
  }

This would be ambiguous: does each lambda declare a *pack* of values, or a *slice* of that pack of values? The way to fix this is to consistently use pack expansion whenever you expand a pack:

  // assuming 'types' names a pack of types
  types ...values1; // ok, declare a pack of values named 'values1'.

This would still be problematic if you allowed pack expansions as a static or non-static data member, because of the above-mentioned problem with being unable to syntactically determine what is and is not a pack.


type_pack
<int, char, double> values2;
types values3
= 10; // all 3 variables are initialized to 10
types values4
(10); // same
types values5
= values2;
type_pack
<double, double, double> values6 = values2;
//type_pack<int, char> values7 = values2; //error: mismatching pack sizes

template <class... T>
class C
{
 T
... values;

This, for instance, is a problem, because X::values (for a dependent type X) might or might not be a pack if this were valid.
 

};

template <class... T>
void f(T... t)
{
 T
... t2 = t * 2;
}
 
 
A variable pack can also be declared without a defined type pack. In that case, it must be initialized from another variable pack.
 
//double... values8; //error: unknown pack size
//double... values9 = 10.0; //error: unknown pack size
double... values10 = values2; // pack of 3 variables of type double
double... values11(values2); // same

Seems fine to me: the pack expansion includes 'values2', so can infer its length from there.
 

auto... values12 = values2; // pack of 3 variables of types int, char and double

Presumably this means that your expansion model is that this expands to

  auto values12$0 = values2$0;
  auto values12$1 = values2$1;
  auto vaules12$2 = values2$2;

not the more natural choice of

  auto values12$0 = values2$0, values12$1 = values2$1, vaules12$2 = values2$2;

(which would be ill-formed, because the 'auto' deduces to different types in different deductions).
 



template <class... T>
void g(T... t)
{
 
auto... t2 = t * 2;
}


Operations can be done on packs if all the unexpanded packs are of the same size.

values10 = values11 * 2.0;
values11
+= values10;
values11
= values10 + values11;

Again, this doesn't work, due to ambiguity over where the expansion happens. Instead, I'd suggest (and indeed, have been intending to propose for standardization) adding an optional '...' before the ';' in an expression-statement, to represent a pack expansion. So you'd write:

  values10 = values11 * 2.0 ...;

and it would be equivalent to today's

  sink{ values10 = values11 * 2.0 ... };
 
(for an appropriately-defined class 'sink')

Single values can be used along with pack but only in operations in which those single values are const.

double single_value = 1.0;
values10
*= single_value; //ok: rhs is const single_value
single_value
*= values10; //error: lhs of operator*= cannot be const

void h(const double&, double&) {}

h
(values10, values11); //ok: 2 packs of the same size (h will be called 3 times)
h
(single_value, values10); //ok: first arg of h is const (h will be called 3 times, each time with the same single_value)
h
(values10, single_value); //error: trying to pass a non-const single value in an unexpanded context

 

Variable template pack

Variable packs should also work for variable templates.

The most basic (and useful) variable template packs would be an equivalent to std::integer_sequence that would allow working directly with an integer pack (instead of having to store that pack in a type).

template <class T, T... I>
constexpr T... integer_pack = I;

template <size_t... I>
constexpr size_t... index_pack = I;

template <class T, T N>
constexpr T... make_integer_pack = integer_pack<T, /* a sequence 0, 1, 2, ..., N-1 */>;

My prior proposal had syntax for this sequence: ...N. This was also overloadable for user-defined types, so that (for instance) for a tuple t, '...t' could be sugar for 'get<...N>(t)'.


template <size_t N>
constexpr size_t... make_index_pack = make_integer_pack<size_t, N>;

template <class... T>
constexpr size_t... index_pack_for = make_index_pack<sizeof...(T)>;
 

With these variable template packs, additional utilies can be provided for tuple-like classes:

// tuple_indices<T> is equivalenent to 0, 1, ..., tuple_size<T>::value - 1
template <class T>
constexpr size_t... tuple_indices = make_index_pack<tuple_size<T>::value>;

// tuple_elements<T> is equivalent to tuple_element_t<0, T>, tuple_element_t<1, T>, ..., tuple_element_t<N-1, T>
template <class T>
using... tuple_elements = tuple_element_t<tuple_indices<T>, T>;


These are much more simpler to use than current workarounds requiring usage of std::integer_sequence. Example of passing all elements of tuple t as arguments to a function f:

template <class... Args>
void f(Args&&... args);

template <class Tuple>
void g(T&& t)
{
 f
(get<tuple_indices<T>>(forward<T>(t))...);
}
 
This is still a little unclean. My proposal supports this as:

  f(...std::forward<T>(t) ...);

Returning a pack from a function

With pack variables comes the need for functions returning a pack of values.


Why not return a tuple? Packs are not types, so functions returning packs doesn't really seem to make much sense.
 

The most basic function that could be defined is one returning a pack from several values:

template <class... T>
decltype(auto)... make_pack(T&&... t) { return forward<T>(t); }
 
Usage example:
 
void g(int a, int b, int c, int d, int e, int f, int g, int h);

// Following functions forward all arguments to g by first multiplying them by 2 and adding 3
void f(int a, int b, int c, int d, int e, int f, int g, int h)
{
 
// Old way
 g
(a*2 + 3, b*2 + 3, c*2 + 3, d*2 + 3, e*2 + 3, f*2 + 3, g*2 + 3, h*2 + 3);
 
// New way
 g
(make_pack(a, b, c, d, e, f, g, h)*2 + 3 ...);

Again, this has the problem of not syntactically identifying what's a pack and what's not. With my syntax:

  g(...tie(a, b, c, d, e, f, g, h)*2 + 3...);

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

Richard Smith

unread,
Nov 22, 2013, 3:36:52 PM11/22/13
to std-pr...@isocpp.org

Alex B

unread,
Nov 25, 2013, 10:41:33 AM11/25/13
to std-pr...@isocpp.org
Hi Richard,

Thanks for you time; you bring a lot of good points. I just took a look at the discussion you linked. I have to say that overloading operator... like you propose is indeed really interesting. It is really neat and doesn't have some of the issues introduced by my proposal. However, if I can manage to solve those issues, I think (from my point of view) that my proposal would provide a bit more power and fill more gaps in the language. See below my answers to the issues you raised.

Usage example:

using... my_types = type_pack<int, char, double>;
using my_tuple = tuple<my_types...>;


This immediately creates a problem. One of the properties of packs today is that it is possible to syntactically determine whether some snippet of C++ syntax contains unexpanded parameter packs, and to determine which packs it contains. This is crucial to some implementation techniques.

It's also important in resolving some grammar ambiguities. Today, if I have:

  template<typename T> void f(typename T::type x ...);

I know this is a vararg function. With your proposal, I could presumably write:

  template<typename...Ts> struct X {
    using ...type = Ts;
  };
  void g() {
    f<X<int, int, int>>(1, 2, 3);
  }

and 'f' would mean something fundamentally different.

First, the ellipsis should go after the type name, not the var name. So with my proposal, f would be declared like this:

   template<typename T> void f(typename T::type... x);

Secondly, you are right that it remains ambiguous if T::type is a pack or not. I guess a proper solution would be to have the "typename..." keyword (used in the same context/fashion as regular "typename"). The example would become:

   template<typename T> void f(typename... T::type... x);

So first ellipsis (after typename) would indicate that what follows is a pack (of types) and second ellipsis is for pack expansion.

Note that the same problem exists for a pack of values as you point out later (see my answer to this).



I had a similar-but-different proposal a while back (which I've not written up for committee consideration yet) which avoids this problem by using a slightly different syntax:

  template<typename...Ts> struct types {
    using ... = Ts; // "...types<Ts>" is the pack "Ts"
  };

  typedef std::types<int, char, double> icd; // just a normal type
  void f(...icd ...x) // "...icd" is a pack of types; this is "void f(int, char, double)" except that we have a pack of parameters

With my proposal you save the first ellipsis. It's no big deal, but sounds cleaner to me to be able to define a pack alias directly than having to use a wrapper type which has to be "unwrapped" every time.


using... types = type_pack<int, char, double>;
types values1
;

This syntax doesn't work. Consider:

  template<typename ...Ts>
  void f(Ts ...ts) {
    g(ts + []() {
      types values;
      return values;
    }() ...);
  }

This would be ambiguous: does each lambda declare a *pack* of values, or a *slice* of that pack of values? The way to fix this is to consistently use pack expansion whenever you expand a pack:

  // assuming 'types' names a pack of types
  types ...values1; // ok, declare a pack of values named 'values1'.

I think you are right. And it sounds consistent to me; that is how function parameter packs are declared. Note taken.


template <class... T>
class C
{
 T
... values;

This, for instance, is a problem, because X::values (for a dependent type X) might or might not be a pack if this were valid.

Good point. Those would be ambiguous:

   template <class T>
   void f(T t)
   {
      using... icd = type_pack<int, char, double>;
      icd... a = t.x; // member variable
      icd... b = t.y(); // member function
      icd... c = T::z; // static member variable
      icd... d = T::w(); // static member function
   }

Here it is ambiguous whether x, y, z and w are single values or packs of values (note that there is no ambiguity for a non-template type T). To disambiguate those cases, we could prefix with an ellipsis (indicating that what follows is a pack):

      icd... a = ... t.x;
      icd... b = ... t.y();
      icd... c = ... T::z;
      icd... d = ... T::w();

This is similar to the "typename..." keyword but without the "typename" since these are values.

Note that this would be in conflict with the operator... in your proposal. It's a bit sad because I like your idea of overloading operator... and so far it wasn't conflicting.



auto... values12 = values2; // pack of 3 variables of types int, char and double

Presumably this means that your expansion model is that this expands to

  auto values12$0 = values2$0;
  auto values12$1 = values2$1;
  auto vaules12$2 = values2$2;

not the more natural choice of

  auto values12$0 = values2$0, values12$1 = values2$1, vaules12$2 = values2$2;

(which would be ill-formed, because the 'auto' deduces to different types in different deductions).

I didn't think about this. Indeed, as you say, for declaring packs it sounds better to expand in the "less natural" way that you described (but I don't like calling it less natural).

Operations can be done on packs if all the unexpanded packs are of the same size.

values10 = values11 * 2.0;
values11
+= values10;
values11
= values10 + values11;

Again, this doesn't work, due to ambiguity over where the expansion happens. Instead, I'd suggest (and indeed, have been intending to propose for standardization) adding an optional '...' before the ';' in an expression-statement, to represent a pack expansion. So you'd write:

  values10 = values11 * 2.0 ...;

That sounds like a good idea that would work with the current proposal.
I wonder if it would be required also when declaring :

1)  auto... values12 = values2;
vs
2)  auto... values12 = values2 ...;

Unless I'm wrong, (2) is invalid because the first ellipsis is already expanding the pack.


template <class... Args>
void f(Args&&... args);

template <class Tuple>
void g(T&& t)
{
 f
(get<tuple_indices<T>>(forward<T>(t))...);
}
 
This is still a little unclean. My proposal supports this as:

  f(...std::forward<T>(t) ...);

See my "cleaner" example a bit further:
f(get_all(forward<T>(t))...);

I've got to admit that in this case it's not as clean as your solution, but still it's not that bad.
Change the global get_all by a member function and it also gets nicer:
f(forward<T>(t).all()...);

Returning a pack from a function

With pack variables comes the need for functions returning a pack of values.


Why not return a tuple? Packs are not types, so functions returning packs doesn't really seem to make much sense.

In your proposal, isn't the operator...() function returning a pack? It looks like we have the same problem to solve.

So far I can think of two expansion models for returning packs. I'm not sure which one is the best:
1- Calling a function returning a pack would in fact call several functions, one for each pack element (so functions make_pack$0, make_pack$1, etc would be called). The biggest problem with this approach is that the code in the function is called several times... which might not always be clear to the user. I think this approach is also considered in the thread you linked where those issues are raised.
2- Have the language manage those multiple return values by itself. Maybe internally the compiler can translate it to/from a tuple-like struct, with a bit of "magic". But I'm not an expert in this area so couldn't say. It might be a lot more trickier to implement (if possible at all) but would lead to a much cleaner behavior from the user perspective.

  
void g(int a, int b, int c, int d, int e, int f, int g, int h);

// Following functions forward all arguments to g by first multiplying them by 2 and adding 3
void f(int a, int b, int c, int d, int e, int f, int g, int h)
{
 
// Old way
 g
(a*2 + 3, b*2 + 3, c*2 + 3, d*2 + 3, e*2 + 3, f*2 + 3, g*2 + 3, h*2 + 3);
 
// New way
 g
(make_pack(a, b, c, d, e, f, g, h)*2 + 3 ...);

Again, this has the problem of not syntactically identifying what's a pack and what's not. With my syntax:

  g(...tie(a, b, c, d, e, f, g, h)*2 + 3...);

Isn't the make_pack declaration explicit enough that what is returned by it is a pack?

Richard Smith

unread,
Nov 25, 2013, 7:32:03 PM11/25/13
to std-pr...@isocpp.org
On Mon, Nov 25, 2013 at 7:41 AM, Alex B <deva...@gmail.com> wrote:
Hi Richard,

Thanks for you time; you bring a lot of good points. I just took a look at the discussion you linked. I have to say that overloading operator... like you propose is indeed really interesting. It is really neat and doesn't have some of the issues introduced by my proposal. However, if I can manage to solve those issues, I think (from my point of view) that my proposal would provide a bit more power and fill more gaps in the language. See below my answers to the issues you raised.

Usage example:

using... my_types = type_pack<int, char, double>;
using my_tuple = tuple<my_types...>;


This immediately creates a problem. One of the properties of packs today is that it is possible to syntactically determine whether some snippet of C++ syntax contains unexpanded parameter packs, and to determine which packs it contains. This is crucial to some implementation techniques.

It's also important in resolving some grammar ambiguities. Today, if I have:

  template<typename T> void f(typename T::type x ...);

I know this is a vararg function. With your proposal, I could presumably write:

  template<typename...Ts> struct X {
    using ...type = Ts;
  };
  void g() {
    f<X<int, int, int>>(1, 2, 3);
  }

and 'f' would mean something fundamentally different.

First, the ellipsis should go after the type name, not the var name.

Sorry, the example I meant was

  template<typename T> void f(typename T::type ...);
 
So with my proposal, f would be declared like this:

   template<typename T> void f(typename T::type... x);

Secondly, you are right that it remains ambiguous if T::type is a pack or not. I guess a proper solution would be to have the "typename..." keyword (used in the same context/fashion as regular "typename"). The example would become:

   template<typename T> void f(typename... T::type... x);

So first ellipsis (after typename) would indicate that what follows is a pack (of types) and second ellipsis is for pack expansion.

Note that the same problem exists for a pack of values as you point out later (see my answer to this).

I think it'd be unfortunate to need to introduce another disambiguation syntax (in addition to 'template' and 'typename'). Since different elements of a nested-name-specifier may or may not be packs, I think a better place for the ... would be after a ::. So:

  typename T::...U::type // T::U is a pack
  typename T::U::...type // T::U::type is a pack
  T::...value // T::value is a pack
Examples like these are why I'm concerned about introducing first-class packs in a broad set of contexts. (There seems to be very little advantage in this over using std::tuple and the like, with an appropriate 'packification' operator.)

FWIW, there seems to be no problem with introducing packs at block scope (local variable packs in particular), and I suspect that covers most of the use cases.


auto... values12 = values2; // pack of 3 variables of types int, char and double

Presumably this means that your expansion model is that this expands to

  auto values12$0 = values2$0;
  auto values12$1 = values2$1;
  auto vaules12$2 = values2$2;

not the more natural choice of

  auto values12$0 = values2$0, values12$1 = values2$1, vaules12$2 = values2$2;

(which would be ill-formed, because the 'auto' deduces to different types in different deductions).

I didn't think about this. Indeed, as you say, for declaring packs it sounds better to expand in the "less natural" way that you described (but I don't like calling it less natural).

Operations can be done on packs if all the unexpanded packs are of the same size.

values10 = values11 * 2.0;
values11
+= values10;
values11
= values10 + values11;

Again, this doesn't work, due to ambiguity over where the expansion happens. Instead, I'd suggest (and indeed, have been intending to propose for standardization) adding an optional '...' before the ';' in an expression-statement, to represent a pack expansion. So you'd write:

  values10 = values11 * 2.0 ...;

That sounds like a good idea that would work with the current proposal.
I wonder if it would be required also when declaring :

1)  auto... values12 = values2;
vs
2)  auto... values12 = values2 ...;

Unless I'm wrong, (2) is invalid because the first ellipsis is already expanding the pack.

A single ellipsis here seems sufficient. The second one would presumably be ill-formed.

template <class... Args>
void f(Args&&... args);

template <class Tuple>
void g(T&& t)
{
 f
(get<tuple_indices<T>>(forward<T>(t))...);
}
 
This is still a little unclean. My proposal supports this as:

  f(...std::forward<T>(t) ...);

See my "cleaner" example a bit further:
f(get_all(forward<T>(t))...);

I've got to admit that in this case it's not as clean as your solution, but still it's not that bad.
Change the global get_all by a member function and it also gets nicer:
f(forward<T>(t).all()...);

Returning a pack from a function

With pack variables comes the need for functions returning a pack of values.


Why not return a tuple? Packs are not types, so functions returning packs doesn't really seem to make much sense.

In your proposal, isn't the operator...() function returning a pack? It looks like we have the same problem to solve.

operator... names a set of functions produced by pack expansion, and a different function is used by each slice of the pack expansion (this is your option (1) below).
 
So far I can think of two expansion models for returning packs. I'm not sure which one is the best:
1- Calling a function returning a pack would in fact call several functions, one for each pack element (so functions make_pack$0, make_pack$1, etc would be called). The biggest problem with this approach is that the code in the function is called several times... which might not always be clear to the user. I think this approach is also considered in the thread you linked where those issues are raised.
2- Have the language manage those multiple return values by itself. Maybe internally the compiler can translate it to/from a tuple-like struct, with a bit of "magic". But I'm not an expert in this area so couldn't say. It might be a lot more trickier to implement (if possible at all) but would lead to a much cleaner behavior from the user perspective.

This second approach is very problematic, and actually doesn't really make sense from the user's perspective, nor from the mechanical perspective of pack expansion. Consider:

template<typename...T> using ...types = T;
types<int, char, double> f() { // return a pack
  std::cout << "hello, world\n";
  return {0, 0, 0};
}
template<typename...T> void g(T...);
template<typename...T> void h(T ...t) {
  h(t ? f() : 0 ...);
}
void i() { h(true, false, true); }

How many times is f() called here? It seems "obvious" to me that it should get called once per pack expansion, just like any other code within a pack expansion, and the message should be printed out twice. It's not really clear that it'd be possible to support this kind of construct with the "language magic" in approach 2 (where the message would somehow be printed out once?).

With f() returning a tuple, local variable packs, and explicit packification, we get to write three different meaningful programs:

template<typename...T> void h1(T ...t) {
  // call 'f()' once for each element of 't' that is true, and use the corresponding element of f()'s result
  h(t ? ...f() : 0 ...);
}
template<typename...T> void h2(T ...t) {
  // call 'f()' three times, put each slice of 'f' into a local variable...
  auto ...elems = ...f();
  // ... and pass in the elements for which the corresponding element of 't' is true
  h(t ? elems : 0 ...);
}
template<typename...T> void h3(T ...t) {
  // call 'f()' once...
  auto elems = f();
  // ... and pass in the elements for which the corresponding element of 't' is true
  h(t ? ...elems : 0 ...);
}

void g(int a, int b, int c, int d, int e, int f, int g, int h);

// Following functions forward all arguments to g by first multiplying them by 2 and adding 3
void f(int a, int b, int c, int d, int e, int f, int g, int h)
{
 
// Old way
 g
(a*2 + 3, b*2 + 3, c*2 + 3, d*2 + 3, e*2 + 3, f*2 + 3, g*2 + 3, h*2 + 3);
 
// New way
 g
(make_pack(a, b, c, d, e, f, g, h)*2 + 3 ...);

Again, this has the problem of not syntactically identifying what's a pack and what's not. With my syntax:

  g(...tie(a, b, c, d, e, f, g, h)*2 + 3...);

Isn't the make_pack declaration explicit enough that what is returned by it is a pack?

That example would be fine, but if you put the same code inside a template, it would be problematic.

Alex B

unread,
Nov 26, 2013, 10:25:43 AM11/26/13
to std-pr...@isocpp.org
Sorry, the example I meant was

  template<typename T> void f(typename T::type ...);

With the following syntax, there shouldn't be ambiguity:

template <typename T> void f(typename T::...type ...);

(similar to the following, which is not ambiguous:)

template <typename... T> void f(T ...);

 
I think it'd be unfortunate to need to introduce another disambiguation syntax (in addition to 'template' and 'typename').

Well, that's what your proposal do (indirectly) as well. With your proposal, you have to prefix with an ellipsis everywhere. With mine, you only prefix with an ellipsis in ambiguous contexts.

To me, it sounds more consistent with actual syntax for declaring packs:

template <class... T>
void f(T... t) {}

In the function declaration, we don't need to prefix T with another ellipsis to illustrate that it is a pack. In my proposal, it's the same thing if T is an alias (except if it is an alias in a dependant name). In your proposal, you always need to prefix T with an ellipsis if it is an alias but never if it isn't. So to me, there is a tradeoff with both approaches. In some cases your approach is more ellegant, but in some other cases it is mine, like the following:

template <int First, int... Others>
class A
{
constexpr static int first = First;

// Your proposal:
constexpr static auto others = std::make_tuple(Others...);
constexpr static auto all = std::make_tuple(first, ...others...);
constexpr static auto all2 = std::make_tuple(...all * 2 ...);

// My proposal:
constexpr static int... others = Others;
constexpr static int... all = std::make_pack(first, others...);
constexpr static int... all2 = all * 2;

// Or if we do this in one line...

// Your proposal:
constexpr static auto all2 = std::make_tuple(...std::make_tuple(First, Others...) * 2 ...);

// My proposal:
constexpr static int... all2 = std::make_pack(First, Others...) * 2;
};


Since different elements of a nested-name-specifier may or may not be packs, I think a better place for the ... would be after a ::. So:

  typename T::...U::type // T::U is a pack
  typename T::U::...type // T::U::type is a pack
  T::...value // T::value is a pack

That sounds better indeed.

Unfortunately, it gets a little dirtier when dealing with values:

      icd... a = t. ...x;
      icd... b = t. ...y();
      icd... c = T::...z;
      icd... d = T::...w();

That makes 4 dots in a row...


This second approach is very problematic, and actually doesn't really make sense from the user's perspective, nor from the mechanical perspective of pack expansion. Consider:

template<typename...T> using ...types = T;
types<int, char, double> f() { // return a pack
  std::cout << "hello, world\n";
  return {0, 0, 0};
}
template<typename...T> void g(T...);
template<typename...T> void h(T ...t) {
  h(t ? f() : 0 ...);
}
void i() { h(true, false, true); }

How many times is f() called here? It seems "obvious" to me that it should get called once per pack expansion, just like any other code within a pack expansion, and the message should be printed out twice. It's not really clear that it'd be possible to support this kind of construct with the "language magic" in approach 2 (where the message would somehow be printed out once?).

Your example is quite convincing.

 
With f() returning a tuple, local variable packs, and explicit packification, we get to write three different meaningful programs:

template<typename...T> void h1(T ...t) {
  // call 'f()' once for each element of 't' that is true, and use the corresponding element of f()'s result
  h(t ? ...f() : 0 ...);
}
template<typename...T> void h2(T ...t) {
  // call 'f()' three times, put each slice of 'f' into a local variable...
  auto ...elems = ...f();
  // ... and pass in the elements for which the corresponding element of 't' is true
  h(t ? elems : 0 ...);
}
template<typename...T> void h3(T ...t) {
  // call 'f()' once...
  auto elems = f();
  // ... and pass in the elements for which the corresponding element of 't' is true
  h(t ? ...elems : 0 ...);
}

I'm a bit confused... are you mixing both our proposals in these examples? What is the meaning of ... as a  prefix in these examples? (is it the meaning of my proposal or your proposal?)
Also, I'm not sure I understand that third example. What is the type of elems?



I also have a question regarding your proposal. In the last part of my original post, I proposed 2 different ways to expand a template pack alias. It would allow the following (see my original post for more detailed examples):

template <class... T>
using... twice = types<T..., T...>;

using... AB = types<A, B>;
using... ABAB = twice<AB...>; // A, B, A, B
using... AABB = twice<AB>...; // A, A, B, B

Would the last line be possible with your proposal?

template <class... T>
using twice = types<T..., T...>;

using AB = types<A, B>;
using ABAB = twice<...AB...>;
using AABB = ???;

Thiago Macieira

unread,
Nov 26, 2013, 10:55:59 AM11/26/13
to std-pr...@isocpp.org
On terça-feira, 26 de novembro de 2013 10:25:43, Alex B wrote:
> // My proposal:
> constexpr static int... others = Others;
> constexpr static int... all = std::make_pack(first, others...);
> constexpr static int... all2 = all * 2;

What type is int... in memory?

That sounds awfully like an array. You can write:

constexpr static int others[] = { Others... };
constexpr static int all[] = std::make_pack(first, others...);
constexpr static int all2[] = { (Others * 2)... };

Accessing all from all2 requires something that makes an index list so you can
write all[make_index<Others>::value]...

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

Alex B

unread,
Nov 26, 2013, 11:23:15 AM11/26/13
to std-pr...@isocpp.org
Yes, in memory it is similar to an array.

constexpr static int... others = Others;

Translates to
constexpr static int others$0 = Others$0;
constexpr static int others$1 = Others$1;
[...]

Of course a pack of integers can be stored in an array. But then you can't expand the array like you expand a pack.

The goal is to be able to store and reuse (expand) a pack in other contexts. In this example I was only showing how to store a pack but didn't really reuse/expand it in other contexts (except for all2 being defined from all).




--

---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/YkHPCYb-KPQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

Richard Smith

unread,
Nov 26, 2013, 3:58:49 PM11/26/13
to std-pr...@isocpp.org
On Tue, Nov 26, 2013 at 7:25 AM, Alex B <deva...@gmail.com> wrote:
Sorry, the example I meant was

  template<typename T> void f(typename T::type ...);

With the following syntax, there shouldn't be ambiguity:

template <typename T> void f(typename T::...type ...);

(similar to the following, which is not ambiguous:)

template <typename... T> void f(T ...);

 
I think it'd be unfortunate to need to introduce another disambiguation syntax (in addition to 'template' and 'typename').

Well, that's what your proposal do (indirectly) as well. With your proposal, you have to prefix with an ellipsis everywhere. With mine, you only prefix with an ellipsis in ambiguous contexts.

I don't see it that way. In my proposal, x and ...x are distinct things, and both are meaningful and useful. You only use the ... operator when you are converting a non-pack into a pack -- it's *not* performing disambiguation, it's performing a conversion to a different kind of entity.
=( Indeed. I think allowing packs as class members creates too many problems, and probably isn't worth the trouble.
The example assumes that we have both my proposal and local variable packs (which are something which I think we both agree are desirable, and which seem to not have any practical or theoretical problems).
 
Also, I'm not sure I understand that third example. What is the type of elems?

Sorry, I should have included this with that example:

tuple<int, char, double> f() { // return a tuple, not a pack
  std::cout << "hello, world\n";
  return {0, 0, 0};
}
 
I also have a question regarding your proposal. In the last part of my original post, I proposed 2 different ways to expand a template pack alias. It would allow the following (see my original post for more detailed examples):

template <class... T>
using... twice = types<T..., T...>;

using... AB = types<A, B>;
using... ABAB = twice<AB...>; // A, B, A, B
using... AABB = twice<AB>...; // A, A, B, B

Would the last line be possible with your proposal?

template <class... T>
using twice = types<T..., T...>;

using AB = types<A, B>;
using ABAB = twice<...AB...>;
using AABB = ???;

This isn't quite as clean in my proposal:

  using AABB = types<...twice<...AB>... ...>;

or perhaps more clearly:

  template<typename...T> using concat = types<...T ... ...>;
  using AABB = concat<twice<...AB> ...>;


How do you write concat in your proposal? I think you'd need a pack-of-packs as a template parameter:

  template<typename ... ...T> using ...concat = ... ...T ... ...;

right?

Alex B

unread,
Nov 27, 2013, 10:36:10 AM11/27/13
to std-pr...@isocpp.org
The example assumes that we have both my proposal and local variable packs (which are something which I think we both agree are desirable, and which seem to not have any practical or theoretical problems).

Well, to me, variable packs are desirable only if we support them in all contexts (not if only local context). If we can't agree on a way to support them in all contexts, it just sound to me that we didn't yet nailed the right design. Even if it would practically and theoretically work in local contexts, the chosen syntax might get in the way if we (someday) find a reasonable way to add it in other contexts.

 
How do you write concat in your proposal?

I don't see a need for a thing like concat in my proposal. The following alias is enough:

template <class... T> using... types = T;`

using... packA = /*...*/;
using... packB = /*...*/;
using... concatenated = types<packA..., packB...>;
 

I think you'd need a pack-of-packs as a template parameter:

  template<typename ... ...T> using ...concat = ... ...T ... ...;

right?

Uh... no, unexpanded packs as template parameters would cause a bunch of problems. Even more if we are talking about "a pack of unexpanded packs". I'm pretty sure we don't want to go in that direction :)


To sum things up, we seem to agree that:
- Variable packs should not cause any problems when declared in function-local contexts. I guess the same could be said about global/namespace context, right? Ambiguity problems begin to arise for member declarations.
- All the same could probably be said about type pack aliases, right? They have ambiguity problems only when declared as a member.
- Same for functions returning packs.

I also am all for your proposal about adding a packification operator (both for types and values). The only thing that I proposed so far that was in conflict with that is to use an ellipsis as a prefix to disambiguate packs (when needed). Even if we were to only consider my proposal, we saw that this way to disambiguate was sometimes not very nice (resulting in things like 4 dots in a row).

Still, I would really like to find a clean syntax to disambiguate packs (and make sure that disambiguation syntax doesn't conflict with the packification syntax that you propose). That way we could allow widespread support (in all contexts) for variable packs, type pack aliases and functions returning packs.

What about (...) as a way to disambiguate? (not sure in that case if it should be called a keyword or an operator)

icd... a = t.(...)x;
icd... b = t.(...)y();
icd... c = T::(...)z;
icd... d = T::(...)w();

template <typename T> void f(typename T::(...)type ...);

Or maybe [...] or {...} would be better. Or maybe a keyword like "pack" would be better.

Xavi Gratal

unread,
Dec 6, 2013, 7:29:56 AM12/6/13
to std-pr...@isocpp.org
On a related note, why is parameter pack expansion only allowed in the context of a function call?
I have the following code:

const std::string &to_string(const std::string &str) { return str; }

void do_concat(std::string&) {}

template<typename... rest_t>
void do_concat(std::string &str,const std::string &next,const rest_t&... rest) {
    str
+=next;
    do_concat
(str,rest...);
}

template<typename... args_t>
std
::string make_string(const args_t&... args) {
   
using std::to_string;
   
using ::to_string;

    std
::string str;
    do_concat
(str,to_string(args)...);
   
return str;
}

int main()
{
    std
::cout << make_string("aaa ",1," bbb ",1.3) << "\n";
}

Which works fine, but if we allowed parameter pack statement expansion, make_string could be implemented as:

template<typename... args_t>
std
::string make_string(const args_t&... args) {
   
using std::to_string;
   
using ::to_string;

    std
::string str;
   
(str+=to_string(args))...;
   
return str;
}

and do_concat would no longer be necessary. It is almost possible to do it with:

template<typename... args_t> do_nothing(const args_t&... args) {}

template<typename... args_t>
std
::string make_string(const args_t&... args) {
   
using std::to_string;
   
using ::to_string;

    std
::string str;
    do_nothing
((str+=to_string(args))...);
   
return str;
}

except that the result is not the expected one, since the arguments to do_nothing are evaluated in arbitrary order.

Was there any specific reason to disallow expansion outside of a function call? Is there any simpler way to implement make_string?

Alex B

unread,
Dec 6, 2013, 9:44:27 AM12/6/13
to std-pr...@isocpp.org
I guess the reason is to initially make it simple and uniform to expand a pack. Right now, the equivalent of a pack expansion is as simple as separating each elements of the pack by a comma and nothing else. Note that current rules do not only allow to expand in function call but in other contexts as well.

Here is one example:

template <class... Bases>
class Derived : public Bases...
{};

And another one:

template <int... Values>
void f()
{
    int values[] = { Values... };
}

As you can see, all current use cases are expanded by adding a comma between each expanded elements.

One of the things Richard suggested in this thread was adding a new meaning to the unpack operator when it is followed by a semicolon. It would be expanded into several expression statement.

Now about the problem you are trying to solve with the current standard, just convert your 'do_nothing' function to a class instead and call its constructor using list-initialization:

do_nothing{(str += to_string(args))...};

This is guaranteed to be evaluated left to right (note that Richard did talk about something like this in the current thread when he was referring to a class named 'sink'). But of course, it would be nicer if this workaround wasn't needed.

You can even expand over several statements using a lambda:

do_nothing{[&]() {
    str += to_string(args);
    str += separator;
}() ...};

I saw someone in another thread propose expanding packs in a for-range fashion (using a 'for...' syntax):

for... (const auto& a : args) {
    str += to_string(a);
    str += separator;
}

I like this idea a lot, but I'm not sure the one who proposed it will go forward with it.

Something similar can currently be done if we expand all elements into an initializer list:

for (const auto& a : { to_string(args)... }) {
    str += a;
    str += separator;
}

It works in this example but requires that all elements are expanded to a single type. It is not always wanted or even possible to convert all elements to a single type.


--
 
---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/YkHPCYb-KPQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

xavi

unread,
Dec 6, 2013, 10:23:37 AM12/6/13
to std-proposals
Thanks a lot for the explanation!


2013/12/6 Alex B <deva...@gmail.com>

I guess the reason is to initially make it simple and uniform to expand a pack. Right now, the equivalent of a pack expansion is as simple as separating each elements of the pack by a comma and nothing else. Note that current rules do not only allow to expand in function call but in other contexts as well.

Here is one example:

template <class... Bases>
class Derived : public Bases...
{};

And another one:

template <int... Values>
void f()
{
    int values[] = { Values... };
}

As you can see, all current use cases are expanded by adding a comma between each expanded elements.

One of the things Richard suggested in this thread was adding a new meaning to the unpack operator when it is followed by a semicolon. It would be expanded into several expression statement.

It wouldn't even be necessary to change the simple and uniform way to expand a pack you mention. If instead of being expanded into several expression statements, it was expanded into a single comma-separated expression, it would still work, since operands to the comma operator are always evaluated left-to right. It would of course behave differently if the comma operator is overloaded for the result type, but if you overloaded the comma operator, that's probably what you want.
 
Now about the problem you are trying to solve with the current standard, just convert your 'do_nothing' function to a class instead and call its constructor using list-initialization:

do_nothing{(str += to_string(args))...};

That's great... the only drawback of this (apart from being more verbose) is that it doesn't work when the result of the expression is of type void, while the operands of the comma operator can both be of type void.
I guess a workaround would be:

    do_nothing{ (func_returning_void(args), 0)... };

but it's getting a bit loaded on workarounds.
 

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

Xeo

unread,
Dec 6, 2013, 10:42:53 AM12/6/13
to std-pr...@isocpp.org
That is actually the now common idiom to expand packs whereever.

namespace detail{
// array alias, since GCC doesn't correctly support LTR evaluation for structs / classes
using swallow = int[];
} // detail::

// extra 0 at front since 0-sized arrays aren't permitted
// void() in the middle to suppress overloaded comma-operators
// 0 at end to get a uniform type
#define VARIADIC_EXPAND(expr) detail::swallow{ 0, ((expr), void(), 0)... }

And then

VARIADIC_EXPAND( str += to_string(args) );

Alex B

unread,
Dec 9, 2013, 2:26:29 PM12/9/13
to std-pr...@isocpp.org
Hi Richard,

I am thinking a bit more about your proposal and have a few questions.

1.
Consider the following code:
template <class... T>
struct A
{
using data_types = types<T...>;

T operator...() const;

/*...*/
};

template <class AType> // AType is supposed to be a type with the same interface as A
struct B
{
using data_types = types<typename ...AType::data_types...>;

std::tuple<...data_types...> _data;
B(...data_types... d) : _data(d...) {}
B(const AType& a) : B(...a...) {}
};

Q: Is the underlined typename required in your proposal? Or does the ellipsis prefix removes the need for it in that context?
(also please feel free to correct any other mistake I may have made)


2.
(answer to this one might alter answer to first)

Did you think about static operator... ?

template <int... I>
struct int_seq
{
    static decltype(I) operator...() { return I; }
};

using seq = int_seq<0, 1, 2, 3, 4>;
using seq2 = int_seq<...seq * 2 ...>; // 0, 2, 4, 6, 8

If this is allowed, this would make typename in my first example required.


3.
You propose two (three?) packification syntax:
- using ... =
- operator...
- (static operator...)

Would a class be allowed to define more than one of these?


4.
Consider the following code:

struct A { using Y = int; };
struct B { using Y = char; };

struct X {
using ... = ...types<A, B>;
struct Y {
using ... = ...types<float, double>;
};
};

void f(...X::Y...);

The definition of f looks ambiguous to me; it is not clear if the ... prefix applies to X or Y. Is it equivalent to
void f(int, char);
or
void f(float, double);
?

Richard Smith

unread,
Dec 9, 2013, 3:16:29 PM12/9/13
to std-pr...@isocpp.org
On Mon, Dec 9, 2013 at 11:26 AM, Alex B <deva...@gmail.com> wrote:
Hi Richard,

I am thinking a bit more about your proposal and have a few questions.

1.
Consider the following code:
template <class... T>
struct A
{
using data_types = types<T...>;

T operator...() const;

/*...*/
};

template <class AType> // AType is supposed to be a type with the same interface as A
struct B
{
using data_types = types<typename ...AType::data_types...>;

std::tuple<...data_types...> _data;
B(...data_types... d) : _data(d...) {}
B(const AType& a) : B(...a...) {}
};

Q: Is the underlined typename required in your proposal? Or does the ellipsis prefix removes the need for it in that context?
(also please feel free to correct any other mistake I may have made)

First, a caveat: I've not thought about supporting prefix '...' for types as much as I've thought about supporting it for values.

The 'typename' would be required here, to indicate that the complete qualified-id names a type (pack). Consider:

  ...AType::static_tuple_data_member...

I'd previously been expecting the syntax for the above to be:

  ...typename AType::data_types...

for consistency with ... as a prefix operator on values, but you raise an excellent point (implied here and made explicit in your question 4): we could potentially want to turn any element of a nested name specifier into a pack. That being the case, one possible way of writing this would be:

  typename AType::...data_types

However, that introduces an ambiguity in the expression case. Is:

  ...AType::static_tuple_data_member...

packifying AType, or packifying its static data member?

2.
(answer to this one might alter answer to first)

Did you think about static operator... ?

template <int... I>
struct int_seq
{
    static decltype(I) operator...() { return I; }
};

using seq = int_seq<0, 1, 2, 3, 4>;
using seq2 = int_seq<...seq * 2 ...>; // 0, 2, 4, 6, 8

If this is allowed, this would make typename in my first example required.

I've not thought about this. We don't allow this sort of thing for other operators, though, so I don't know whether people would expect this to work here.

  constexpr auto seq = int_seq<0, 1, 2, 3, 4>();
  constexpr auto seq2 = int_seq<...seq * 2 ...>();

... or ...

  using seq = int_seq<0, 1, 2, 3, 4>;
  using seq2 = int_seq<...seq{} * 2 ...>;

... seem nearly-as-good syntaxes for the same thing.

3.
You propose two (three?) packification syntax:
- using ... =
- operator...
- (static operator...)

Would a class be allowed to define more than one of these?

Yes. "using ... = " would specify what happens when prefix ..." is applied to the type, "operator..." would specify what happens when prefix "..." is applied to a value of that type. I don't see a need for a restriction here, since I don't think there is any ambiguity between these two.
 
4.
Consider the following code:

struct A { using Y = int; };
struct B { using Y = char; };

struct X {
using ... = ...types<A, B>;
struct Y {
using ... = ...types<float, double>;
};
};

void f(...X::Y...);

The definition of f looks ambiguous to me; it is not clear if the ... prefix applies to X or Y. Is it equivalent to
void f(int, char);
or
void f(float, double);
?

That's a great question. I was intending for it to mean the latter, but that makes it really hard to express the former. I think your approach of allowing member typedef packs solves this really nicely.

Alex B

unread,
Dec 10, 2013, 2:32:34 PM12/10/13
to std-pr...@isocpp.org
The 'typename' would be required here, to indicate that the complete qualified-id names a type (pack). Consider:

  ...AType::static_tuple_data_member...

I'd previously been expecting the syntax for the above to be:

  ...typename AType::data_types...

for consistency with ... as a prefix operator on values, but you raise an excellent point (implied here and made explicit in your question 4): we could potentially want to turn any element of a nested name specifier into a pack. That being the case, one possible way of writing this would be:

  typename AType::...data_types

However, that introduces an ambiguity in the expression case. Is:

  ...AType::static_tuple_data_member...

Thinking about it, we could add support for parenthesises to disambiguate. Parenthesises can be used when dealing with values, but not with types. We could add also support for the following:
    (... T)::U
which would apply packification on T (and not on T::U which would be the default without parenthesises). So we could have:
1) (...A)::b
2) ...(A::b)  // or simply:
   ...A::b
3) typename (...A)::B
4) typename ...(A::B)  // error
   typename ...A::B    // ok
5) (...a).b
6) ...(a.b)  // or simply:
   ...a.b

Note that I do not propose generalizing the use of optional parenthesises when dealing with types (things could quickly become conflicting with function declarations and function pointers). An opening parenthesis followed by an ellipsis would have a very special meaning. The example in 4) is thus invalid.


  constexpr auto seq = int_seq<0, 1, 2, 3, 4>();
  constexpr auto seq2 = int_seq<...seq * 2 ...>();

... or ...

  using seq = int_seq<0, 1, 2, 3, 4>;
  using seq2 = int_seq<...seq{} * 2 ...>;

... seem nearly-as-good syntaxes for the same thing.

It sounds fair enough to me.
 

Yes. "using ... = " would specify what happens when prefix ..." is applied to the type, "operator..." would specify what happens when prefix "..." is applied to a value of that type. I don't see a need for a restriction here, since I don't think there is any ambiguity between these two.

I think there can be ambiguity. Consider a class A which defines both.

struct A {
    using ... = ...types<int, long>;
    
    using Ret = types<float, double>;
    ...Ret operator...() const { return (...Ret)1; }
};

f(...A{}...);

Are we calling
f(0, 0L);
or
f(1.0f, 1.0);
?

With my suggested parenthesises usage:

f(...A{}...);   // ==> f(1.0f, 1.0);
f((...A){}...); // ==­> f(0, 0L);


 
4.
Consider the following code:

struct A { using Y = int; };
struct B { using Y = char; };

struct X {
using ... = ...types<A, B>;
struct Y {
using ... = ...types<float, double>;
};
};

void f(...X::Y...);

The definition of f looks ambiguous to me; it is not clear if the ... prefix applies to X or Y. Is it equivalent to
void f(int, char);
or
void f(float, double);
?

That's a great question. I was intending for it to mean the latter, but that makes it really hard to express the former.

One (very ugly) way to do it is:

void f(decltype(declval<...X>())::Y...);
 
Another (less ugly) way is:

template <class T>
using identity = T;

void f(identity<...X>::Y...);
 
Reply all
Reply to author
Forward
0 new messages