operator... proposal

522 views
Skip to first unread message

Tomasz Kamiński

unread,
Nov 5, 2012, 3:26:54 PM11/5/12
to std-pr...@isocpp.org
I would like to propose addition for operator... in to set of
over-loadable operators of C++ language.

The basic syntax would be:
template<typename T, typename U>
struct pair
{
/*..*/
T first;
U second;

operator...() { return {first, second}; } // no return type,
// number of elements must be know on compile time.
};

template<typename Ts...>
struct tuple;

template<>
struct tuple
{
operator...() { return {}: }
};

template<typename T, typename Ts...>
struct tuple<T, Ts...> : private tuple<Ts...>
{
/* ... */

operator...() { return { head, tail()...} };
private:
T head;
tuple<Ts...>& tail() { return *this; }
}

This syntax allows tuples to work as augments packs, so the functions
may be invoked as follows. t and t1, t2 are instances of different
classes that defines operator....
f(t...); // possible with boost::fused,
f(1, t1.., 2, t2...); //may be implemented today as invoke(f, 1,
expand(t1), 2, expand(t2)).


The standard library function tuple_cat may be implemented as follows.
template<typename... Tuples>
auto tuple_cat(Tuples&&... tuples) -> decltype(std::make_tuple(tuples...
...))
{
return std::make_tuple(tuples... ...);
}

The operator... should be over-loadable via cv-qualification and
ref-qualifications as any other non-static method.
struct example
{
T u;
T v;

operator...() cv & { return {u, v}; } //returns <T cv &, U cv &>
operator...() cv && { return {u, v}; } //returns <T cv &&, U cv &&>
};
So the expression example e; f(std::move(e)...) will use move
constructor to initialize the augments of function f.

The operator... should be defined for standard library components listed
below:
std::pair, std::tuple, std::array

The implementation for std::array may use following library:
template<std::size_t B, std::size_t E>
struct constant_range
{
constexpr operator...() const { return { B,
constant_range<B+1,E>()...}; } //note the use of constexpr
};

template<std::size_t E>
struct constant_range
{
consexpr operator...() const { return {} };
};

template<typename T, std::size_t N
struct array
{

operator...() cv ref { return {data[const_range<0,N>()]...}; }

T data[N];
};

The following addition to standard library should be also considered:
template<typename T, std::size_t N>
struct static_intializer_list : initializer_list<T>
{
constexpr std::size_t size() const { return N; }
operator...() cv ref { return {begin()[const_range<0,N>()]...} };
};
And compiler should invoke f(static_intailizer_list<N,3>) in case of
invocation f({n1, n2, n3}). This change won't break any existing code
because of explicit conversion of static_intializer_list to intializer_list.

Motivating example:
inline bool and() { return true; }

template<typename T, typename... Ts>
bool and(const T& t, const Ts&... ts)
{
return t && and(ts...);
}


struct A
{
typename /* .. */ T;
constexpr static N = /* .. */;

//This change would allow to write:
A(const std::static_intializer_list<T,N>& list)
: data{list...}
{}

//Instead of
template<typename Ts...>
A(const Ts&... ts, std::enable_if<and(std::is_same<Ts, T>....) &&
sizeof...(Ts) == N>)
: data{ts...}
{}

private:
T data[N];
};

For the consistency the following candidate functions should exist for
every build in array of compile time size:
template<typename T, typename N>
operator...(const T (&)[N]);

template<typename T, typename N>
operator...(T (&)[N]);

template<typename T, typename N>
operator...(const T (&&)[N]);

template<typename T, typename N>
operator...(T (&&)[N]);

Richard Smith

unread,
Nov 5, 2012, 5:41:22 PM11/5/12
to std-pr...@isocpp.org
Interesting, I've been working on a proposal of my own for an
overloaded 'operator...' (I also have a semi-working implementation).
My approach differs from yours in a few ways, which I'll cover below.

