Selective Compile-Time Reflection for C++

Introspective

Some quotes from StackOverflow regarding reflection in C++:

"Inspection by iterating over members of a type, enumerating its methods and so on. This is not possible with C++."

"You can't iterate over the member functions of a class for example."

You can. Sort of.

Well, there's obviously more to it than that.

Introspective is a header file that makes good use of template capabilities and allows new classes to selectively open up some or all of their members to reflection, regardless of whether the inspected member is a constant, a static variable or a instance member function. It records their (function) types and addresses and passes them along unchanged during compile-time, with the ultimate goal of making the interaction with embedded scripting languages like Lua a little less of a hassle.

Compile-time reflection

Let's take a tour.

#include <string>
#include <introspective.h>

using namespace introspective;

struct Reflective: Introspective<Reflective>
{
    // Declaring and defining functions with the supplied macros might seem
    // a little odd at first.
    FnDecl(add, (int x, int y) -> int) { return x + y; }
    
    // It does not look a lot like C++, I agree.
    MemDecl(static constexpr Pie, double) = 3.14;
    
    // What the macro needs is the name of the declaration and its (function) type, nothing else.
    FnDecl(sub, (double x, double y) -> double) { return x - y; }
    
    // Declare it, but define it somewhere else. It can wait.
    FnDecl(virtual div, (double x, double y) -> double);
    
    // We might record a instance variable just as easily.
    MemDecl(strung, std::string);
    
    // Say we had a object variable that we do not want recorded.
    // Just leave out the macro then.
    double value;
    
    // Instance member functions are just another declaration in the
    // eyes of reflection.
    FnDecl(virtual mul, (double y) -> double) { return value * y; }
    
    // Overloads? No problem.
    FnDecl(mul, (int y) -> double) { return 2 * value * y; }
};

// The definition of the function already declared requires
// no reflection magic; the reflection is already behind us.
double Reflective::div(double x, double y) { return x / y; }

Although it tries to offload the burden of reflection to template metaprogramming as much as possible, the fact remains that templates are not known for being concise. That is why Introspective employs macros to dress the reflected members like some version of C++ one could reason with just by looking at it, while staying faithful to C++ idioms.

How bad can the interface to this be, one might wonder.

#include <iostream>

int main()
{
    // Get the address of the first member that has been indexed
    // in the definition of the Reflective struct. It is a
    // static function taking two integers.
    // No casting of any kind necessary.
    int (* addAddress)(int, int) = Reflective::GetMemberByIndex<0>().Stencilled();
    
    // The local variable above might have been annotated with 'auto' as well,
    // the deduced type would have still been the same!

    // Guess what 9 + 8 is.
    std::cout << addAddress(9, 8) << std::endl;

    // Just underneath the definition of the add function there is pie.
    const double* pieAddress = Reflective::GetMemberByIndex<1>().Stencilled();

    // Get the address of div, the function split into declaration and definition.
    double (* divAddress)(double, double) = Reflective::GetMemberByIndex<3>().Stencilled();
    std::cout << divAddress(1.0, *pieAddress) << std::endl;
}

Handles object member functions and variables just as well.

int main()
{
    // Aggregate initialization rules are respected, if applicable.
    Reflective t{ .value = 2.71, .strung = "Lorem ipsum" };

    std::string Reflective::* strungAddress =
        t.GetMemberByIndex<Reflective::GetReflectiveMemberCount() - 1>()
         .Stencilled();
    double (Reflective::* mulAddress)(double) = t.GetMemberByIndex<4>().Stencilled();

    std::cout << (t.*mulAddress)(Reflective::Pie) << std::endl;
    std::cout << t.*strungAddress << std::endl;
}

Gets members even by name, although admittedly with a little clunkier syntax. If someone has a way to make this easier without macros, please feel free to contribute!

int main()
{
    using namespace introspective;

    // String literals may not under any circumstances be used as arguments
    // to templates, directly or indirectly. One can get around this by
    // storing a constexpr static character array somewhere else.
    constexpr static char queryName[] = "sub";

    auto subRef = Reflective::GetMemberByName<Compiled<Intern(queryName)>>()
                             .Stencilled();
    std::cout << subRef(5, 8) << std::endl;
}

Reflective template members? You got it. Even C++20-ready!

#include <concepts>

