Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ on:

jobs:

# FIXME: We're having a linker error wrt. Catch2 on Windows with MSVC
windows-msvc:
name: "Windows - MSVC - C++${{ matrix.cxx_standard }}"
runs-on: windows-latest
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(reflection-cpp VERSION 0.4.0 LANGUAGES CXX)
project(reflection-cpp VERSION 0.5.0 LANGUAGES CXX)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
Expand Down
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,94 @@ This is a C++ static reflection library.
- Minimal to zero runtime overhead
- Works with C++20 and later
- Prepared to integrate C++26 reflections when they are available

## Requirements

A compiler with C++20 support. The library is tested with GCC 13 and later, Clang 17 and later,
Apple Clang, MSVC and clang-cl. It is a single header without dependencies beyond the standard library.

Reflection works on aggregates: types without user-declared constructors, private members or base classes.
Members may be of any type, including types without a default constructor. An aggregate may have up to
150 members.

With MSVC, member names are only available for types with external linkage, that is, not for types declared
in an anonymous namespace or inside a function.

## Usage

```cpp
#include <reflection-cpp/reflection.hpp>

struct Person
{
std::string name;
std::string email;
int age;
};

auto person = Person { .name = "John Doe", .email = "john@doe.com", .age = 42 };
```

Everything about the type itself is available at compile time:

```cpp
static_assert(Reflection::CountMembers<Person> == 3);
static_assert(Reflection::MemberNameOf<0, Person> == "name");
static_assert(Reflection::MemberNames<Person>[2] == "age");
static_assert(std::same_as<Reflection::MemberTypeOf<2, Person>, int>);
static_assert(Reflection::TypeNameOf<Person> == "Person");
static_assert(Reflection::NameOf<&Person::email> == "email");
static_assert(Reflection::MemberIndexOf<&Person::email> == 1);
```

Members of an object are accessed by reference, and never copied:

```cpp
Reflection::GetMemberAt<2>(person) = 43;

// name and value of each member
Reflection::CallOnMembers(person, [](std::string_view name, auto const& value) {
std::println("{}: {}", name, value);
});

// index of each member as a template argument, e.g. to look up something else by it
Reflection::EnumerateMembers(person, []<size_t I>(auto& value) {
std::println("{}: {}", Reflection::MemberNameOf<I, Person>, value);
});

// only the members at index 0 and 2
Reflection::EnumerateMembers<std::integer_sequence<size_t, 0, 2>>(person, []<size_t I>(auto& value) { /* ... */ });
```

A type can be traversed without having an object of it:

```cpp
Reflection::EnumerateMembers<Person>([]<size_t I, typename MemberType>() {
std::println("{} is a {}", Reflection::MemberNameOf<I, Person>, Reflection::TypeNameOf<MemberType>);
});

// counting the members of type std::string
auto const stringCount = Reflection::FoldMembers<Person>(size_t { 0 }, []<size_t I, typename MemberType>(size_t count) {
return count + (std::same_as<MemberType, std::string> ? 1 : 0);
});
```

Folding over the members of an object:

```cpp
auto const totalLength = Reflection::FoldMembers(person, size_t { 0 }, [](std::string_view name, auto const& /*value*/, size_t length) {
return length + name.size();
});
```

For debugging, objects can be printed and compared:

```cpp
Reflection::Inspect(person); // name="John Doe" email="john@doe.com" age=43

auto other = person;
other.age = 50;
Reflection::CollectDifferences(person, other, [](std::string_view name, auto const& lhs, auto const& rhs) {
std::println("{}: {} != {}", name, lhs, rhs); // age: 43 != 50
});
```
75 changes: 64 additions & 11 deletions include/reflection-cpp/reflection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
namespace Reflection
{

/// A string usable as a template argument, e.g. `template <StringLiteral Name> struct Tagged;`
///
/// N is the size of the string literal it is created from, including the terminating null character.
template <size_t N>
struct StringLiteral
{
Expand Down Expand Up @@ -105,7 +108,9 @@ namespace detail
}
} // namespace detail

// Helper to get the value out
/// Concatenates strings at compile time into a single std::string_view.
///
/// @tparam Strs The strings to join, each one a std::string_view with static storage duration
template <std::string_view const&... Strs>
inline constexpr auto JoinStringLiterals = detail::join<Strs...>();

Expand Down Expand Up @@ -220,7 +225,9 @@ namespace detail
#endif
} // namespace detail