On Mon, Nov 5, 2012 at 12:26 PM, Tomasz Kamiński <toma...@gmail.com> wrote:
> I would like to propose addition for operator... in to set of over-loadable
> operators of C++ language.
>
> The basic syntax would be:
> template<typename T, typename U>
> struct pair
> {
> /*..*/
> T first;
> U second;
>
> operator...() { return {first, second}; } // no return type,
> // number of elements must be know on compile time.

In my proposal, the operator has a return type, which must contain an
unexpanded parameter pack. The operator... function declaration is
itself a pack expansion (expanding to a set of functions), so
unexpanded packs can be used in the function definition.

template<typename...Ts>
struct tuple : tuple_elem<...sizeof...(Ts), Ts>... {
Ts operator...() { return get<...sizeof...(Ts)>(*this); }
Ts operator...() const { return get<...sizeof...(Ts)>(*this); }
};

template<typename T, typename U>
struct pair {
T first;
U second;
decltype(...declval<tuple<T,U>>()) operator...() {
return ...tuple<T,U>(first,second);
}
};

Any reference to operator... refers to a pack of functions (the
expansions of the operator...), so (for instance)
my_tuple.operator...() contains an unexpanded parameter pack.

> This syntax allows tuples to work as augments packs, so the functions may be
> invoked as follows. t and t1, t2 are instances of different classes that
> defines operator....
> f(t...); // possible with boost::fused,
> f(1, t1.., 2, t2...); //may be implemented today as invoke(f, 1, expand(t1),
> 2, expand(t2)).

I've experimented and found this to be a bad syntactic approach, for a
few different reasons:

1) Ambiguity. In an expression like f( g(t1, t2) ... ), it's not clear
which of t1 and t2 should be expanded.
2) Support for various implementation approaches. Some approaches for
variadic templates require the compiler to know syntactically which
entities are packs and which are not, and this loses that property.
3) Support for early checking of templates. If a pack expansion
appears in a template, it becomes impossible to check whether it is
actually expanding any packs until the template is instantiated.

In essence, in C++11 the "contains an unexpanded parameter pack"
predicate is a syntactic one, and this approach breaks that property.
Instead, I add a unary prefix operator '...', which is used to
transform an expression into a pack (note that prefix is the
appropriate choice here because the behavior of the ellipsis in
(almost) all existing contexts depends solely on the token(s) which
follow it).

Hence a tuple can be expanded to arguments to a single function:

f(...t1...) -> f(t1a, t1b, t1c)

... or can be converted into a pack for later use:

f( g(...t1) ...) -> f(g(t1a), g(t1b), g(t1c))

... or whatever else you want to do.

f( g(...t1, ...t2...) ...) -> f(g(t1a, t2a, t2b, t2c), g(t1b, t2a,
t2b, t2c), g(t1c, t2a, t2b, t2c))

My implementation allows a prefix ellipsis to be applied to an integer
constant expression, producing a pack {0, 1, ... N-1}. You can see
this in use in the above tuple example.

> The implementation for std::array may use following library:
> template<std::size_t B, std::size_t E>
> struct constant_range
> {
> constexpr operator...() const { return { B, constant_range<B+1,E>()...};
> } //note the use of constexpr
> };
>
> template<std::size_t E>
> struct constant_range
> {
> consexpr operator...() const { return {} };
> };

This is a slow implementation (its compilation time grows
quadratically in the number of elements produced), and is one of the
primary motivators for supporting the ...N syntax: C++11 does not
admit a method of constructing a pack of N elements (for some unknown
compile-time constant N) in linear compile time.

My implementation lives here:

https://github.com/zygoloid/clang/tree/pack-expressions

DeadMG

unread,
Nov 5, 2012, 6:15:46 PM11/5/12
to std-pr...@isocpp.org, toma...@gmail.com
I gotta admit, I like where this thread is going.

toma...@gmail.com

unread,
Nov 5, 2012, 6:45:38 PM11/5/12
to std-pr...@isocpp.org
I really like you idea of threating operator... as unary operator that returns parameter pack.
Maybe you should consider the syntax proposed in http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3416.html for the return type of operator... . So example listed below with look like:
   <T&,U&> operator...();

I doesn't like idea of defining parameter packs by std::tuple and would use brace-intializer-list instead. So for example for std::pair:

struct pair {
  T first;
  U second;
  <T&,U&> operator...() & { return { first, second }; }
  <T const &,U const &> operator...() const & { return { first, second }; }
  <T&&,U&&> operator...() && { return { first, second }; }
  //The 4th options seems to be useless
  //<T const &&,U const &&> operator...() const && { return { first, second }; }
};
This would lead to invocation of only one move constructor in case of expression pair<T,U>(...pair<T,U>()). In your example this will be threaded as pair<T,U>(...tuple<T,U>(pair<T,U>()).

For tuples:
  <Ts&...> operator...() & { return { *static_cast<tuple_elem<...sizeof...(Ts), Ts>*>(this)::value... }; }

This would lead to more generic implementation of get working on any type that has operator... (The std::forward should be added in implementation of arg).
   template<std::size_t N>
   struct arg
   {

      template<typename T, typename... Ts>
      auto operator()(T&& t, Ts&&... ts) -> decltype(arg<N-1>()(ts...)) { return arg<N-1>()(ts...); }
   };

   template<>
   struct arg<0>
   {

      template<typename T, typename... Ts>
      T&& operator()(T&& t, Ts&&... ts)  { return t; }
   };

   template<std::size_t N, typename WithOperatorDotDotDot>
   auto get(WithOperatorDotDotDot&& dotdot) -> delctype(arg<N>()(...dotdot...)) { return arg<N>()(...dotdot...); }

Any reference to operator... refers to a pack of functions (the
expansions of the operator...), so (for instance)
my_tuple.operator...() contains an unexpanded parameter pack.

> This syntax allows tuples to work as augments packs, so the functions may be
> invoked as follows. t and t1, t2 are instances of different classes that
> defines operator....
> f(t...); // possible with boost::fused,
> f(1, t1.., 2, t2...); //may be implemented today as invoke(f, 1, expand(t1),
> 2, expand(t2)).

I've experimented and found this to be a bad syntactic approach, for a
few different reasons:

1) Ambiguity. In an expression like f( g(t1, t2) ... ), it's not clear
which of t1 and t2 should be expanded.
2) Support for various implementation approaches. Some approaches for
variadic templates require the compiler to know syntactically which
entities are packs and which are not, and this loses that property.
3) Support for early checking of templates. If a pack expansion
appears in a template, it becomes impossible to check whether it is
actually expanding any packs until the template is instantiated.
4) It is also more consistent. All operators with one argument (counting explicit object pointer for method) are prefix.


Also the partial forwarding of tuple with be easier in case of using brace-intializer-list. Example: f(std::get<...{3,4,5}>(t)...);
Message has been deleted

toma...@gmail.com

unread,
Nov 5, 2012, 7:07:59 PM11/5/12
to std-pr...@isocpp.org, toma...@gmail.com

For tuples:
  <Ts&...> operator...() & { return { *static_cast<tuple_elem<...sizeof...(Ts), Ts>*>(this)::value... }; }
Actually, this version shoud also work:
     <Ts&...> operator...() & { return *static_cast<tuple_elem<...sizeof...(Ts), Ts>*>(this)::value; }
 
This would lead to more generic implementation of get working on any type that has operator... (The std::forward should be added in implementation of arg).
   template<std::size_t N>
   struct arg
   {
      template<typename T, typename... Ts>
      auto operator()(T&& t, Ts&&... ts) -> decltype(arg<N-1>()(ts...)) { return arg<N-1>()(ts...); }
   };

   template<>
   struct arg<0>
   {
      template<typename T, typename... Ts>
      T&& operator()(T&& t, Ts&&... ts)  { return t; }
   };

   template<std::size_t N, typename WithOperatorDotDotDot>
   auto get(WithOperatorDotDotDot&& dotdot) -> delctype(arg<N>()(...dotdot...)) { return arg<N>()(...dotdot...); }

Of course the get  may be specialized to avoid recursive template instation and reduce compile time in case of yours tuple implementation.

Richard Smith

unread,
Nov 5, 2012, 7:53:04 PM11/5/12
to std-pr...@isocpp.org
I have some concerns about that syntax:
- it's very inventive, and it's not immediately clear whether using
'<'/'>' as bracketing characters in more contexts will create problems
(for compilers, text editors, humans, etc)
- it would presumably need a new rule for splitting '<<' analogous to
the '>>' rule
- it violates the symmetry between declarations and expressions
- with a facility like the one we're discussing, it's providing a
language feature for something which we can provide in a library

Here's another way to get the same effect, with some library-provided sugar:

template<unsigned I, typename ...Ts> struct select_impl;
template<unsigned I, typename T, typename ...Ts> struct select_impl<I,
T, Ts> : select_impl<I-1, Ts> {};
template<typename T, typename ...Ts> struct select_impl<0, T, Ts> {
typedef T type; };
template<unsigned I, typename ...Ts> using select_type = typename
select_impl<I, Ts...>::type;
template<unsigned I, typename ...Ts> select_type<I, Ts...> select(Ts
&&...ts) { /* ... */ }