struct Templatte: Introspective<Templatte>
{
    // Mark non-type template parameters always with 'auto'.
    // You can check the type of the parameter in the 'requires'-clause without worries.
    FnDecl(static Lattemp, template(auto x, auto y, auto default(5) z), (double a) -> double)
    {
        return x - y + z * a;
    }
    
    FnDecl(static LatteMacchiate, template(typename default(int) A), requires(std::integral<A>), (A a) -> decltype(12 * a))
    {
        return 12 * a;
    }
};

int main()
{
    // Observe where the template arguments went.
    double (* generic)(double) = Templatte::GetMemberByIndex<0>()
                                           .Stencilled<5, 6, 7>();
    std::cout << generic(3.14) << std::endl;
}

Interaction with scripting languages

Having the ability to let the compiler generate a list of selected functions along with their names and signatures gives us the opportunity to "wrap" every function in that list inside another new function with some specific signature. One particularly popular signature in the realm of embedded scripting is typedef int (* lua_CFunction)(lua_State* L). The Lua virtual machine encapsulated in L is perfectly capable of indirectly providing the functions in the compile-time list with some arguments of their own; the only thing missing is a bridge that marshalls the necessary data in each direction.

Let MarshallSig be the function pointer signature type to wrap each function in; in the case of a marshalling bridge to Lua, it would be the function pointer type int(*)(lua_State*), aka lua_CFunction. Let also MarshallArgs... be the list of parameter types in MarshallSig; it may contain more than one type (or even zero, but that would make marshalling pointless). In the example above, MarshallArgs... would contain the pointer type lua_State* as its only element.

Automatic conversion of functions to functions with signature MarshallSig is done using static member functions of the template type instance introspective::ArgsMarshalling<MarshallSig>. This template is meant to be specialised for your MarshallSig and any specialisation needs to provide following function template definitions:

  • template <bool isStaticCall, typename Data> static auto FromEmbedded(MarshallArgs..., std::size_t where). Extracts one value of type Data through the facilities exposed in MarshallArgs..., and returns that value. Whether you return a Data value by copy, by reference or const-qualified is your choice; the only thing this function template needs to satisfy are the needs of the wrapped functions. The isStaticCall flag indicates whether the embedded script is trying to call a static function or an instance function (some scripting languages make an explicit difference between those two).

This function is always invoked when the scripting language wants to make a call to the wrapped function. where tells the position of the argument it needs to be in for the call to the wrapped function to make any sense - if the wrapped function requires a double as its first argument and a std::string as its second, then FromEmbedded will be asked to extract a double with where = 0 and a std::string with where = 1.

  • template <typename Data> static «Return Type» ToEmbedded(MarshallArgs..., Data data). Marshalls data back to a representation that the MarshallArgs... facilities can understand again. Called when the wrapped function returns a value. That value will be provided with data.

Wrapped functions returning void cause the marshalling bridge to not call ToEmbedded, since there is no data to marshall back. «Return Type» needs to be the same type as the return type in MarshallSig.

  • static «Return Type» ToEmbedded(MarshallArgs...). Same as the other overload of ToEmbedded, except that this overload is called when the wrapped function returns void.

  • template <bool isStaticCall, typename... DataArgTypes> static bool PrepareExtraction(MarshallArgs...). Called to inform the MarshallArgs... to prepare for extraction of DataArgTypes... in that specific order. Returns a bool indicating the readiness and the ability to extract these arguments. This function exists to enable restrictions on types that may be marshalled and to make type checking on the incoming arguments possible.

  • static «Return Type» FailExtracted(MarshallArgs...). Called when PrepareExtraction returns false. As above, «Return Type» needs to be the same type as the return type in MarshallSig. The value returned from this function will be the value returned from the wrapper function.

Once such a specialisation has been written, all that's left to do to get the desired functions is

// The returned value is a std::array, and its length depends on the number of members declared with
// the Introspective macros.
constexpr auto scriptReadyFnArray = introspective::MarshalledFns<MarshallSig>(«Introspective Type»::GetMembers());

That array will contain introspective::FnBrief<MarshallSig> elements, where the first element in such a pair is the name of the wrapped function and the second element is a pointer to a function with signature MarshallSig which automatically converts arguments that are provided inside the embedded scripting language to C++ arguments and feeds them to the wrapped function in the correct order, using the five functions described above.

Take a look at the examples for more details.

Additional member detection in classes

