diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 382d1d0..c480d36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,20 +13,32 @@ jobs: # FIXME: We're having a linker error wrt. Catch2 on Windows with MSVC windows-msvc: - name: "Windows - MSVC" + name: "Windows - MSVC - C++${{ matrix.cxx_standard }}" runs-on: windows-latest + strategy: + fail-fast: false + matrix: + cxx_standard: [20, 23] steps: - uses: actions/checkout@v4 - name: Install dependencies run: choco install ninja + - name: Setup MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 - name: "vcpkg: Install dependencies" uses: lukka/run-vcpkg@v11.1 id: runvcpkg with: vcpkgDirectory: ${{ runner.workspace }}/vcpkg - vcpkgGitCommitId: 80403036a665cb8fcc1a1b3e17593d20b03b2489 + vcpkgGitCommitId: eb2d3a3279fd019cb7733072d86900d0ad2a1aef + # The preset names a specific Visual Studio generator; Ninja builds with whichever version the runner provides. - name: configure - run: cmake --preset windows-cl-release -D REFLECTION_TESTING=ON + run: > + cmake --preset windows-cl-release + -G Ninja + -D CMAKE_BUILD_TYPE=Release + -D REFLECTION_TESTING=ON + -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} env: VCPKG_ROOT: "${{ runner.workspace }}/vcpkg" - name: build @@ -34,13 +46,52 @@ jobs: - name: test run: ctest --preset windows-cl-release -VV + windows-clangcl: + name: "Windows - ClangCL - C++${{ matrix.cxx_standard }}" + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + cxx_standard: [20, 23] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: choco install ninja + - name: Setup MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 + - name: "vcpkg: Install dependencies" + uses: lukka/run-vcpkg@v11.1 + id: runvcpkg + with: + vcpkgDirectory: ${{ runner.workspace }}/vcpkg + vcpkgGitCommitId: eb2d3a3279fd019cb7733072d86900d0ad2a1aef + - name: configure + run: > + cmake --preset windows-clangcl-release + -D REFLECTION_TESTING=ON + -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} + -D CMAKE_TOOLCHAIN_FILE="${{ runner.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake" + env: + VCPKG_ROOT: "${{ runner.workspace }}/vcpkg" + - name: build + run: cmake --build --preset windows-clangcl-release + - name: test + run: ctest --preset windows-clangcl-release -VV + ubuntu24-clang: - name: "Ubuntu Linux - Clang ${{ matrix.clang_version }}" + name: "Ubuntu Linux - Clang ${{ matrix.clang_version }} - C++${{ matrix.cxx_standard }}" runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - clang_version: [17, 18] + clang_version: [17, 18, 19, 20, 21, 22] + cxx_standard: [20, 23, 26] + exclude: + # Clang 17 cannot compile the C++23 of the libstdc++ 14 found on Ubuntu 24.04 + - clang_version: 17 + cxx_standard: 23 + - clang_version: 17 + cxx_standard: 26 steps: - uses: actions/checkout@v4 - name: Install clang @@ -51,26 +102,83 @@ jobs: - name: Install ninja and catch2 run: sudo apt-get install ninja-build catch2 - name: configure - run: cmake --preset linux-clang-release -D REFLECTION_TESTING=ON + run: > + cmake --preset linux-clang-release + -D REFLECTION_TESTING=ON + -D CMAKE_CXX_COMPILER=clang++-${{ matrix.clang_version }} + -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} - name: build run: cmake --build --preset linux-clang-release - name: test run: ctest --preset linux-clang-release -VV ubuntu24-gcc: - name: "Ubuntu Linux - GCC ${{ matrix.gcc_version }}" + name: "Ubuntu Linux - GCC ${{ matrix.gcc_version }} - C++${{ matrix.cxx_standard }}" runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: gcc_version: [13, 14] + cxx_standard: [20, 23, 26] + exclude: + # CMake has no C++26 flag for GCC 13 + - gcc_version: 13 + cxx_standard: 26 steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo apt-get install ninja-build catch2 g++-${{ matrix.gcc_version }} - name: configure - run: cmake --preset linux-gcc-release -D REFLECTION_TESTING=ON + run: > + cmake --preset linux-gcc-release + -D REFLECTION_TESTING=ON + -D CMAKE_CXX_COMPILER=g++-${{ matrix.gcc_version }} + -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} + - name: build + run: cmake --build --preset linux-gcc-release + - name: test + run: ctest --preset linux-gcc-release -VV + + # GCC releases newer than what Ubuntu 24.04 packages, via the official GCC container images. + linux-gcc-container: + name: "Linux - GCC ${{ matrix.gcc_version }} - C++${{ matrix.cxx_standard }}" + runs-on: ubuntu-24.04 + container: gcc:${{ matrix.gcc_version }} + strategy: + fail-fast: false + matrix: + gcc_version: [15, 16] + cxx_standard: [20, 23, 26] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + apt-get update + apt-get install -y cmake ninja-build + - name: configure + run: > + cmake --preset linux-gcc-release + -D REFLECTION_TESTING=ON + -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} - name: build run: cmake --build --preset linux-gcc-release - name: test run: ctest --preset linux-gcc-release -VV + + macos: + name: "macOS - AppleClang - C++${{ matrix.cxx_standard }}" + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + cxx_standard: [20, 23] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: brew install ninja catch2 + - name: configure + run: cmake --preset macos-release -D REFLECTION_TESTING=ON -D CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }} + - name: build + run: cmake --build --preset macos-release + - name: test + run: ctest --preset macos-release -VV diff --git a/CMakePresets.json b/CMakePresets.json index a8b8edc..eef93a1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -83,6 +83,8 @@ { "name": "linux-gcc-debug", "configurePreset": "linux-gcc-debug", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, { "name": "linux-gcc-release", "configurePreset": "linux-gcc-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, { "name": "linux-clang-debug", "configurePreset": "linux-clang-debug", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, - { "name": "linux-clang-release", "configurePreset": "linux-clang-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } } + { "name": "linux-clang-release", "configurePreset": "linux-clang-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, + { "name": "macos-debug", "configurePreset": "macos-debug", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } }, + { "name": "macos-release", "configurePreset": "macos-release", "output": {"outputOnFailure": true}, "execution": { "noTestsAction": "error", "stopOnFailure": true } } ] } diff --git a/include/reflection-cpp/reflection.hpp b/include/reflection-cpp/reflection.hpp index 0b7c857..e32f2b8 100644 --- a/include/reflection-cpp/reflection.hpp +++ b/include/reflection-cpp/reflection.hpp @@ -121,17 +121,90 @@ namespace detail // return std::source_location::current().function_name(); return REFLECTION_PRETTY_FUNCTION; } +} // namespace detail + +constexpr size_t MaxReflectionMemerCount = 150; + +namespace detail +{ +#if defined(__cpp_structured_bindings) && __cpp_structured_bindings >= 202411L + // A structured binding pack yields the member count directly, at constant compile-time cost. + // + // Clang provides structured binding packs as an extension before C++26, and warns about their use there. + #if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wc++26-extensions" + #endif + template + auto CountMembersImpl(T& t) + { + [[maybe_unused]] auto& [... members] = t; + return std::integral_constant {}; + } + #if defined(__clang__) + #pragma clang diagnostic pop + #endif - template - requires(std::is_aggregate_v) - constexpr inline auto CountMembers = []() constexpr { + template + constexpr inline size_t CountMembers = decltype(CountMembersImpl(std::declval()))::value; +#else + template + consteval bool AcceptsInitializersImpl(std::index_sequence /*unused*/) + { // NOLINTNEXTLINE(modernize-use-designated-initializers) - if constexpr (requires { AggregateType { Args {}..., AnyType {} }; }) - return CountMembers; + return requires { T { (static_cast(I), AnyType {})... }; }; + } + + // Whether the aggregate T can be initialized from exactly N values. + // + // With N being the member count and F the position of the last member without a default constructor + // (0 if there is none), exactly the counts in [F, N] are accepted. + template + constexpr inline bool AcceptsInitializers = AcceptsInitializersImpl(std::make_index_sequence {}); + + // Finds the smallest accepted initializer count, searching linearly from N. + template + consteval size_t FirstAcceptedInitializerCount() + { + if constexpr (AcceptsInitializers) + return N; + else if constexpr (N < MaxReflectionMemerCount) + return FirstAcceptedInitializerCount(); + else + { + static_assert(N < MaxReflectionMemerCount, + "Reflection: unable to count the members of this aggregate type. It either has more " + "than MaxReflectionMemerCount (150) members, or it has a member that cannot be " + "initialized from a single value (e.g. a reference member)."); + return 0; + } + } + + // Finds the largest accepted initializer count by binary search, given that Low is accepted and High is not. + template + consteval size_t LastAcceptedInitializerCount() + { + if constexpr (High - Low == 1) + return Low; + else if constexpr (AcceptsInitializers) + return LastAcceptedInitializerCount(); + else + return LastAcceptedInitializerCount(); + } + + // Given that Low is accepted, doubles Step until a rejected count is found, then bisects that range. + template + consteval size_t CountMembersFrom() + { + if constexpr (AcceptsInitializers) + return CountMembersFrom(); else - return sizeof...(Args); - }(); + return LastAcceptedInitializerCount(); + } + template + constexpr inline size_t CountMembers = CountMembersFrom()>(); +#endif } // namespace detail // Count the number of members in an aggregate type. @@ -139,8 +212,6 @@ template requires(std::is_aggregate_v>) constexpr inline auto CountMembers = detail::CountMembers>; -constexpr size_t MaxReflectionMemerCount = 150; - /** elisp functions to fill the ToTuple function @@ -1273,7 +1344,7 @@ void CallOnMembers(Object& object, Callable&& callable) template void CallOnMembersWithoutName(Object& object, Callable&& callable) { - EnumerateMembers(object, [&](T&& value) { callable.template operator()(value); }); + EnumerateMembers(object, [&](T&& value) { callable.template operator()(value); }); } /// Folds over the members of a type without an object of it. diff --git a/test-reflection-cpp.cpp b/test-reflection-cpp.cpp index 58e1ccd..2d42942 100644 --- a/test-reflection-cpp.cpp +++ b/test-reflection-cpp.cpp @@ -35,6 +35,25 @@ struct SingleValueRecord int value; }; +struct NoDefaultCtor +{ + NoDefaultCtor() = delete; + constexpr NoDefaultCtor(int) {} +}; + +struct WithReq +{ + int a; + NoDefaultCtor b; +}; + +struct WithReq2 +{ + int a; + int _a; + NoDefaultCtor b; +}; + TEST_CASE("MemberIndex", "[reflection]") { static_assert(Reflection::MemberIndexOf<&Person::name> == 0); @@ -64,6 +83,41 @@ TEST_CASE("NameOf", "[reflection]") CHECK(singleValueField == "value"); } +struct NoDefaultCtorFirst +{ + NoDefaultCtor a; + int b; +}; + +struct NoDefaultCtorMiddle +{ + int a; + NoDefaultCtor b; + int c; + std::string d; +}; + +struct NoDefaultCtorOnly +{ + NoDefaultCtor a; + NoDefaultCtor b; +}; + +TEST_CASE("CountMembers.NoDefaultCtor", "[reflection]") +{ + static_assert(Reflection::CountMembers == 2); + static_assert(Reflection::CountMembers == 3); + static_assert(Reflection::CountMembers == 2); + static_assert(Reflection::CountMembers == 4); + static_assert(Reflection::CountMembers == 2); + + auto record = NoDefaultCtorMiddle { .a = 1, .b = NoDefaultCtor { 2 }, .c = 3, .d = "four" }; + auto names = std::vector {}; + Reflection::EnumerateMembers( + record, [&](T&&) { names.push_back(Reflection::MemberNameOf); }); + CHECK(names == std::vector { "a", "b", "c", "d" }); +} + TEST_CASE("single value record", "[reflection]") { static_assert(Reflection::CountMembers == 1); @@ -78,7 +132,6 @@ TEST_CASE("single value record", "[reflection]") CHECK(name == "value"); CHECK(value == 42); }); - } TEST_CASE("core", "[reflection]") @@ -170,7 +223,6 @@ TEST_CASE("EnumerateMembers.partial", "[reflection]") }); } - TEST_CASE("CallOnMembers", "[reflection]") { auto ps = Person { .name = "John Doe", .email = "john@doe.com", .age = 42 }; @@ -292,6 +344,6 @@ TEST_CASE("Compare.nested", "[reflection]") TEST_CASE("TemplateFor over sequence", "[refleciton]") { std::string result {}; - Reflection::template_for>([&](){result += std::to_string(I);}); + Reflection::template_for>([&]() { result += std::to_string(I); }); CHECK(result == "321"); } diff --git a/vcpkg.json b/vcpkg.json index 4e1a2bb..b8efc67 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", - "builtin-baseline": "80403036a665cb8fcc1a1b3e17593d20b03b2489", + "builtin-baseline": "eb2d3a3279fd019cb7733072d86900d0ad2a1aef", "dependencies": [ { "name": "catch2", "version>=": "3.4.0" } ]