template<typename T, typename U>
struct pair {
T first;
U second;
select_type<...2, T, U> operator...() const {
return select<...2>(first, second);
}
};

I don't think that's too bad.

> I doesn't like idea of defining parameter packs by std::tuple and would use
> brace-intializer-list instead. So for example for std::pair:
>
> struct pair {
> T first;
> U second;
> <T&,U&> operator...() & { return { first, second }; }
> <T const &,U const &> operator...() const & { return { first, second }; }
> <T&&,U&&> operator...() && { return { first, second }; }
> //The 4th options seems to be useless
> //<T const &&,U const &&> operator...() const && { return { first, second
> }; }
> };
> This would lead to invocation of only one move constructor in case of
> expression pair<T,U>(...pair<T,U>()). In your example this will be threaded
> as pair<T,U>(...tuple<T,U>(pair<T,U>()).

The braced-initialization form doesn't mesh well with my approach of
the operator... being treated as a pack expansion. Your

<T&,U&> operator...() & { return { first, second }; }

would seem to naturally expand to two functions:

T &operator...$0() & { return {first, second}; }
T &operator...$1() & { return {first, second}; }

... which wouldn't do the right thing. To support this, we'd
presumably need some form of literal expression pack syntax, as you
suggest below, if a library-based solution isn't elegant enough. Then
perhaps something like this could work:

<T&,U&> operator...() & { return < first, second >; }

or maybe

...{T&,U&} operator...() & { return ...{ first, second }; }

although micro-optimizing for the elegance of the standard library
implementation doesn't really seem worthwhile.

> Also the partial forwarding of tuple with be easier in case of using
> brace-intializer-list. Example: f(std::get<...{3,4,5}>(t)...);

This particular case could be written as f(std::get<3+...3>(t)...),
but I take your point. Without a literal expression pack syntax, you
could use one of these forms:

f(std::get<std::select<...3>(3, 4, 5)>(t)...);

... or

f(std::get<...std::make_tuple(3, 4, 5)>(t)...);

... or

const int indices[] = {3, 4, 5};
f(std::get<...indices>(t)...);

toma...@gmail.com

unread,
Nov 6, 2012, 2:26:30 PM11/6/12
to std-pr...@isocpp.org
I think we should reconsider declaration of operator... without return type, so it would like:
operator...() { return pack-expression; }
Instead of:
type-pack operator...() { return pack-expression; }
While is still will be interpreted as collection:
auto operator....$0 () -> decltype(pack_expression$0) { return pack_expression$0; }
...
auto operator....$n () -> decltype(pack_expression$n) { return pack_expression$n; }

This would stop discussion about the methods of specifing the return type of oeprator... .

Also the declaration:
tuple<typename... Ts>
{
  Ts operator...() { /* ... */ }
};
is very similar to the normal method declaration, so the user may except that it behaves as normal functions, so the result of function may stored:
Ts t = ...(*this);
or the address of the method may be taken:
Ts (tuple::* method_ptr)() = &tuple::operator...; /* inside of another method */
(this->*method_ptr)()...;
Which shouldn't be considered as part of this proposal. Both of this construct requires specifying return type so omitting it in declaration would suggest user that this is not possible.

Also if we allow to specify parameter pack as return type in operator..., this would suggest that it is also possible to write:
template<typename... Ts>
Ts f() { /* ... */ }
Which is also not in part of the proposla.


template<unsigned I, typename ...Ts> struct select_impl;
template<unsigned I, typename T, typename ...Ts> struct select_impl<I,
T, Ts> : select_impl<I-1, Ts> {};
template<typename T, typename ...Ts> struct select_impl<0, T, Ts> {
typedef T type; };
template<unsigned I, typename ...Ts> using select_type = typename
select_impl<I, Ts...>::type;
template<unsigned I, typename ...Ts> select_type<I, Ts...> select(Ts
&&...ts) { /* ... */ }

template<typename T, typename U>
struct pair {
  T first;
  U second;
  select_type<...2, T, U> operator...() const {
    return select<...2>(first, second);
  }
};

I don't think that's too bad.

Consider the following example:
struct A
{
  operator... ()  { select<..2>(expr1, expr2); }
};
Where the expr1, expr2 are standard expression including creating a temporaries and invoking function. I see no reason for prohibiting this kind of use.

Now consider expression:
A a;
f(...a...);
This will expand to:
f(select<0>(expr1, expr2), select<1>(expr1, expr2));
So the expr1 and expr2 will be evaluated twice. This would lead to many unexpected behaviors as:
  - runtime overhead
  - if expr1 or expr2 has a side effect it would be invoked twice instead of once:
  - if new T[10] will be used as expr1 this would lead to memory leak as only the one of two allocated tables will be forwarded. The pointer from select<1>(new T[10], expr2) will be silently discarded.
  - if expr1 acquire mutex (example unique_lock<mutex>(m)) this would lead to deadlock because the will be two attempt to acquire mutex.

This may be solved by introducing operator... to brace_initializer_list which lead as to  creating parameter pack literal so the operator will look like:
struct A
{
  operator... ()  { ...{expr1, expr2}; }
};
So the invocation f(...a...) will lead to f(expr1, expr2) as expected.

The problem mentonied above will grow if the operator... is used with a tuples, examples:
T1 t1; T2 t2;
operator...() { return select<...(tuple_size<T1>::value + tuple_size<T2>::value)>(some_costly_function(...t1)..., some_costly_function(...t2)...) };
Which will invoke some_costly_function for each argument (tuple_size<T1>::value + tuple_size<T2>::value) times instead of one. While using the parameter pack literal will work as expected:
operator...() { return ...{ some_costly_function(...t1)..., some_costly_function(...t2)... } }


The second solutions is to return to idea of using tuple as the return of operator..., so the example will look like:
operator...() { return make_tuple(expr1, expr2); }
The expression expr1, expr2 will be computed twice, but the addition tuple will be created. This solution has also it flaws. Consider the following example:
struct UseResource;

struct Holder
{
  Holder();
  /* non move or copy constructible */
  Holder(const Holder&) = delete;
  Holder(Holder&&) = delete;

  friend UseResurce;
};

struct UseResource
{
  UseResource(Holder&&) {.. } /* Will accuire resource and leave holder empty */
};

While the following example are pefectly valid:
  std::tuple<Holder> t{};
   UseResource r(std::get<0>(std::move(t)));

The following would generate an error:
  struct A
  {
    //This should move a h member so the expression UseResource r(...A()...) is well defined

     //operator...() && { return std::tuple<Holder>(std::move(h)); } //This will fails at tuple creation, because holder is not move and copy constructilbe
     //operator...() && { return std::tuple<Holder&&>(std::move(h)); } //There cannot be r-value reference memebers, so instation of std::tuple<Holder&&>
     operator...() && { return ...{std::move(h)} } // This one works as excepted.
  private:
    Holder h;
  };
This problem may be resolved but use select implementation instead, but this lead to another problems.


PS. Should we allow not one line implementation of operator...? Example:

struct B
{

operator... ()
{
   some_work();
   if (condition())
      return ...{ 1, 2.0, A() };
   else
      return ...{ 5, double{10}, A() };
};

}
My opinion is that this should be allowed, if all the returns return parameters packs of the same size and the same type of each member (maybe allow promotions and derived to base pointer/ref convertion), similiar to requirments of lambda without explicity specified return type.

The meaning of:
B b;
f(...b...)
Should be then equivalent to
B b;
some_work();
if (condition())
  f( 1, 2.0, A() ); // 1
else
  f( 5, double{10}, A() ); // 2
Note the 1 and 2 would invoke the same function via overload resolution because of requirements of returning pack with members of the same type.
Message has been deleted
Message has been deleted

toma...@gmail.com

unread,
Nov 6, 2012, 3:02:45 PM11/6/12
to std-pr...@isocpp.org, toma...@gmail.com
Also introducing the pack literal with allow us to differentiate between:

Tuple2 u;

struct MyTyple
{
   Tuple t;
   operator...() { return f(...t); } //Will not compute values of f(t$0), f(t$1) and so on
} mt;

So the invocation g(...mt..., ...u) will expand to g(f(t$0), ... , f(t$N), u$0) .... g(f(t$0), ... , f(t$N), u$k), so f(t$i) will be computed k times

And second implementation:
struct MyTuple2
{
   Tuple t;
   operator...() { return ...{ f(...t)... }; } //Will compute values of f$0 = f(t$0), f$1 = f(t$1) and so on before the return
 } mt2;

So the invocation g(...mt2..., ...u) will expand to g(f$0, ... , f$N, u$0) .... g(f$0, ... , f$N, u$k), so f(t$i) will be computed once.

Richard Smith

unread,
Nov 6, 2012, 4:16:58 PM11/6/12
to std-pr...@isocpp.org
On Tue, Nov 6, 2012 at 11:26 AM, <toma...@gmail.com> wrote:
> I think we should reconsider declaration of operator... without return type,
> so it would like:
> operator...() { return pack-expression; }
> Instead of:
> type-pack operator...() { return pack-expression; }
> While is still will be interpreted as collection:
> auto operator....$0 () -> decltype(pack_expression$0) { return
> pack_expression$0; }
> ...
> auto operator....$n () -> decltype(pack_expression$n) { return
> pack_expression$n; }
>
> This would stop discussion about the methods of specifing the return type of
> oeprator... .

It would also mean we couldn't forward-declare operator... and use it
before it's defined (or indeed define it in a separate translation
unit), and would require us to be able to deduce the produced type,
with no way to override (for instance) whether it returns by value or
by reference. Consequently, I'm not convinced that this is the right
direction to follow.

