C++ Logo

sg7

Advanced search

Re: [SG7] From TMP to value-based reflection

From: David Rector <davrec_at_[hidden]>
Date: Thu, 9 Dec 2021 17:07:33 -0500
> On Dec 9, 2021, at 1:20 PM, Matus Chochlik via SG7 <sg7_at_[hidden]> wrote:
>
> On Thu, Dec 9, 2021 at 10:43 AM Jean-Baptiste Vallon Hoarau via SG7 <sg7_at_[hidden] <mailto:sg7_at_[hidden]>> wrote:
> Besides the thing that Peter mentioned in another response, something like this didn't work either when I played with the meta::info implementation:
> ```
> auto unreflect_type(meta::info mo) {
> return std::type_identity<typename [: mo :]>{};
> }
> ```
> i.e the return type cannot depend on a non-template meta::info value. You basically have to create a separate consteval function that creates and returns the metaobject. This limits how the metaobjects are passed around in a composition of metaprogramming algorithms. You probably can work-around this by making all the metaprogramming algorithms templates, but is sort-of defeats the thing where all metaobjects are of a single type (meta::info) and In the end it's almost the same as what the "mirror" header does.
>
> Yes, but this is by design. meta::info objects as function parameters belongs to (just like other functions parameters) the evaluation-time world, not instantiation-time. Indeed metaprogramming with the meta::info proposal implies that, if the output of your metaprogramming algorithm depends on the type of your reflection, then it must return something which must then be injected in the context where that value is an instantiation-time construct.
>
> So IIUC the difference that we're talking about is the following:
> ```
> // type-based metaobjects ---------------------------
>
> template <typename T>
> string_view classify(type_identity<T>) { return "a type"; }
> template <typename T>
> string_view classify(type_identity<T*>) { return "a pointer type"; }
> template <typename T>
> string_view classify(type_identity<unique_ptr<T>>) { return "a smart pointer"; }
>
> auto transform(auto mo) {
> return do_(something(complicated(with(mo))));
> }
>
> auto get_type_id(auto mo) {
> return type_identity<do_t<something_t<here_t<unreflect(transform(mo))>>>>{};
> }
>
> string_view my_algo(auto mo) {
> return classify(get_type_id(mo));
> }
>
> // meta::info metaobjects ------------------------
>
> template <typename T>
> consteval string_view classify(type_identity<T>) { return "a type"; }
> // ...
>
> consteval meta::info transform(meta::info mo) {
> return do_(something(complicated(with(mo))));
> }
>
> consteval string_view my_algo(meta::info mo) {
> return classify(type_identity<do_t<something_t<here_t<[: mo :]>>>>{});
> }
> ```
> A function like the `get_type_id` above cannot be implemented in pure consteval. This is IMO a pretty big limitation if the thing that is done in `get_type_id` is really complex and it is used in many places.
> (Actually I'm not even sure if the `my_algo` function could be implemented without being a template, because it depends on the consteval argument. I'll have to try the meta::info implementation).
>
>
> This does not defeat the purpose of operating over uniformly typed values. For example, imagine a meta-function which generates the code needed to serialize any value. With the meta::info proposal, the whole meta-function never needs to be instantiated, simply evaluated, and then the returned fragment is injected (a task essentially equivalent to instantiation) into the caller's context.
> I've heard this reasoning before and I'm still not sure what advantages does this bring over template function instantiation, since as you say (evaluation + injection = almost like instantiation).

Injection does not intrinsically require instantiation; injections are instantiated and evaluated only when an enclosing template is instantiated like anything else. One can use injection statements in ordinary control flow statements, since all injectable entities will have the same type (whether strings or meta::infos or a fragment type specific to that injection context). Whereas, with instantiated reflections some facility like TMP or `template for` is required to iterate.

That said, while I very much support getting away from template instantiation-based reflection in the long run, I think the fact that meta::info vs. template instantiations is an *interface/language* issue, rather than an *implementation* question, suggests a more general issue the language should address: need templates & concepts really be syntactically distinct from types, anywhere in the language?

Consider my old clang-based reflection implementation, which reflects the clang interface, only as template instantiations a la Matus’s approach, but object-oriented. That is to say: a class, represented as a `CXXRecordDecl` in clang, would be reflected to the user as a `CXXRecordDecl<uintptr_t>`.

For reference here is the (very large) library header providing the interface: https://raw.githubusercontent.com/drec357/clang-meta/master/llvm/tools/clang/tools/reflection-src-generator/generated_files/client_reflection_impl.hpp <https://raw.githubusercontent.com/drec357/clang-meta/master/llvm/tools/clang/tools/reflection-src-generator/generated_files/client_reflection_impl.hpp>.

By defining the right templated function overloads, one can almost achieve the same syntax as one would use in clang; e.g.

```
  template for (auto D : reflexpr(MyClassTemplate)->getTemplatedDecl()->decls()) {
    if constexpr (D->isImplicit())
      continue;
    if constexpr (auto_ FD = dyn_cast<FieldDecl>(D)) {
      auto QT = FD->getType();
      if (QT->isDependentType())
 meta::debug("Field named ", FD->getQualifiedNameAsString(), " has a dependent type");
    } else {
      D->dump(); //displays at compile-time
      meta::error( D->getBeginLoc()
          , "Unhandled Decl kind; see dump"
          , user::FixItHint::CreateRemoval(user::SourceRange(D->getBeginLoc(), D->getEndLoc()));
          );
    }
  }
```

What are the *syntax* differences between this and the ordinary clang interface? How could they be minimized?

1. `template for` instead of `for`
2. the ubiquitous need for auto types

1 can be handled simply; I don’t see why we couldn't get rid of `template for` syntax, instead using `for`, and simply ask that the compiler perform compile-time expansion when the type of the range variable is tuple-like.

As for 2, suppose one could use templates and concepts just like types, a la:
```
namespace meta::clang {

template<uintptr_t metaobjptr>
class DeclContext {...};

template<uintptr_t metaobjptr>
class CXXRecordDecl {...};

// CXXRecordDecl is a templates so is treated like `auto`.
template<typename T>
CXXRecordDecl *getClassReflection() { return reflexpr(T); }

// DeclContext and CXXRecordDecl are templates so are treated like `auto`;
// A function template will be implicitly generated from this code.
DeclContext *getParentOf(CXXRecordDecl *D) { return D->getDeclContext(); }
}
```
(I am not up to date on the latest thinking re: concepts etc, and surely this has been discussed at some point and so I would appreciate any links to papers that bear on this.)

If the language allowed for both of these changes, I believe there would be no need to specify how the compiler reflects information. I.e. a compiler could use a meta::info type, or an implicit meta::info<intptr_t> template *which is always referenced as meta::info* — i.e. essentially the same way Matus reflects information. (Indeed perhaps the template-ness of an entity could be private information, to an extent.)

Dave










Received on 2021-12-09 16:07:38