// Count the number of members in an aggregate type.
/// The number of members of an aggregate type.
///
/// The members may be of types without a default constructor.
template <class T>
requires(std::is_aggregate_v<std::remove_cvref_t<T>>)
constexpr inline auto CountMembers = detail::CountMembers<std::remove_cvref_t<T>>;
Expand Down Expand Up @@ -1049,16 +1056,18 @@ constexpr decltype(auto) GetMemberAt(T&& t)
return std::get<I>(ToTuple(std::forward<T>(t)));
}

/// Represents the type of the member at index I of type Object
/// Represents the type of the member at index I of type Object, without reference or cv-qualifiers
template <auto I, typename Object>
using MemberTypeOf = std::remove_cvref_t<decltype(std::get<I>(ToTuple(std::declval<Object&>())))>;

/// Wraps a pointer, to pass the address of a member as a template argument.
template <class T>
struct WrappedPointer final
{
T* pointer;
};

/// Gets the address of the member at index N of an aggregate.
template <size_t N, class T>
constexpr auto GetElementPtrAt(T&& t) noexcept
{
Expand All @@ -1068,8 +1077,35 @@ constexpr auto GetElementPtrAt(T&& t) noexcept

namespace detail
{
#if defined(_MSC_VER) && !defined(__clang__)
// An object of type T that only has to exist at compile time: only the addresses of its members are used.
//
// MSVC requires T to have external linkage for this, and does not accept the alternative below either,
// so member names of types in anonymous namespaces or at function scope are not available with it.
template <class T>
extern const T External;
#else
// An object that is declared but never defined, as above, is rejected for types without external linkage,
// as no other translation unit could define it. A union holding the object as a member that is never
// constructed is a real definition, and still does not require T to be constructible.
template <class T>
union UninitializedStorage {
constexpr UninitializedStorage() noexcept:
dummy {}
{
}
constexpr ~UninitializedStorage() {}

char dummy;
T value;
};

template <class T>
inline constexpr UninitializedStorage<T> ExternalStorage {};

template <class T>
inline constexpr T const& External = ExternalStorage<T>.value;
#endif

#if defined(__clang__)
#pragma clang diagnostic push
Expand Down Expand Up @@ -1122,11 +1158,16 @@ namespace detail
};
} // namespace detail

/// The name of the member at index N of the aggregate type T, as a std::string_view.
template <auto N, class T>
inline constexpr auto MemberNameOf = []() constexpr {
return detail::MemberNameOfImpl<N, T>::stripped_literal;
}();

/// The name of the type T, including its namespace, as a std::string_view.
///
/// The spelling of the name is the compiler's, and differs between compilers for some types,
/// such as templates and arrays.
template <class T>
constexpr auto TypeNameOf = [] {
constexpr std::string_view name = detail::MangledName<T>();
Expand Down Expand Up @@ -1185,6 +1226,7 @@ namespace detail
}
} // namespace detail

/// The names of all members of the aggregate type T, as a std::array of std::string_view.
template <class T>
inline constexpr auto MemberNames = [] {
return detail::MemberNamesImpl<T>(std::make_index_sequence<CountMembers<T>> {});
Expand Down Expand Up @@ -1254,6 +1296,7 @@ namespace detail

} // namespace detail

/// Calls `f<I>()` for every I in the half-open range [B, E), with I as a template argument.
template <auto B, auto E, typename F>
constexpr void template_for(F&& f)
{
Expand All @@ -1264,6 +1307,9 @@ constexpr void template_for(F&& f)
}(std::make_integer_sequence<t, E - B> {});
}