However, since N3386 was approved, it would seem reasonable to allow
the return type to be optionally deduced (and that also side-steps the
problem of writing the return type explicitly):

auto operator...() { return ...whatever; }

> Also the declaration:
> tuple<typename... Ts>
> {
> Ts operator...() { /* ... */ }
> };
> is very similar to the normal method declaration, so the user may except
> that it behaves as normal functions, so the result of function may stored:
> Ts t = ...(*this);

This would be fine if the pack is expanded in some enclosing context:

f( [] {
Ts t = ...(*this);
return t;
} () ...);

And omitting the return type doesn't prevent people from trying this
in other ways:

auto t = ...(*this);
decltype(...(*this)) t = ...(*this);

In any case, the same problem already exists for function parameter packs:

template<typename ...Ts> void f(Ts ...ts) {
Ts t = ts; // error
}

(It would seem reasonable to support that usage, with syntax like...

Ts ...t = ts; // error

... and perhaps this should be part of any proposal to extend
variadics in a future C++ standard).

> or the address of the method may be taken:
> Ts (tuple::* method_ptr)() = &tuple::operator...; /* inside of another
> method */

Again, this is fine, if the pack is expanded in some surrounding
context. Otherwise, we get a diagnostic pointing at the operator...,
and saying that we haven't expanded a parameter pack. That doesn't
seem to me like it would be confusing.

