On Feb 17, 2021, at 5:14 PM, Andrew Sutton <asutton.list@gmail.com> wrote:

Here is then what I would write based on the current proposals, probably some mistakes, but let me know if you’re doing something different.

Yes.

But first, one thing I note from actually trying this: given the expansiveness of the proposed syntax — 4 characters or more needed to enclose most entities ([::], <()>, <{}>, <class{ }> etc.) — it now seems a bit ridiculous define reflection via a simple ^.  Splices seem to be more commonly used than reflections (a single reflection often is the source of many splice), and so the few times you use ^ it practically becomes hidden amidst all those [::] etc., negating the goal of making it stand out.  And in any case, reflections are not really as substantially different and thus in need of visually standing out as splices anyway.  So, if using this very expansive syntax everywhere else, I say might as well just use reflexpr(…), or reflect(…), or even some characters-on-either-side analog of [::] as variously suggested by others.  (Cue the outraged objections :)

```
template<typename... Ts>
class tuple {
  consteval {
    unsigned I = 0;
    for (auto Trefl : ^Ts)
      << <class { [:Trefl:] [:idexpr("m", I++):] }>;
  } 

This is close to what generating members might look using injection. Except that P2237 spells it |# str #| and makes allowances for concatenation.

I prefer Barry's approach for declaring data members in this case (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1858r2.html):

template<typename... Ts>
class tuple {
  Ts... elems;
};

I look at this as a special case of what injection could be used for, but it's also a really good fit for this problem. And concise.


  // Option A: using initializer list
  constexpr tuple(const Ts&... args) 
    : eat( ...[: [](){
        meta::info inits = <()>;
unsigned I = 0;
        template for (auto &arg : args) 
          meta::push_back(inits, <( [:idexpr("m", I++):](arg) )> ); 
return inits;
      }() :]... )
  {}

eat() didn't actually do anything in P2237. It was a hypothetical variadic function that accepted arguments. If we couldn't do better than this, I would be deeply disappointed. This should work:

constexpr tuple(T const&... args)
  : ... [: data_members_of(^tuple) :]([:parameters_of(current_function()):])
{ }

Daveed, Wyatt, and I realized we missed splices in member initializer lists, but this seems sane. We're still mulling over the semantics of pack expansions involving both ranges and conventional packs. The phrasing here uses just range splices, so I've left off the postfix expansion (there are no parameter packs in the pattern).

Barry's proposal would just be this:

constexpr tuple(T const&... args)
  : elems(args)...
{ }

I don't know yet if the pack expansion would be adjusted to also need a prefix ... since elems not a conventional parameter pack. Again, this is a nice, concise subset of what we probably do with more verbose injection/splicing code.

 
  // Option B: no initializer list
  constexpr tuple(const Ts&... args) {
    unsigned I = 0;
    template for (auto &arg : args)
      << <{ [:idexpr("m", I++):] = arg; }>;
  }

eat() is actually useful in this context, but only because I get an initializer clause. There's probably a better way to do this.

constexpr tuple(T const&... args) {
  eat(...[: data_members_of(^tuple) :] = [:parameters_of(current_function()):]);
}

A fold expression is tempting but doesn't do the right thing.


  template<typename... Us>
  constexpr tuple(Us&&... args);

  template<typename… Us>
  constexpr tuple(Us&&... args) 
    : eat( ...[: [](){
        meta::info inits = <()>;
unsigned I = 0;
        template for (auto &arg : args) 
          meta::push_back(inits, <( [:idexpr("m", I++):](std::move(arg)) )> ); 
return inits;
      }() :]... )
  {}


Same as above, but with a move.

  : ... [: data_members_of(^tuple) :](std::move([:parameters_of(current_function()):]))
 
I'd worry about generating constraints, but those don't actually involve the data members. We don't have to do anything novel there.

b) tuple_cat

  template< class... Tuples >
  constexpr std::tuple<CTypes...> tuple_cat(Tuples&&... args);


```
template< class... Tuples >
constexpr auto tuple_cat(Tuples&&... tuples) {
  using OutputTuple = typename SomeTypeTraitWhichCatsTheTs<Tuples...>::type;  
  return OutputTuple(eat( ...[: []() {
    meta::info inits = <()>;
    for (auto tuplerefl : ^tuples) {
      for (unsigned J = 0, J < decltype([:tuplerefl:])::size; ++J) {
        meta::push_back(inits, <( std::get<J>( [:tuplerefl:] )> );
      }
    }
    return inits;
  }() :]... );
}

I haven't had an opportunity to think about what this would look like, but Barry has :)

template <typename... Tuples>
constexpr std::tuple<Tuples.[:]... ...> tuple_cat(Tuples&&... tuples) {
    return {std::forward<Tuples>(tuples).[:]... ...};
}


Perhaps the lesson we will learn is that any metaprogramming attempt of this scale is going to have a tendency to produce difficult-to-read code.  

I think you're jumping to conclusions. Metaprogramming code *is* different. It will take adjustments to learn to read and write effectively, but concluding that it must be difficult seems premature to me.

Introducing new language features to make a problem less verbose does not necessarily make it less difficult; it also increases the steepness of the learning curve which new users must ascend, and the likelihood they will forego ascending it.  

The tuple_cat example is indeed directly from Mr. Revzin's paper (P1858R1), and seems purpose built to solve this particular problem.  But what about the next problem?   Serious complexity creep seems to be happening here, and it is not clear where it will end, given that, as Roland pointed out, the examples so far have been toy examples, "wooden," not realistic use cases that push the features to the limits.  When the first complicated test case presented must be solved with new, purpose-built language features…to me, that smells.

What is more, even just considering all the features already introduced, I suspect you will have difficulty convincing users to pore through massive papers to learn these features, much less to learn which capabilities are not yet properly implemented despite appearances (e.g. my assumption that "eat" was intended to solve the initializer list issue).

This is why, in the back of everyone’s mind, should be the proposal to append to Andrew’s features some kind of fallback "glorified preprocessor" approach previously discussed — essentially just a mechanism of parsing string literals and spliced identifiers into template instantiations.  It always works, doesn’t need fragments or complex pack expansion mechanisms etc.; any fool can understand it in 5 minutes, and implement a tuple in an hour.  

Sure that tuple might be less readable (though that is plainly arguable at this point), and sure if they screw it up, the diagnostics might suck, the same way they suck when debugging a bad macro.  But at a minimum it can serve as a starting point for learning Andrew's "correct" way to do things, the same way a beginner to C++ might begin writing easy-to-understand preprocessor macros before they realize the difficulties introduced and are thereby motivated to learn templates.  

Beyond making the learning curve shallower, it would also serve as a fallback mechanism, should there be further cases where Andrew and his guys missed something, to allow the user to just get the job done, to get the metafunction written, however messily, pending a semantically "correct" solution.

I won’t raise the matter again, and we needn’t discuss it further now, as there is plenty to discuss in Andrew et all papers, just please keep this option in the back of your minds going forward.