Introduction

C++ lacks native runtime reflection — unlike Java, C#, or Python, you cannot inspect class members, enumerate properties, or dynamically invoke methods at runtime. This has spawned a rich ecosystem of libraries that bridge the gap using template metaprogramming, macros, and code generation. Three prominent solutions have emerged: rttr (runtime type reflection), refl-cpp (compile-time static reflection), and ponder (declarative C++ reflection).

Each takes a fundamentally different approach to solving the same problem. This article compares their APIs, use cases, and trade-offs with real code examples for serialization, GUI binding, and plugin systems.

Comparison Table

Featurerttrrefl-cppponder
GitHub Stars3,4681,204~400
Last UpdateApril 2024November 20222019
ApproachRuntime (C++11)Compile-Time (C++17)Macro-Based (C++11)
Property EnumerationYesYesYes
Method InvocationYesLimitedYes
Type RegistrationManual macrosManual refl::reflectManual macros
SerializationBuilt-in variantUser-implementedUser-implemented
GUI BindingSupportedManualSupported
Inheritance SupportFullManualFull
Plugin SystemsYes (dynamic loading)NoYes
PerformancePointer-heavy (vtable)Zero-overhead (constexpr)Pointer-based

rttr: The Runtime Powerhouse

rttr (Run Time Type Reflection) is the most mature and feature-rich option. It provides a complete reflection API that mirrors what you would expect in languages like C# — you can query types, enumerate properties, invoke methods, and create instances by name. It is the only library suitable for building plugin systems where types are discovered at runtime.

Installation

1
2
3
4
5
6
# Clone and build with CMake
git clone https://github.com/rttrorg/rttr.git
cd rttr && mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
1
2
3
4
5
6
7
8
9
# Or use FetchContent in your project
include(FetchContent)
FetchContent_Declare(
  rttr
  GIT_REPOSITORY https://github.com/rttrorg/rttr.git
  GIT_TAG v0.9.6
)
FetchContent_MakeAvailable(rttr)
target_link_libraries(myapp PRIVATE rttr_core)

Registering and Using Types

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <rttr/registration>
#include <iostream>

struct Vector3 {
  float x, y, z;
  Vector3() : x(0), y(0), z(0) {}
  void normalize() { /* normalize vector */ }
  std::string to_string() const {
    return "(" + std::to_string(x) + "," + std::to_string(y) + "," + std::to_string(z) + ")";
  }
};

// Manual registration block — define reflection metadata
RTTR_REGISTRATION {
  rttr::registration::class_<Vector3>("Vector3")
    .constructor<>()
    .property("x", &Vector3::x)
    .property("y", &Vector3::y)
    .property("z", &Vector3::z)
    .method("normalize", &Vector3::normalize)
    .method("to_string", &Vector3::to_string);
}

int main() {
  // Create instance by type name at RUNTIME
  rttr::type t = rttr::type::get_by_name("Vector3");
  rttr::variant obj = t.create(); // Constructs Vector3
  
  // Set properties dynamically
  t.set_property_value("x", obj, 1.0f);
  t.set_property_value("y", obj, 2.0f);
  
  // Invoke method by name
  rttr::variant result = t.invoke("to_string", obj, {});
  std::cout << result.to_string() << std::endl; // (1,2,0)
}

The standout feature is t.create() — you can instantiate types whose names are only known at runtime, enabling true plugin architectures.

Serialization with rttr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Generic JSON serializer using rttr reflection
void to_json(rttr::instance obj, std::ostream& out) {
  out << "{";
  auto props = obj.get_derived_type().get_properties();
  for (size_t i = 0; i < props.size(); ++i) {
    auto val = props[i].get_value(obj);
    out << "\"" << props[i].get_name() << "\": ";
    if (val.is_type<float>()) out << val.to_float();
    else if (val.is_type<int>()) out << val.to_int();
    else out << "\"" << val.to_string() << "\"";
    if (i < props.size() - 1) out << ", ";
  }
  out << "}";
}

refl-cpp: Compile-Time Static Reflection

refl-cpp by Veselin Karaganev takes a fundamentally different approach — everything happens at compile time using C++17 constexpr. There is no runtime overhead and no dynamic allocation. The trade-off is that type names must be known at compile time (no runtime plugin loading).

Basic Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <refl.hpp>
#include <iostream>

struct Point {
  float x, y;
};

// Reflection metadata — compile-time constant
REFL_TYPE(Point)
  REFL_FIELD(x)
  REFL_FIELD(y)
REFL_END

int main() {
  Point p{10.0f, 20.0f};
  
  // Iterate over fields at COMPILE TIME
  refl::util::for_each(refl::reflect<Point>().members, [&](auto member) {
    std::cout << member.name << " = " << member(p) << std::endl;
  });
  // Output: x = 10, y = 20
}

The magic is that refl::reflect<Point>().members returns a compile-time tuple of field descriptors. The for_each loop is fully unrolled by the optimizer — there is zero iteration overhead.