/// Calls `f<I>()` for every element I of ElementMask, in order, with I as a template argument.
///
/// @tparam ElementMask A std::integer_sequence listing the values to call f with
template <typename ElementMask, typename F>
constexpr void template_for(F&& f)
{
Expand Down Expand Up @@ -1341,31 +1387,36 @@ template <auto P>
constexpr size_t MemberIndexOf =
detail::MemberIndexHelperImpl<P>(std::make_index_sequence<CountMembers<MemberClassType<P>>> {});

/// Calls a callable on members of an object specified with ElementMask sequence with the index of the member as the
/// first argument. and the member's default-constructed value as the second argument.
/// Calls a callable on the members of an object selected by ElementMask, with the index of the member as
/// template argument and a reference to the member as argument.
///
/// @tparam ElementMask A std::integer_sequence listing the indices of the members to visit
template <typename ElementMask, typename Object, typename Callable>
constexpr void EnumerateMembers(Object& object, Callable&& callable)
{
template_for<ElementMask>([&]<auto I>() { callable.template operator()<I>(GetMemberAt<I>(object)); });
}

/// Calls a callable on members of an object specified with ElementMask sequence with the index and member's type as
/// template arguments.
/// Calls a callable for the members of a type selected by ElementMask, with the index and the type of the member
/// as template arguments. No object of the type is needed.
///
/// @tparam ElementMask A std::integer_sequence listing the indices of the members to visit
template <typename ElementMask, typename Object, typename Callable>
constexpr void EnumerateMembers(Callable&& callable)
{
template_for<ElementMask>([&]<auto I>() { callable.template operator()<I, MemberTypeOf<I, Object>>(); });
}

/// Calls a callable on each member of an object with the index of the member as the first argument.
/// and the member's default-constructed value as the second argument.
/// Calls a callable on each member of an object, with the index of the member as template argument
/// and a reference to the member as argument.
template <typename Object, typename Callable>
constexpr void EnumerateMembers(Object& object, Callable&& callable)
{
template_for<0, CountMembers<Object>>([&]<auto I>() { callable.template operator()<I>(GetMemberAt<I>(object)); });
}

/// Calls a callable on each member of an object with the index and member's type as template arguments.
/// Calls a callable for each member of a type, with the index and the type of the member as template arguments.
/// No object of the type is needed.
template <typename Object, typename Callable>
constexpr void EnumerateMembers(Callable&& callable)
{
Expand All @@ -1378,6 +1429,7 @@ constexpr void EnumerateMembers(Callable&& callable)
// clang-format on
}

/// Calls a callable on each member of an object, with the name of the member and a reference to the member.
template <typename Object, typename Callable>
requires(CountMembers<Object> == 0) || std::is_invocable_v<Callable, std::string, MemberTypeOf<0, Object>>
void CallOnMembers(Object& object, Callable&& callable)
Expand All @@ -1400,7 +1452,7 @@ void CallOnMembersWithoutName(Object& object, Callable&& callable)
///
/// @param initialValue The initial value to fold with
/// @param callable The callable to fold with. The parameters are the member name,
/// the member's default value and the current result of the fold.
/// the member's type and the current result of the fold.
/// The current result is passed as an rvalue, so it is moved rather than copied.
///
/// @return The result of the fold
Expand Down Expand Up @@ -1555,6 +1607,7 @@ std::string Inspect(Object const& object)
return str;
}

/// Creates a human readable representation of a list of objects, one object per line.
template <typename Object>
std::string Inspect(std::vector<Object> const& objects)
{
Expand Down
40 changes: 40 additions & 0 deletions test-reflection-cpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,46 @@ struct WithReq2
NoDefaultCtor b;
};

// MSVC cannot provide member names for types without external linkage.
#if !defined(_MSC_VER) || defined(__clang__)

namespace
{
struct InternalRecord
{
int first;
std::string second;
};
} // namespace

TEST_CASE("internal linkage types", "[reflection]")
{
static_assert(Reflection::CountMembers<InternalRecord> == 2);
static_assert(Reflection::MemberNameOf<0, InternalRecord> == "first");
static_assert(Reflection::MemberNameOf<1, InternalRecord> == "second");
static_assert(Reflection::NameOf<&InternalRecord::second> == "second");
static_assert(Reflection::MemberIndexOf<&InternalRecord::second> == 1);

CHECK(Reflection::Inspect(InternalRecord { .first = 1, .second = "two" }) == R"(first=1 second="two")");
}

TEST_CASE("function local types", "[reflection]")
{
struct LocalRecord
{
int alpha;
std::string beta;
};

static_assert(Reflection::CountMembers<LocalRecord> == 2);
static_assert(Reflection::MemberNameOf<0, LocalRecord> == "alpha");
static_assert(Reflection::MemberNameOf<1, LocalRecord> == "beta");

CHECK(Reflection::Inspect(LocalRecord { .alpha = 1, .beta = "two" }) == R"(alpha=1 beta="two")");
}

#endif

TEST_CASE("MemberIndex", "[reflection]")
{
static_assert(Reflection::MemberIndexOf<&Person::name> == 0);
Expand Down
Loading