C++ Logo

sg7

Advanced search

Re: [SG7] Metaprogramming

From: Andrew Sutton <asutton.list_at_[hidden]>
Date: Thu, 29 Oct 2020 14:37:31 -0400
>
> On further reflection, Andrew’s current syntax stands out, is easy to get
> used to after the initial shock, and — most importantly to me — leaves
> plenty of room for further expansion as is: you can just expand the
> contexts in which |# #| may be considered semantically valid, as in fact I
> originally thought it allowed for.
>

This is the way :)

I'm still not comfortable expanding that particular operator to more
contexts, but we'll see how things shake out moving forward.


(Small point though: I have carefully not considered why the unquote
> operator %{ } is needed, and was a little confused by the various usages —
> why not always assume any generated string therein is unquoted when
> parsed? Something for Andrew to explain/you all to discuss in your
> meeting.)
>

%{x} is somewhere between an inline lambda capture and a string
interpolation operator. Within a fragment (and only within a fragment) it
denotes a placeholder for a value to supplied during evaluation.
Specifically, it's a placeholder for whatever value x has at the point the
fragment is evaluated.

consteval {
  int x = 0;
  -> <namespace { int n1 = %{x}; }>; // generates int n1 = 0;
  ++x;
  -> <namespace { int n2 = %{x}; }>; // generates int n2 = 1;
}

It's entirely orthogonal to splicing. Its purpose is to turn values during
constant expression evaluation into constants in injected code. It just
happens to work particularly well with splicing.

Here's some code I showed in a talk that generates a sequence of
initializers for a static array:

template<enumeral T>
consteval std::vector<meta::info> values_of() {
  std::vector<meta::info> result;
  for (meta::info e : meta::members_of(reify(T))) {
    auto frag = <( {|%{e}|, %{meta::name_of(e)}} )>;
    result.push_back(frag);
  }
  return result;
}

// elsewhere with E being an enum type:
static std::unordered_map<E, char const*> map = { |values_of<E>()|... };

Adding an operator is not the only way to do this. We can easily identify
names of local variables inside of fragments and just "make it work". In
fact, that's how fragments worked from 2016 until '19 or so when Wyatt and
I decided to change the approach. Here's why:

- Our experience was that non-trivial fragments become really hard to read
when they contain a mix of fragment-declared names, globally visible names,
and names of local variables. It was also hard to figure out what was being
injected. I don't remember getting negative feedback on the design, but
there were definitely examples where Wyatt and I went back and forth trying
to figure out what they should mean.

- Implicit "captures" make easy to shadow locals, meaning you have to be
careful about the naming of declarations in your fragment---or your locals.
Explicitly escaping turns out to be a really clean and obvious way of
addressing that problem (as opposed to lambda-like captures). That was just
weirdly limiting.

- If we can't interpolate expressions (e.g., %{meta::name_of(e)}), it
forces us to declare local variables with those results and then use those
names, limiting expressivity. It's probably worth noting that lambda
captures allow a similar feature: [s = name_of(x)]... But fragments only
ever "capture" values, so we don't need quite the same level of control
over references and copies.

The other approach we considered was to explicitly declare these captures
just like a lambda expression. In fact, that's how the implementation
worked before 2016 and Herb suggested they should be implicit :)

Received on 2020-10-29 13:37:44