As a bonus, the header also provides some short macros for detecting a specific member in an unspecified generic class. I'll mention them here briefly.

  • HasMember(InType, member, ...) -> bool. Indicates true if member member can be found in type InType, regardless of whether the member is a function or a variable. Actually returns either std::true_type or std::false_type, but these are implicitly convertible to bool in any context. The varargs must be filled with matching parameter types if the subject of the search is a function with specific parameters.
  • GetStaticMember(InType, member, MemberType) -> MemberType*. Returns a pointer to the static variable InType::member, if one such exists, otherwise nullptr. MemberType may also be a function type, as in int(std::string, double) without pointer notation. Actually returns either a valid value of MemberType* or a std::nullptr_t, depending on the existence of the member.
  • GetStaticConstant(InType, member, MemberType) -> const MemberType*. Same as GetStaticMember, but enforces const-ness of the member. If the member is not const-qualified, returns nullptr.
  • GetObjectMember(InType, member, MemberType) -> MemberType InType::*. Same as the static version, but returns a pointer-to-member. Does not support object member functions at the moment.

These macros may be used inside a function and may be treated as such, they only hide two lines of template boilerplate code. As a consequence of template metaprogramming, these macros also support template type parameters as their argument.

Requirements

Fairly thin; the header file only depends on the standard library. However, it is written for C++20 and uses some features that have been introduced with that or the previous revision:

  • __VA_OPT__
  • Structural types as non-type template parameters
  • Lambda literals in unevaluated contexts
  • Default-constructible lambda types where their closure is equal to itself.
  • consteval for making sure none of the reflection algorithms spill over into the runtime.
  • Fold expressions for variadic template arguments (might have been already introduced with C++17, mentioned for the sake of completeness)

This header has been tested with recent versions of g++-11 and clang++ 13.0.0; other compilers may or may not work. Note that clang 13.0.0 has not been released yet, this necessitates building clang 13.0.0 yourself from source. Observe that current release versions of clang 12.0.x can't compile this header, as they lack support for some C++20 constructs used here.

Until C++ implements some real universal reflection, this header ought to do it for the time being.

Any feedback or contribution is greatly appreciated!

Similar Resources

a compile-time, header-only, dimensional analysis and unit conversion library built on c++14 with no dependencies.

UNITS A compile-time, header-only, dimensional analysis library built on c++14 with no dependencies. Get in touch If you are using units.h in producti

Sep 19, 2022

Compile-time String to Byte Array

STB Compile-time String to Byte Array. Why? You may ask, why'd you want to do this? Well, this is a common issue in the cheat development scene, where

May 7, 2022

obfuscated any constant encryption in compile time on any platform

obfuscated any constant encryption in compile time on any platform

oxorany 带有混淆的编译时任意常量加密 English 介绍 我们综合了开源项目ollvm、xorstr一些实现思路,以及c++14标准中新加入的constexpr关键字和一些模板的知识,完成了编译时的任意常量的混淆(可选)和加密功能。

Sep 19, 2022

Header-only compile time key-value map written in C++20.

C++ Static Map Header-only compile time key-value map written in C++20. Getting Started Simply add the files in your source and #include "@dir/Static_

Oct 19, 2021

lightweight, compile-time and rust-like wrapper around the primitive numerical c++ data types

prim_wrapper header-only, fast, compile-time, rust-like wrapper around the primitive numerical c++ data types dependencies gcem - provides math functi

Oct 22, 2021

compile time symbolic differentiation via C++ template expressions

SEMT - Compile-time symbolic differentiation via C++ templates The SEMT library provides an easy way to define arbitrary functions and obtain their de

Apr 8, 2022

Modern C++ 20 compile time OpenAPI parser and code generator implementation

OpenApi++ : openapipp This is a proof of concept, currently under active work to become the best OpenAPI implementation for C++. It allows compile tim

Aug 20, 2022

Instant compile time C++ 11 metaprogramming library

Brigand Meta-programming library Introduction Brigand is a light-weight, fully functional, instant-compile time C++ 11 meta-programming library. Every

Sep 22, 2022

Ctpg - Compile Time Parser Generator

Ctpg - Compile Time Parser Generator is a C++ single header library which takes a language description as a C++ code and turns it into a LR1 table parser with a deterministic finite automaton lexical analyzer, all in compile time.

Sep 13, 2022

DimensionalAnalysis - A compact C++ header-only library providing compile-time dimensional analysis and unit awareness

Dimwits ...or DIMensional analysis With unITS is a C++14 library for compile-time dimensional analysis and unit awareness. Minimal Example #include i