> Which shouldn't be considered as part of this proposal. Both of this
> construct requires specifying return type so omitting it in declaration
> would suggest user that this is not possible.

These cases don't seem to be fundamentally different from other cases
where an expression contains an unexpanded parameter pack.
Yes, you would need to write something like

select<...2>([]{return expr1;}, []{return expr2;})()

to avoid seeing the side-effects multiple times. That's unfortunate. A
pack-literal syntax like your ...{ expr1, expr2 } would indeed nicely
solve this problem, and this seems like a good motivating case for it.

toma...@gmail.com

unread,
Nov 7, 2012, 2:36:56 AM11/7/12
to std-pr...@isocpp.org

Yes, but that will reintroduce the  same problem, as std::make_tuple(expr1, expr2). While thr copy-move construction of object of type expr1 or exp2 must be well formed (even if is eliminated by RVO), so the example of Holder/UseResource won't work in this scenario, while pack literal while work fine in both situations.
 

toma...@gmail.com

unread,
Nov 8, 2012, 4:21:23 PM11/8/12
to std-pr...@isocpp.org


W dniu wtorek, 6 listopada 2012 22:16:59 UTC+1 użytkownik Richard Smith napisał:
On Tue, Nov 6, 2012 at 11:26 AM, <toma...@gmail.com> wrote:
> I think we should reconsider declaration of operator... without return type,
> so it would like:
> operator...() { return pack-expression; }
> Instead of:
> type-pack operator...() { return pack-expression; }
> While is still will be interpreted as collection:
> auto operator....$0 () -> decltype(pack_expression$0) { return
> pack_expression$0; }
> ...
> auto operator....$n () -> decltype(pack_expression$n) { return
> pack_expression$n; }
>
> This would stop discussion about the methods of specifing the return type of
> oeprator... .

It would also mean we couldn't forward-declare operator... and use it
before it's defined (or indeed define it in a separate translation
unit), and would require us to be able to deduce the produced type,
with no way to override (for instance) whether it returns by value or
by reference. Consequently, I'm not convinced that this is the right
direction to follow.