Compile-Time Serialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename T>
std::string to_json(const T& obj) {
  std::string result = "{";
  refl::util::for_each(refl::reflect<T>().members, [&](auto member) {
    result += "\"" + std::string(member.name) + "\": ";
    result += std::to_string(member(obj));
    result += ", ";
  });
  result.pop_back(); result.pop_back(); // remove trailing ", "
  result += "}";
  return result;
}

refl-cpp is ideal for situations where you want reflection-like capabilities but cannot afford any runtime cost — embedded systems, game engines, and performance-critical serialization paths.

ponder: Declarative Macro-Based Reflection

ponder takes yet another approach — it uses macros and a runtime registry similar to rttr but with a more declarative syntax inspired by Qt’s meta-object system. While development appears stalled (last commit 2019), it remains a functional option for C++11 codebases that prefer a cleaner registration syntax.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <ponder/classbuilder.hpp>

struct Player {
  std::string name;
  int health;
  void takeDamage(int amount) { health -= amount; }
};

// Declare reflection outside the class
PONDER_TYPE(Player)

void register_reflection() {
  ponder::Class::declare<Player>("Player")
    .property("name", &Player::name)
    .property("health", &Player::health)
    .function("takeDamage", &Player::takeDamage);
}

int main() {
  register_reflection();
  
  const ponder::Class& cls = ponder::classByType<Player>();
  Player p{"Hero", 100};
  
  // Set property by name
  cls.property("health").set(p, 50);
  // Call function by name
  cls.function("takeDamage").call(p, 10);
}

Choosing the Right Approach

rttr is the clear winner for applications requiring true runtime reflection — plugin systems, scripting engine bindings, GUI property editors, and serialization frameworks. Its ability to create instances by type name at runtime is unmatched by the other two.

refl-cpp shines in performance-critical contexts where compile-time guarantees matter — game engines, embedded systems, and serialization hot paths. The constexpr approach means the optimizer can inline everything, producing assembly identical to hand-written accessors.

ponder is best viewed as a C++11-compatible alternative to rttr. If you are locked into C++11 and the rttr API feels too verbose, ponder offers a cleaner syntax. However, with no updates since 2019, it should not be a first choice for new projects.

For related reading, see our guides on C++ serialization libraries, schema serialization frameworks, and template metaprogramming. If your project involves runtime type discovery, also see our C++ variant and sum type libraries comparison for alternative approaches to dynamic typing.

Integration Patterns and Deployment Considerations

When integrating reflection into a build pipeline, each library imposes different constraints. rttr requires linking against a shared library (librttr_core) — this adds a runtime dependency but enables true dynamic plugin loading. In a Docker-based microservice architecture, you need to ensure the rttr shared library is present in all containers that might load reflected types. For Kubernetes deployments, bundling librttr_core.so in your container image alongside your application binary is standard practice.

refl-cpp, being header-only, has zero deployment overhead. There is no shared library to ship, no ABI compatibility concerns, and no dynamic linker dependencies. This makes it the natural choice for static linking scenarios — single-binary deployments, embedded firmware images, and WebAssembly targets where dynamic loading is impossible. The trade-off is that you lose the ability to discover types at runtime; all reflected types must be known at compile time.

ponder occupies a middle ground similar to rttr — it requires a compiled library but offers a more declarative registration syntax. For legacy C++11 codebases that cannot adopt C++17, ponder or rttr are the only viable options. Consider your target platform constraints: if you are deploying to Alpine Linux with musl libc (common in minimal Docker images), test rttr compatibility — it uses exceptions and RTTI which some minimal toolchains strip. refl-cpp avoids both exceptions and RTTI, working correctly even with -fno-exceptions -fno-rtti compiler flags.

FAQ

Why does C++ lack built-in reflection like Java or C#?

C++ follows a “do not pay for what you do not use” philosophy. Runtime reflection requires storing type metadata (vtables, string tables) that would bloat every binary whether used or not. The C++ standards committee is working on static reflection (P2996) for C++26, which will provide compile-time introspection without runtime overhead — similar to what refl-cpp already offers today.

Can rttr handle template classes?

rttr can register specific template instantiations (e.g., std::vector<int>) but cannot reflect the template itself. Each instantiation must be registered separately. refl-cpp can reflect template instantiations at compile time using the same pattern.

Is there a performance penalty for using reflection?

rttr adds a vtable-like indirection layer — property access is approximately 3-5x slower than direct member access. refl-cpp has zero overhead because everything is resolved at compile time. For most applications (GUI, serialization, configuration), the overhead is negligible. In hot loops where nanoseconds matter, use refl-cpp or access members directly.

How do these libraries handle inheritance?

rttr automatically discovers base class properties and methods. ponder requires explicit base<BaseClass>() declaration. refl-cpp requires manual bases<BaseClass> in the reflection metadata but the iteration is compile-time.

Can I use these libraries in a header-only fashion?

refl-cpp is fully header-only. rttr requires linking against librttr_core. ponder requires linking against libponder. For header-only requirements, refl-cpp is the only option.

What about C++26 static reflection (P2996)?

The proposed C++26 reflection will make libraries like refl-cpp partially obsolete by providing the reflection operator (^) and splicing ([: ... :]) as core language features. However, the standard will not support runtime type discovery by name (rttr’s killer feature), so both approaches will coexist for different use cases.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com