Jul 8, 2022

Type safe - Zero overhead utilities for preventing bugs at compile time

type_safe type_safe provides zero overhead abstractions that use the C++ type system to prevent bugs. Zero overhead abstractions here and in following

Sep 20, 2022

Pipet - c++ library for building lightweight processing pipeline at compile-time for string obfuscation, aes ciphering or whatever you want

Pipet Pipet is a lightweight c++17 headers-only library than can be used to build simple processing pipelines at compile time. Features Compile-time p

Jul 30, 2022

A simple framework for compile-time benchmarks

Metabench A simple framework for compile-time microbenchmarks Overview Metabench is a single, self-contained CMake module making it easy to create com

Sep 15, 2022

Compile-time C Compiler implemented as C++14 constant expressions

constexpr-8cc: Compile-time C Compiler constexpr-8cc is a compile-time C compiler implemented as C++14 constant expressions. This enables you to compi

Sep 9, 2022

Set of tests to benchmark the compile time of c++ constructs

CompileTimer Set of tests to benchmark the compile time of c++ constructs This project is an attempt to understand what c++ construct take how much ti

Sep 21, 2019

[WIP] Experimental C++14 multithreaded compile-time entity-component-system library.

ecst Experimental & work-in-progress C++14 multithreaded compile-time Entity-Component-System header-only library. Overview Successful development of

Aug 27, 2022

Entity-Component-System (ECS) with a focus on ease-of-use, runtime extensibility and compile-time type safety and clarity.

Entity-Component-System (ECS) with a focus on ease-of-use, runtime extensibility and compile-time type safety and clarity.

Kengine The Koala engine is a type-safe and self-documenting implementation of an Entity-Component-System (ECS), with a focus on runtime extensibility

Sep 22, 2022

A C++ 17 implementation of qntm's base65536 that runs at compile time

A C++ 17 implementation of qntm's base65536 that runs at compile time. With alternatives for C++ 11 and C++ 14 that runs at runtime. Useage: At compil

Feb 13, 2022
A compile-time enabled Modern C++ library that provides compile-time dimensional analysis and unit/quantity manipulation.

mp-units - A Units Library for C++ The mp-units library is the subject of ISO standardization for C++23/26. More on this can be found in ISO C++ paper

Sep 21, 2022
A modern compile-time reflection library for C++ with support for overloads, templates, attributes and proxies

refl-cpp v0.12.1 Documentation refl-cpp encodes type metadata in the type system to allow compile-time reflection via constexpr and template metaprogr

Sep 15, 2022
Compile-Time Reflection in C++ for use with Scripting Languages

Introspective Introspective is a header file that brings reflection to any class that wants it, regardless of whether the reflected member is a consta

Mar 10, 2022
Selective user space swap (kubernetes swap / kubeswap)
Selective user space swap (kubernetes swap / kubeswap)

BigMaac ?? ?? ( Big Malloc Access And Calloc ) because sometimes a happy meal is not big enough BigMaac can be used in userspace (e.g. inside Kubernet

Jul 12, 2022
A compiling time static reflection framework for C++

static_reflect This is a fully compiling time static reflection lightweight framework for C++. It provides a very rich compile-time reflection functio

Sep 5, 2022
C++ compile-time enum to string, iteration, in a single header file
C++ compile-time enum to string, iteration, in a single header file

Better Enums Reflective compile-time enum library with clean syntax, in a single header file, and without dependencies. In C++11, everything can be us

Sep 19, 2022
A Compile time PCRE (almost) compatible regular expression matcher.

Compile time regular expressions v3 Fast compile-time regular expressions with support for matching/searching/capturing during compile-time or runtime

Sep 22, 2022
StrCrypt Compile-time string crypter library for C++
StrCrypt  Compile-time string crypter library for C++

StrCrypt Compile-time string crypter library for C++ Having plain strings stored in the binary file or in memory can help reversering attempts to be m

Jun 26, 2022
C++20 compile time compressed string tables

Squeeze - C++20 Compile time string compression Experiments in building complex compile time executed code using constexpr functions to generate a com

May 29, 2022
cavi is an open-source library that aims to provide performant utilities for closed hierarchies (i.e. all class types of the hierarchy are known at compile time).

cavi cavi is an open-source library that aims to provide performant utilities for closed hierarchies (i.e. all class types of the hierarchy are known

Mar 9, 2022