field-reflection C++
July 17, 2026 ยท View on GitHub
Compile-time reflection for C++ to get field names and types from a struct/class.
Features
- compile-time reflection
- header-only single file
- no user-side macros
- no dependencies
Requirements
C++20 compilers are required to use this library.
- GCC >= 11
- Clang >= 15
- with libc++-16 or later
- MSVC >= 19.37
- clang-cl >= 17
Installation
This library is header-only. You can copy include/field_reflection.hpp into your include path, or consume the CMake target from this repository:
add_subdirectory(field-reflection)
target_link_libraries(your_target PRIVATE field_reflection::field_reflection)
If the library has been installed as a CMake package, use:
find_package(field_reflection CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE field_reflection::field_reflection)
Usage
#include <array>
#include <cstdint>
#include <iostream>
#include <map>
#include <string>
#include <string_view>
#include "field_reflection.hpp"
using namespace field_reflection;
struct my_struct
{
int i = 287;
double d = 3.14;
std::string hello = "Hello World";
std::array<std::uint64_t, 3> arr = {1, 2, 3};
std::map<std::string, int> map{{"one", 1}, {"two", 2}};
};
// get field names
constexpr auto my_struct_n0 = field_name<my_struct, 0>; // "i"sv
constexpr auto my_struct_n1 = field_name<my_struct, 1>; // "d"sv
constexpr auto my_struct_n2 = field_name<my_struct, 2>; // "hello"sv
constexpr auto my_struct_n3 = field_name<my_struct, 3>; // "arr"sv
constexpr auto my_struct_n4 = field_name<my_struct, 4>; // "map"sv
// get field types
using my_struct_t0 = field_type<my_struct, 0>; // int
using my_struct_t1 = field_type<my_struct, 1>; // double
using my_struct_t2 = field_type<my_struct, 2>; // std::string
using my_struct_t3 = field_type<my_struct, 3>; // std::array<uint64_t, 3>
using my_struct_t4 = field_type<my_struct, 4>; // std::map<std::string, int>
// get field values with index
auto s = my_struct{};
auto& my_struct_v0 = get_field<0>(s); // s.i
auto& my_struct_v1 = get_field<1>(s); // s.d
auto& my_struct_v2 = get_field<2>(s); // s.hello
auto& my_struct_v3 = get_field<3>(s); // s.arr
auto& my_struct_v4 = get_field<4>(s); // s.map
// visit each field
for_each_field(s, [](std::string_view field, auto& value) {
// i: 287
// d: 3.14
// hello: Hello World
// arr: [1, 2, 3]
// map: {"one": 1, "two": 2}
std::println("{}: {}", field, value);
});
API References
Concepts
template<typename T>
concept field_countable;
template<typename T>
concept field_referenceable;
template<typename T>
concept field_namable;
The field_countable is a concept that checks if the type T is a field-countable struct. Internally, it is equivalent to that T is an aggregate type and the number of fields is less than or equal to the implementation limit, which is min(100, sizeof(T) * CHAR_BIT).
The field_referenceable is a concept that checks if a field of the type T can be referenced by index. This includes the field_countable concept. The implementation of the field_referenceable concept is the condition that the field_countable type T has no base class.
The field_namable is a concept that checks if a field name of the type T can be obtained by index statically. This includes the field_referenceable concept and also requires that the type T has a field and (practically) there is no reference type member.
The concepts form the following hierarchy:
| Type shape | field_countable | field_referenceable | field_namable |
|---|---|---|---|
| Aggregate with ordinary data members | yes | yes | yes |
| Aggregate with reference data members | yes | yes | no |
| Aggregate with a base class | yes | no | no |
| Empty aggregate | yes | yes | no |
| Non-aggregate type | no | no | no |
field_count
template <field_countable T>
constexpr std::size_t field_count;
Get the number of fields from the field_countable type T.
field_name
template <field_namable T, std::size_t N>
constexpr std::string_view field_name;
Get the name of the N-th field as std::string_view from the field_namable type T.
field_type
template <field_referenceable T, std::size_t N>
using field_type;
Get the type of the N-th field from the field_referenceable type T.
This operation inspects only the field type. It does not extract the field value or convert the object to a tuple, so it can be used in cases where value-based operations are not available. Examples include aggregates with non-copyable or non-movable fields, and compiler-specific packed aggregates whose fields cannot be referenced as ordinary lvalues.
get_field
// reference
template <std::size_t N, field_referenceable T>
constexpr auto& get_field(T& t) noexcept;
// const reference
template <std::size_t N, field_referenceable T>
constexpr const auto& get_field(const T& t) noexcept;
// rvalue reference
template <std::size_t N, field_referenceable T>
constexpr auto get_field(T&& t) noexcept;
Extracts the N-th element from the field_referenceable type T. The lvalue overloads return a reference to the field. The rvalue overload returns the selected field by value, moving it from the source object when possible.
Because this operation accesses the field value, it is subject to the usual language and compiler rules for forming references or moving values from the selected field.
type_name
template <class T>
constexpr std::string_view type_name;
Get the name of the type T.
Example
#include <field_reflection.hpp>
#include <format>
#include <type_traits>
#include <utility> // std::exchange
#include <variant>
using Token = std::variant<struct Number, struct Identifier>;
struct Number {
int value;
};
struct Identifier {
std::string name;
};
template <typename T, typename Variant>
inline constexpr bool alternative_of = false;
template <typename T, typename... Types>
inline constexpr bool alternative_of<T, std::variant<Types...>> =
(std::is_same_v<T, Types> || ...);
template <typename T>
requires alternative_of<T, Token>
struct std::formatter<T> {
constexpr auto parse(auto& ctx) -> decltype(ctx.begin()) {
auto it = ctx.begin();
if (it != ctx.end() and *it != '}') {
throw std::format_error("invalid format");
}
return it;
}
auto format(const T& t, auto& ctx) const -> decltype(ctx.out()) {
auto out = ctx.out();
out = std::format_to(out, "{} {{", field_reflection::type_name<T>);
const char* dlm = "";
field_reflection::for_each_field(
t, [&](std::string_view name, const auto& value) {
std::format_to(
out, "{}\n .{}={}", std::exchange(dlm, ","), name, value);
});
out = std::format_to(out, "\n}}");
return out;
}
};
#include <iostream>
int main() {
Number num{42};
Identifier ident{"ident"};
std::cout << std::format("{}", num) << std::endl;
std::cout << std::format("{}", ident) << std::endl;
// Expected Output
// ===============
// Number {
// .value=42
// }
// Identifier {
// .name=ident
// }
}
for_each_field, all_of_field, any_of_field
// unary operation
template <field_referenceable T, typename Func>
void for_each_field(T&& t, Func&& func);
template <field_referenceable T, typename Func>
bool all_of_field(T&& t, Func&& func);
template <field_referenceable T, typename Func>
bool any_of_field(T&& t, Func&& func);
// binary operation
template <field_referenceable T, typename Func>
void for_each_field(T&& t1, T&& t2, Func&& func);
template <field_referenceable T, typename Func>
bool all_of_field(T&& t1, T&& t2, Func&& func);
template <field_referenceable T, typename Func>
bool any_of_field(T&& t1, T&& t2, Func&& func);
Visits each field of the type T and applies the unary or binary operation func. The func must be a callable object that takes one of the following kinds of arguments:
- Arguments of one or two references to the field for the
field_referenceabletypeT. - Arguments of
std::string_viewand one or two references to the field for thefield_namabletypeT.
The for_each_field just applies the func and returns void, while the all_of_field and any_of_field return bool indicating whether all or any of the func returns true.
For example, the following code prints the field names of the my_struct s:
constexpr auto func = [](std::string_view field, auto& value) {
std::println("{}: {}", field, value);
};
for_each_field(s, func);
The above is equivalent to:
func("i"sv, s.i);
func("d"sv, s.d);
func("hello"sv, s.hello);
func("arr"sv, s.arr);
func("map"sv, s.map);
The first argument in the definition of the func can be omitted if it is not needed.
The binary operation version of for_each_field is useful for comparing each field of two objects of the same type:
constexpr auto func = [](std::string_view field, auto& value1, auto& value2) {
if (value1 != value2) {
std::println("s1 and s2 have a different value: s1.{} = {}, s2.{} = {}",
field, value1, field, value2);
}
};
for_each_field(s1, s2, func);
to_tuple
template <field_referenceable T>
constexpr std::tuple<...> to_tuple(T&& t);
Copy a field_referenceable type T object and convert it to std::tuple, where each tuple element has the same type as the corresponding field. For example, a my_struct object can be converted to an object of type std::tuple<int, double, std::string, std::array<std::uint64_t, 3>, std::map<std::string, int>>.
When t is an rvalue, the tuple elements are move-constructed from the fields when possible. This allows to_tuple and the field visitor functions to work with move-only fields such as std::unique_ptr.
Acknowledgments
This project is strongly inspired by the following and stands as
- an alternative to visit_struct without macros,
- a reflection library that is a partial reimplementation of reflect-cpp.
The C++20 implementation of the counting field in this library is partially referenced to Boost.PFR.