Do you consider providing linkage to operator.. as a function returning multiple values, which would require change of the linkers, or provide linkage to each of operator...$0 functions generated from operator... as normal function. Form the previous post I understand you are considering second solutions, so:

class A
{
   auto operator...() { return ..{1, 2}; }
};

Will be considered as:

class A
{
  auto operator..$0() { return 1; }
  auto operator..$1() { return 2; }
};

And each of them would have separate symbol. This approach may may lead to suprising behaviour if we allow functions diffrent that constrexpr return-only.

Example:
struct  A
{
   auto operator...()
   {
        function();
        if (condition())
             return ...{ 1.0, 2.0 };
         else
             return ...{ 2.0, 3.0 };
   }
};

This will expand to:
struct  A
{
  auto operator...$0()
  {
       function();
        if (condition())
          return 1.0;
       else
          return 2.0;
   }

  auto operator...$1()
  {
       function();
        if (condition())
          return 2.0;
       else
          return 3.0;
   }
};
So for the expression f(...A()...), the function() and the condition() will be invoked twice, which is unexpected, so I think we should allow only one-line return function.

I would like to present you whole new approach for the implementation of operator.... In this proposal I would like to treat operator... as a function template parametrized with one explicit non-type argument of unsigned integer type. So the invocation:
f(...t) would expand to  f(t.operator...<0>(), ...,  t.operator...<N-1>()) [for member] or  f(operator...<0>(t), ...,  operator...<N-1>(t)) [for free standing function], while the each invocation of operator... will be treaded as normal function invocation. The problem remains with providing number N of elements of pack expression. My proposal is to provide it via overloading of operator sizeof...() as std::size_t operator sizeof...() { return N; }. The overloaded operator should be no used in context of expression sizeof...(Type) or sizeof...(object), and sizeof... should still be only applicable for pack-expression.

Examples:

template<typename... Ts>
struct tuple
{
   constexpr std::size_t operator sizeof...() { return sizeof...(Ts); }

   template<std::size_t N>
   typename tuple_element<tuple, N>::type operator... { return get<N>(*this); }
  
   //Or in case of yours implementation
   typename tuple_element<tuple, N>::type operator... { return *static_cast<tuple_elem<N, tuple_element<tuple, N>::type>*>(this)::value; }
};

Notes: sizeof..(tuple<int,int,int>) should be ill-formed, while sizeof...(...tuple<int,int,int>) is well formed as ...tuple is pack-exrpession. This will allow us to avoid dis-ambiguity in situations like:
Let Ts be type pack { Tuple1, Tuple2, Tuple3 }. Our purpose is to call f(sizeof...(Tuple1), sizeof...(Tuple2), sizeof...(Tuple3)) wiht should be f(sizeof...(Ts)...). But this last expression may be considered as f(sizeof...(Ts)...) <=> f(3...) which is ill-formed.

The implementation for class that return values of ...{ expr1, expr2 } with types ...{ T1, T2 };

struct A //non template
{
   std::size_t operator sizeof...() { return 2 };
 
   template<std::size_t N>
   select<N, E1, E2> operator...(); // some ugliness of return type
                                                 // No implementation provided
};

template<>
E1 A::operator...<0>() { return expr1; }

template<>
E2 A::operator...<1>() { return expr2; }

The main advantage is that solutions is that arbitrary complicated function (no return-only) may be provided for each element separately.
This may be problematic for class template, because their members cannot be specialized without the specialization of class, so in this case workarounds similar to this used in implementation of get for pair will be needed.

The implementation for the std::array, build-in arrays and static_initalizer_list will be much simpler in this case:
template<typename T, size_t K>
constexpr std::size_t opertor sizeof...(std::array<T,K> cv ref a) { return K; }

template<std::size_t N, typename T, size_t K>
T cv ref operator...(std::array<T,K> cv ref a)
{
  return a[N];
}  /* no reason to prohibit free standing function */

Also the special syntax for ...<integral> wouldn't be needed as it can be implemented as (notice linear compilation time):
template<std::size_t N>
struct cirange
{
  constexpr operator sizeof...() { return N; };

  template<std::size_t K>
  constexpr operator...() { return K };
}; //
So ...irange<3> will be equivalent to yours ...3. Or more generic:

