C++26 Static Reflection Complete Guide
이 글의 핵심
Query type information at compile time with zero runtime overhead using C++26 Static Reflection. From basic syntax to serialization, code generation, and practical patterns.
Introduction
C++26’s Static Reflection is rated as “the biggest upgrade since template invention”. Previously, obtaining type information required macros, SFINAE, complex template metaprogramming, but now it can be simply queried with standard library functions. This guide explains Static Reflection basic syntax, practical usage patterns (serialization, ORM, code generation), and comparison with existing approaches with code examples. Prerequisites:
- C++ template basics (Template Guide)
- Understanding constexpr functions
- Metaprogramming concepts
Reality in Production
When learning development, everything seems clean and theoretical. But production is different. Wrestling with legacy code, chasing tight deadlines, facing unexpected bugs. The content covered here was initially learned as theory, but through applying it to real projects, I realized “ah, that’s why it’s designed this way.”
Table of Contents
- What is Static Reflection?
- Basic Syntax
- Type Query
- Member Iteration
- Practice: Auto Serialization
- Practice: ORM Mapping
- Practice: Code Generation
- Comparison with Existing Approaches
- Compiler Support
- Performance and Constraints
- Conclusion
What is Static Reflection?
Concept
Reflection is the ability of a program to inspect and manipulate its own structure (types, members, functions, etc.). C++26 Static Reflection characteristics:
- Compile-time only: All operations performed at compile time
- Zero overhead: No runtime performance impact
- Type safe: Compiler detects all errors
- Standard library: Provides
<meta>header
Limitations of Existing Approaches
Macro-based (C++03 style):
#define REFLECT_STRUCT(Type, ...) \
/* Complex macro magic */
REFLECT_STRUCT(Person, name, age, email);
- Difficult to debug
- No type safety
- Poor IDE support SFINAE/enable_if (C++11 style):
template<typename T>
auto serialize(const T& obj)
-> decltype(obj.name, void()) {
// Compiles only when name member exists
}
- Hard to read
- Complex error messages
- Difficult to maintain C++26 Static Reflection:
template<typename T>
std::string serialize(const T& obj) {
std::string result = "{";
[:expand(nonstatic_data_members_of(^T)):] {
result += std::format("\"{}\": {}, ",
identifier_of(^^[::])),
obj.[::]
);
}
return result + "}";
}
- Easy to read
- Type safe
- Compiler optimization possible
Basic Syntax
Reflection Operator: ^
^ operator converts type or expression to reflection:
#include <meta>
struct Person {
std::string name;
int age;
};
// Type reflection
constexpr auto person_reflection = ^Person;
// Member reflection
constexpr auto name_reflection = ^Person::name;
// Expression reflection
int x = 42;
constexpr auto x_reflection = ^x;
Splicer: [: :]
[: :] converts reflection back to code:
// Get type name
constexpr auto type_name = identifier_of(^Person);
// type_name == "Person"
// Convert to type
using PersonType = [:^Person:]; // Same as Person
// Member access
Person p{"Alice", 30};
int age = p.[: ^Person::age :]; // Same as p.age
Basic Query Functions
#include <meta>
#include <iostream>
struct Point {
double x;
double y;
void print() const {
std::cout << "(" << x << ", " << y << ")\n";
}
};
int main() {
// Type name
std::cout << identifier_of(^Point) << '\n'; // "Point"
// Member count
constexpr auto members = nonstatic_data_members_of(^Point);
std::cout << members.size() << '\n'; // 2
// Member function count
constexpr auto methods = member_functions_of(^Point);
std::cout << methods.size() << '\n'; // 1
// Print member names
[:expand(members):] {
std::cout << identifier_of(^^[::]) << '\n';
}
// Output: x, y
}
Type Query
Inspecting a Type Without Instantiating It
Reflection lets you ask questions about a type at compile time that previously required either a running instance or elaborate SFINAE tricks — kind, qualifiers, base classes, and whether it is an aggregate, enum, or class.
#include <meta>
#include <type_traits>
struct Base {};
struct Derived : Base { int x; };
enum class Color { Red, Green, Blue };
constexpr auto derived_info = ^Derived;
constexpr auto color_info = ^Color;
static_assert(is_class_type(derived_info)); // true
static_assert(!is_enum_type(derived_info)); // true
static_assert(is_enum_type(color_info)); // true
// Query base classes
constexpr auto bases = bases_of(derived_info);
static_assert(bases.size() == 1);
static_assert(identifier_of(type_of(bases[0])) == "Base");
Enumerator Reflection
Reflection also reaches into enum values — useful for generating to_string/from_string for enums without hand-writing a switch per enumerator.
#include <meta>
#include <string>
template<typename E>
std::string enum_to_string(E value) {
std::string result = "unknown";
[:expand(enumerators_of(^E)):] {
if (value == [:^^[::]:]) {
result = identifier_of(^^[::]);
}
}
return result;
}
enum class Status { Active, Inactive, Pending };
int main() {
std::cout << enum_to_string(Status::Active) << '\n'; // "Active"
}
Common Type-Query Functions
| Function | Purpose |
|---|---|
is_class_type(r) | Is the reflected type a class/struct? |
is_enum_type(r) | Is it an enum? |
bases_of(r) | List of base classes |
type_of(r) | The type reflected by a member/variable reflection |
enumerators_of(r) | List of an enum’s enumerators |
is_public(r) / is_private(r) | Access level of a member |
Member Iteration
Print All Members
#include <meta>
#include <iostream>
template<typename T>
void print_members(const T& obj) {
std::cout << identifier_of(^T) << " {\n";
[:expand(nonstatic_data_members_of(^T)):] {
std::cout << " " << identifier_of(^^[::])
<< " = " << obj.[::] << '\n';
}
std::cout << "}\n";
}
struct Config {
std::string host = "localhost";
int port = 8080;
bool ssl = false;
};
int main() {
Config config;
print_members(config);
// Output:
// Config {
// host = localhost
// port = 8080
// ssl = 0
// }
}
Practice: Auto Serialization
JSON Serialization
#include <meta>
#include <string>
#include <sstream>
template<typename T>
std::string to_json(const T& obj) {
std::ostringstream oss;
oss << "{";
bool first = true;
[:expand(nonstatic_data_members_of(^T)):] {
if (!first) oss << ", ";
first = false;
oss << "\"" << identifier_of(^^[::]) << "\": ";
// Handle different types
if constexpr (std::is_same_v<decltype(obj.[::]), std::string>) {
oss << "\"" << obj.[::] << "\"";
} else {
oss << obj.[::];
}
}
oss << "}";
return oss.str();
}
struct User {
int id;
std::string name;
int age;
};
int main() {
User user{1, "Alice", 30};
std::cout << to_json(user) << '\n';
// {"id": 1, "name": "Alice", "age": 30}
}
Practice: ORM Mapping
SQL Query Generation
template<typename T>
std::string generate_insert_query(const T& obj) {
std::string table_name = identifier_of(^T);
std::string columns = "";
std::string values = "";
bool first = true;
[:expand(nonstatic_data_members_of(^T)):] {
if (!first) {
columns += ", ";
values += ", ";
}
first = false;
columns += identifier_of(^^[::]);
values += std::format("'{}'", obj.[::]);
}
return std::format("INSERT INTO {} ({}) VALUES ({})",
table_name, columns, values);
}
struct Product {
int id;
std::string name;
double price;
};
int main() {
Product product{1, "Laptop", 999.99};
std::cout << generate_insert_query(product) << '\n';
// INSERT INTO Product (id, name, price) VALUES ('1', 'Laptop', '999.99')
}
Practice: Code Generation
Auto-Generating Getters/Setters
Reflection can generate an entire family of accessor functions from a struct’s member list at compile time — no macros, no code-generation build step.
#include <meta>
template<typename T, auto Member>
auto& get_field(T& obj) {
return obj.[:Member:];
}
template<typename T>
void print_all_getters() {
[:expand(nonstatic_data_members_of(^T)):] {
std::cout << "get_" << identifier_of(^^[::]) << "() -> "
<< identifier_of(type_of(^^[::])) << '\n';
}
}
Generating a Comparison Operator
A common boilerplate task — member-wise operator== — becomes a single generic function instead of one hand-written operator per type.
#include <meta>
template<typename T>
bool structural_equal(const T& a, const T& b) {
bool equal = true;
[:expand(nonstatic_data_members_of(^T)):] {
equal = equal && (a.[::] == b.[::]);
}
return equal;
}
struct Point { int x, y; };
int main() {
Point p1{1, 2}, p2{1, 2};
std::cout << structural_equal(p1, p2) << '\n'; // 1 (true)
}
Generating a Visitor/Printer for Any Struct
Combined with if constexpr, reflection can produce a single debug_print usable on any aggregate — a pattern that previously needed a separate macro invocation per type.
#include <meta>
#include <iostream>
template<typename T>
void debug_print(const T& obj, int indent = 0) {
std::string pad(indent, ' ');
std::cout << pad << identifier_of(^T) << " {\n";
[:expand(nonstatic_data_members_of(^T)):] {
std::cout << pad << " " << identifier_of(^^[::]) << ": " << obj.[::] << '\n';
}
std::cout << pad << "}\n";
}
Comparison with Existing Approaches
Before vs After
Before (Macro-based):
#define SERIALIZE_STRUCT(Type, ...) \
/* 100+ lines of complex macro magic */
SERIALIZE_STRUCT(User, id, name, email);
After (Reflection):
// Single generic function for all types
template<typename T>
std::string serialize(const T& obj) {
return to_json(obj); // Works for any struct
}
Code Reduction
| Task | Before | After | Reduction |
|---|---|---|---|
| Serialization | 100+ lines | 10 lines | 90% |
| ORM mapping | 200+ lines | 20 lines | 90% |
| Debug output | 50+ lines | 5 lines | 90% |
Compiler Support
Current Status (March 2026)
| Compiler | Version | Support Status |
|---|---|---|
| GCC | 14+ | Experimental (-std=c++2c -freflection) |
| Clang | 19+ | Partial (-std=c++2c) |
| MSVC | TBD | In development |
Usage
# GCC
g++ -std=c++2c -freflection source.cpp -o output
# Clang
clang++ -std=c++2c source.cpp -o output
Performance and Constraints
Zero Runtime Cost, by Construction
Every reflection operation shown in this guide (^, nonstatic_data_members_of, expand, splicing with [: :]) happens at compile time. The generated binary contains only the code the splices expand to — there is no reflection metadata, no runtime type table, and no lookup cost, unlike Java/C# reflection.
// This compiles to exactly the same machine code as hand-written
// member-by-member access — the reflection machinery leaves no trace.
template<typename T>
std::string to_json(const T& obj) { /* ... */ }
Compile-Time Cost
The cost reflection does have is at compile time: heavy use of expand over large member lists, deeply nested reflection-generated templates, or reflecting over very large types can noticeably increase build time, similar to heavy template metaprogramming today. Profile build times on large translation units before adopting reflection project-wide.
Current Constraints (as of C++26 rollout)
- Compiler support is still experimental — GCC 14+/Clang 19+ require explicit flags, and codegen quality varies by compiler version
- Reflection of function bodies (as opposed to signatures/members) is out of scope for the initial C++26 feature set
- Third-party library support (serialization libraries, ORMs) is still catching up — expect to write your own reflection-based utilities for a while before mature libraries appear
- Debugger support for splice-generated code varies — step-through debugging of expanded reflection code can be harder to follow than hand-written equivalents
Conclusion
C++26 Static Reflection is a feature that changes the metaprogramming paradigm: Key Advantages:
- Readability: Complex template tricks → Clear code
- Zero overhead: Compile-time only
- Type safety: Compiler validation
- Productivity: Auto-generate boilerplate code Main Use Cases:
- Serialization/deserialization (JSON, Binary, XML)
- ORM, database mapping
- RPC, network protocols
- Test frameworks
- Code generators Getting Started:
- Install GCC 14+ or Clang 19+
- Start with simple
to_stringfunction - Convert project serialization code to Reflection
- Gradually expand application Next Learning:
- C++ Template Advanced
- C++ Concepts
- C++ Metaprogramming References:
- P2996: Reflection for C++26
- C++26 Feature Complete
- GCC Reflection Documentation
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Query type information at compile time with zero runtime overhead using C++26 Static Reflection.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 템플릿 특수화 완벽 가이드 | 완전·부분 특수화, 문제 시나리오, 프로덕션 패턴
- C++20 Concepts | Making Template Error Messages Readable
- C++ SFINAE and Concepts — Complete Guide
Keywords Covered in This Article (Related Search Terms)
This article covers C++26, Reflection, Metaprogramming, Compile-time, std::meta, Serialization, Code Generation.