C++ Logo

SG7

Advanced search

Subject: Re: Metaprogramming
From: David Rector (davrec_at_[hidden])
Date: 2020-10-29 17:25:01


Thanks for this detail. There is still much I have to learn about this functionality. It has been very carefully considered.

To play devil's advocate one last time:

Herb raised the hypothetical of how templates could have been implemented.

Here's another hypothetical (with the caveat that I have little knowledge of the actual history, this is mostly rhetorical): what if there had been no C? What if C++ had come first? Template semantics offered right away, without any motivating context which suggested the ultimate inadequacies of preprocessor solutions? Would the language have gained the popularity that it has?

The upshot: it may (or may not) be that common users do not *initially* fully appreciate the power offered by a fully semantically-structured approach to metaprogramming. Some might initially find it too confusing, and question why a simple string based approach is not enough.

To hook these users, it might be necessary to introduce the basic capabilities via a broader string injection approach -- *or* it may be sufficient to just heavily motivate talks/tutorials/papers about injection syntax with an initial discussion of the problems with D string mixins (mentioned by Herb, though I heard the same rumors): what precisely goes wrong for users when they take this tantalizingly-simple approach, and why is it thus beneficial in the long run to learn this more structured syntax, and -- this would be the ultimate -- why your structured injection syntax can do everything everything the string injection syntax can. (I briefly perused that clang intermediate-file-generating source file I referenced earlier, https://github.com/llvm/llvm-project/blob/master/clang/utils/TableGen/ClangAttrEmitter.cpp <https://github.com/llvm/llvm-project/blob/master/clang/utils/TableGen/ClangAttrEmitter.cpp>, to see if I could find an example only full string injection could do. I could not -- so maybe it really is provable that the only place you need to inject strings is as identifiers -- that would be a great paper.)

Thanks again Andrew, Wyatt et al for your hard work and thought on this,

Dave

> On Oct 29, 2020, at 2:37 PM, Andrew Sutton <asutton.list_at_[hidden]> wrote:
>
> 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 :)
>
>



SG7 list run by sg7-owner@lists.isocpp.org

Older Archives on Google Groups