template<std::size_t B, std::size_t E>
struct cirange
{
  constexpr operator sizeof...() { return E-B; };

  template<std::size_t K>
  constexpr operator...() { return E+K; };
}; //

We can consider adding to proposal built-in candidates:
constexpr operator sizeof...({brace-intializer-list});
template<std::size_t>
auto operator...({brace-intializer-list});
To allow ...{1,2} for consistency, but it will only be syntactic sugar.

toma...@gmail.com

unread,
Nov 8, 2012, 5:24:27 PM11/8/12
to std-pr...@isocpp.org, toma...@gmail.com
The static intializer list wouldn't be needed in case of adoption of constexp size() for initializer list http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3471.html.

W dniu czwartek, 8 listopada 2012 22:21:23 UTC+1 użytkownik toma...@gmail.com napisał:


Daniel Krügler

unread,
Nov 8, 2012, 5:32:24 PM11/8/12
to std-pr...@isocpp.org, toma...@gmail.com
2012/11/8 <toma...@gmail.com>

The static intializer list wouldn't be needed in case of adoption of constexp size() for initializer list http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3471.html.

I don't think this solves the issue, because this is not a static member function, so it characterizes
a specific initializer_list object. This again means that you cannot make a function template
signature dependent on some object value that has not static storage duration.

- Daniel
 
--
 
 
 

toma...@gmail.com

unread,
Nov 26, 2012, 3:39:54 PM11/26/12
to std-pr...@isocpp.org, toma...@gmail.com
Richard, are you still willing to write the proporsal about providing operator... for user definied types? I will surely need some help with wording of the proporsal and also I think it is inaproperiate to use your ideas without your approval.

Firstly, I think the the template function/method approach has one major adavantage: it is not introducing any new language features expect the operator..., while the initial approach required addition of ...N for providing "1,2,...,N-1,N" pacsk expression and special pack-method syntax for declaring the operator itself. I think this may greatly improve chances of adopoting the proporsal into standard.

Secondly, it is cleary suggest that the each member of pack expression is threated as separated function, so if the invocation of operator...$N has side effect, then it will be invoced twice. This is not big advantage, beacause the same result may be achived via allowing only return-only functions.

Of course it has it own flaws. The major problem is the size of generated pack expression. The operator... template should not be instiatiated with paramter argument greater than size of pack expression. This may be avoided via using enable_if hackery. This leads us to the second problem - the initial approach has much more preatier and intuitive syntax than the template approach.

Richard Smith

unread,
Nov 26, 2012, 5:15:46 PM11/26/12
to std-pr...@isocpp.org, toma...@gmail.com
On Mon, Nov 26, 2012 at 12:39 PM, <toma...@gmail.com> wrote:
Richard, are you still willing to write the proporsal about providing operator... for user definied types? I will surely need some help with wording of the proporsal and also I think it is inaproperiate to use your ideas without your approval.

Yes, I am, and I'm also happy to collaborate with you on such a proposal. I'll contact you off-list and we can discuss authoring the paper...
 
Firstly, I think the the template function/method approach has one major adavantage: it is not introducing any new language features expect the operator..., while the initial approach required addition of ...N for providing "1,2,...,N-1,N" pacsk expression and special pack-method syntax for declaring the operator itself. I think this may greatly improve chances of adopoting the proporsal into standard.

Secondly, it is cleary suggest that the each member of pack expression is threated as separated function, so if the invocation of operator...$N has side effect, then it will be invoced twice. This is not big advantage, beacause the same result may be achived via allowing only return-only functions.

Of course it has it own flaws. The major problem is the size of generated pack expression. The operator... template should not be instiatiated with paramter argument greater than size of pack expression. This may be avoided via using enable_if hackery. This leads us to the second problem - the initial approach has much more preatier and intuitive syntax than the template approach.

--
 
 
 

Faisal Vali

unread,
Nov 27, 2012, 11:17:05 AM11/27/12
to std-pr...@isocpp.org, toma...@gmail.com
I apologize for not providing any input earlier on this thread (been
working on a generic lambda implementation using clang in the little
spare time (https://github.com/faisalv/clang-glambda) that I have had)
- but I am certainly interested in this feature (although I need to
seriously sit down and take a closer look at it before I can provide
useful feedback) - so if there is anything I can do to help - please
let me know - and I'll do my best.

thanks for taking this forward!

Faisal Vali
> --
>
>
>
Reply all
Reply to author
Forward
0 new messages