Programmiersprache: Reflection in C++26

Mit Reflection bietet C++26 ein leistungsstarkes Feature, das mehr kann als die Struktur eines Programms zu untersuchen.

In Pocket speichern vorlesen Druckansicht
c++ Schriftzug

(Bild: SergioVas/Shutterstock)

Lesezeit: 3 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Reflection oder zu Deutsch Reflexion ist die Fähigkeit eines Programms, seine Struktur und sein Verhalten zu untersuchen, zu betrachten und zu ändern.

Reflection in C++ ist mehr. Hier sind zwei Aussagen aus dem Proposal (P2996R5).

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

"We not only want to observe the structure of the program: We also want to ease generating code that depends on those observations. That combination is sometimes referred to as "reflective metaprogramming”, but within WG21 discussion the term "reflection” has often been used informally to refer to the same general idea."

"This proposal is not intended to be the end-game as far as reflection and compile-time metaprogramming are concerned. Instead, we expect it will be a useful core around which more powerful features will be added incrementally over time. In particular, we believe that most or all the remaining features explored in P1240R2 and that code injection (along the lines described in [P2237R0]) are desirable directions to pursue."

Die Geschichte der Reflection in C++ basiert auf der Template-Metaprogrammierung. Die Template-Metaprogrammierung begann um 1994, ebenso wie die Reflection. C++98 erhielt die Laufzeit-Reflection (RTTI) und die Funktions-Template-Typ-Deduktion. Die Datentypen-Bibliothek in C++11 verbesserte die Fähigkeiten von C++. In C++26 werden wir wahrscheinlich eine allgemeine Reflection-Unterstützung erhalten.

Ich werde mich bei der Vorstellung von Reflection in C++26 auf den Proposal P2996R5 stützen und Beispiele aus diesem verwenden.

Zunächst möchte ich vom Reflection-Wert zu den grammatikalischen Elementen vor- und zurückspringen.

Das folgende Programm beginnt im grammatikalischen Bereich, springt in den Reflexionsbereich und wieder zurück in den grammatikalischen Bereich.

// forthAndBack.cpp (P2996R5)

#include <iostream>
#include <cassert>
#include <concepts>

int main() {
    constexpr auto r = ^int;
    typename[:r:] x = 42;       // Same as: int x = 42;
    typename[:^char:] c = '*';  // Same as: char c = '*';

    static_assert(std::same_as<decltype(x), int>);
    static_assert(std::same_as<decltype(c), char>);
    assert(x == 42);
    assert(c == '*');
}
  • ^: Reflexionsoperator erzeugt einen Reflexionswert aus seinem Operanden (^int und ^char)
  • [: refl :]: Splicer erzeugt ein grammatikalisches Element aus einem Reflexionswert ([:r:] und [:^char:])
  • Reflexionswert ist eine Darstellung von Programmelementen als konstanter Ausdruck

Der Aufruf ^gramOper erzeugt eine Reflection mit einem Datentyp: std::meta::info. std::same_as ist ein Concept.

Das Springen zwischen den Bereichen Grammatik und Reflection ist alleine wenig sinnvoll. Hier eine genauere Analyse des Programms enumString.cpp:

Enum => String

Das folgende Beispiel wandelt einen Enum-Wert in einen String um:

// enumString.cpp

#include <iostream>
#include <experimental/meta>
#include <string>
#include <type_traits>


template<typename E>
  requires std::is_enum_v<E>                      // (1)
constexpr std::string enum_to_string(E value) {
  std::string result = "<unnamed>";
  [:expand(std::meta::enumerators_of(^E)):] >>    // (2)
  [&]<auto e>{
    if (value == [:e:]) {
      result = std::meta::identifier_of(e);       // (3)
    }
  };
  return result;
}


int main() {

    std::cout << '\n';

    enum Color { red, green, blue };
    std::cout << "enum_to_string(Color::red): " << enum_to_string(Color::red) << '\n';
    // std::cout << "enum_to_string(42): " << enum_to_string(42) << '\n'; 

    std::cout << '\n';

}

In (1) wird mithilfe der Datentyp-Eigenschaft std::is_enum geprüft, ob value ein Enumerator ist. Der Ausdruck ^E in (2) erzeugt den Reflexionswert. Die Funktion expand in derselben Zeile darf man ignorieren. Die Expansionsanweisungen fehlen in der aktuellen Implementierung.

Die Funktionen std::meta::enumerators_of und std::meta::enumerators_of in (2) und (3) sind Metafunktionen. Sie können nur zur Compile-Zeit ausgeführt werden, da sie als consteval deklariert sind.

Hier sind ein paar Metafunktionen.

namespace std::meta {
  consteval auto members_of(info type_class) -> vector<info>;
  consteval auto bases_of(info type_class) -> vector<info>;

  consteval auto static_data_members_of(info type_class) -> vector<info>;
  consteval auto nonstatic_data_members_of(info type_class) -> vector<info>;

  consteval auto subobjects_of(info type_class) -> vector<info> {
    auto subobjects = bases_of(type_class);
    subobjects.append_range(nonstatic_data_members_of(type_class));
    return subobjects;
  }

  consteval auto enumerators_of(info type_enum) -> vector<info>;
}

Alle Metafunktionen geben einen std::vector zurück. Reflection bietet viele Metafunktionen, um Datentypen zu analysieren und Code zu erzeugen.

Die Anwendung der umgekehrten Schritte macht aus String ein Enum:

template <typename E>
  requires std::is_enum_v<E>                           
constexpr std::optional<E> string_to_enum(std::string_view name) {
  template for (constexpr auto e : std::meta::enumerators_of(^E)) {
    if (name == std::meta::identifier_of(e)) {                     
      return [:e:];
    }
  }

  return std::nullopt;
}

Reflection bietet viele Metafunktionen. Ich werde sie in meinem nächsten Artikel anwenden. (rme)