diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index bcad3fd..e66776d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -211,6 +211,18 @@ jobs: std: 20, }, } + - { + name: "Ubuntu GCC-16 (C++26 Reflection)", + os: ubuntu-24.04, + compiler: + { + type: GCC16, + version: 16, + cc: "gcc-16", + cxx: "g++-16", + std: 26, + }, + } steps: - uses: actions/checkout@v4 - uses: seanmiddleditch/gha-setup-ninja@master @@ -231,9 +243,15 @@ jobs: with: version: ${{ matrix.settings.compiler.version }} platform: x64 + - name: Install GCC 16 from ubuntu-toolchain-r PPA + if: matrix.settings.compiler.type == 'GCC16' + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y --no-install-recommends g++-16 gcc-16 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python run: uv sync - name: Run CMake - run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} + run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} ${{ matrix.settings.compiler.type == 'GCC16' && '--reflection' || '' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0626252..772266e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,34 @@ option(ENABLE_UBSAN "Enable Undefined Behaviour Sanitizer" OFF) option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) option(ENABLE_MSAN "Enable Memory Sanitizer" OFF) +option( + XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL + "Also build and test tutorials/3_reflection.cc, which requires a compiler \ +with C++26 P2996 reflection support (GCC 16+ with -freflection)." + OFF) + +if(XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS "-std=c++26 -freflection") + check_cxx_source_compiles( + " + #include + constexpr std::meta::info reflection_of_int = ^^int; + int main() {} + " + XYZ_PROTOCOL_REFLECTION_SUPPORTED) + unset(CMAKE_REQUIRED_FLAGS) + if(NOT XYZ_PROTOCOL_REFLECTION_SUPPORTED) + message( + FATAL_ERROR + "XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL is ON but the compiler " + "(${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}) does not " + "accept '-std=c++26 -freflection'. C++26 reflection currently " + "requires GCC 16 or newer; configure with e.g. " + "CXX=g++-16 CC=gcc-16 and a separate build directory (-B).") + endif() +endif() + if(ENABLE_ASAN OR ENABLE_UBSAN OR ENABLE_TSAN OR ENABLE_MSAN) set(ENABLE_SANITIZERS ON) endif() @@ -148,6 +176,31 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) target_include_directories(protocol_test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + # Standalone teaching tutorials (tutorials/) -- see tutorials/README.md. + # Each is self-contained, with no dependency on protocol.h. + xyz_add_test( + NAME + type_erasure_tutorial + FILES + tutorials/1_type_erasure.cc) + + xyz_add_test( + NAME + vanishing_this_pointer_tutorial + FILES + tutorials/2_vanishing_this_pointer.cc) + + if(XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL) + xyz_add_test( + NAME + reflection_tutorial + VERSION + 26 + FILES + tutorials/3_reflection.cc) + target_compile_options(reflection_tutorial PRIVATE -freflection) + endif() + add_executable(protocol_benchmark protocol_benchmark.cc) target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark) add_dependencies(protocol_benchmark generate_protocols) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e77735..0a936be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,6 +144,20 @@ This library is an active proof of concept and is subject to change. - Development: Use this library for understanding its concepts and contributing to its development. Avoid using it in production code. +## Interactive Docker Shell + +To launch an interactive bash shell in the pre-configured Docker container (which includes GCC 16, Python, CMake, and `uv`) without running an AI agent, use: + +```bash +./scripts/docker-shell.sh [--rebuild-docker] +``` + +Inside the shell, GCC 16 is what lets you build and run the `tutorials/3_reflection.cc` tutorial, which needs C++26 reflection support: + +```bash +./scripts/cmake.sh --release --reflection -B build-reflection +``` + ## AI Coding Sandboxes The repository includes a Docker-based sandbox script for AI coding diff --git a/GEMINI.md b/GEMINI.md index 5e03711..6b4f6e2 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -23,7 +23,8 @@ general defaults for this repository. - **Tooling:** Always use `uv` for Python dependency management (`uv run ...`). - **Build & Test:** Use `scripts/cmake.sh` for all build and test operations. The `scripts/cmake.sh` entrypoint supports `--debug`, `--release`, - `--asan`, `--ubsan`, `--tsan`, and `--msan`. + `--asan`, `--ubsan`, `--tsan`, `--msan`, and `--reflection` (builds and + tests `tutorials/3_reflection.cc`, requires a P2996 reflection compiler). - **Compiler Preferences:** Prefer Clang 19+ for sanitizer-based verification and CI, as it provides superior support for MSAN and TSAN compared to older GCC versions. diff --git a/cmake/xyz_add_test.cmake b/cmake/xyz_add_test.cmake index 00cf265..ff9f4a6 100644 --- a/cmake/xyz_add_test.cmake +++ b/cmake/xyz_add_test.cmake @@ -50,7 +50,7 @@ function(xyz_add_test) if(NOT XYZ_VERSION) set(XYZ_VERSION 20) else() - set(VALID_TARGET_VERSIONS 11 14 17 20 23) + set(VALID_TARGET_VERSIONS 11 14 17 20 23 26) list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index) if(index EQUAL -1) message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>") diff --git a/docker/Dockerfile b/docker/Dockerfile index 4966bbb..7b89f3a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,13 @@ RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/nul && apt-get update && apt-get install -y --no-install-recommends cmake \ && rm -rf /var/lib/apt/lists/* +# Install GCC 16 (experimental C++26 reflection support via -std=c++26 +# -freflection, see https://gcc.gnu.org/gcc-16/changes.html) from the Ubuntu +# Toolchain Test PPA, since Ubuntu 24.04's own repos only go up to GCC 14. +RUN add-apt-repository -y ppa:ubuntu-toolchain-r/test \ +&& apt-get update && apt-get install -y --no-install-recommends g++-16 gcc-16 \ +&& rm -rf /var/lib/apt/lists/* + # Install bazelisk. RUN ARCH=$(dpkg --print-architecture) && \ wget https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-${ARCH} -O /usr/local/bin/bazelisk \ diff --git a/scripts/cmake.py b/scripts/cmake.py index ecfca13..d17f7c5 100644 --- a/scripts/cmake.py +++ b/scripts/cmake.py @@ -2,6 +2,7 @@ """CMake helper script for building and testing the project.""" import argparse +import os import subprocess from typing import Any @@ -37,6 +38,12 @@ def main() -> None: ) parser.add_argument("--tsan", action="store_true", help="Enable Thread Sanitizer") parser.add_argument("--msan", action="store_true", help="Enable Memory Sanitizer") + parser.add_argument( + "--reflection", + action="store_true", + help="Build and test tutorials/3_reflection.cc (requires a P2996 " + "reflection compiler, e.g. CXX=g++-16 CC=gcc-16)", + ) parser.add_argument("-B", "--build-dir", help="Build directory") parser.add_argument( "--clean", action="store_true", help="Fresh configuration and clean-first build" @@ -74,6 +81,8 @@ def log(msg: Any) -> None: f"-DENABLE_UBSAN={'ON' if args.ubsan else 'OFF'}", f"-DENABLE_TSAN={'ON' if args.tsan else 'OFF'}", f"-DENABLE_MSAN={'ON' if args.msan else 'OFF'}", + "-DXYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL=" + + ("ON" if args.reflection else "OFF"), ] if args.build_dir: configure_args.extend(["-B", args.build_dir]) @@ -82,8 +91,17 @@ def log(msg: Any) -> None: configure_args.extend(extra) + # A P2996 reflection compiler is required to configure with + # XYZ_PROTOCOL_BUILD_REFLECTION_TUTORIAL=ON. CMake only reads CXX/CC + # from the environment, not from -D cache variables, so set them here + # rather than as configure_args. + configure_env = os.environ.copy() + if args.reflection: + configure_env["CXX"] = "g++-16" + configure_env["CC"] = "gcc-16" + log(f"Running: {' '.join(configure_args)}") - subprocess.check_call(configure_args) + subprocess.check_call(configure_args, env=configure_env) # Build step (required for build, test, benchmark) build_args = ["cmake", "--build"] diff --git a/scripts/docker-shell.sh b/scripts/docker-shell.sh new file mode 100755 index 0000000..cbe1b88 --- /dev/null +++ b/scripts/docker-shell.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -eu -o pipefail + +WORKSPACE_ROOT=$(git rev-parse --show-toplevel) +IMAGE_NAME="cc-protocol-sandbox" + +REBUILD=0 +for argument in "$@"; do + if [ "$argument" == "--rebuild-docker" ]; then + REBUILD=1 + fi +done + +if [ "$REBUILD" -eq 1 ] || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + echo "--- Building Docker Image: ${IMAGE_NAME} ---" + docker build -t "$IMAGE_NAME" -f "${WORKSPACE_ROOT}/docker/Dockerfile" "$WORKSPACE_ROOT" +fi + +echo "--- Starting Interactive Docker Shell ---" +echo "Project root ${WORKSPACE_ROOT} is mounted at /workspace" +echo "" +echo "To build and test with the C++26 reflection backend (GCC 16):" +echo " ./scripts/cmake.sh --release --reflection -B build-reflection" +echo "" + +exec docker run -it --rm \ + -v "${WORKSPACE_ROOT}:/workspace" \ + -w /workspace \ + "$IMAGE_NAME" bash diff --git a/tutorials/1_type_erasure.cc b/tutorials/1_type_erasure.cc new file mode 100644 index 0000000..5b8019f --- /dev/null +++ b/tutorials/1_type_erasure.cc @@ -0,0 +1,210 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ + +// Type erasure, from first principles: +// 1. The problem type erasure solves. +// 2. A vtable of function pointers. +// 3. The eraser object. + +#include + +#include +#include +#include + +// --------------------------------------------------------------------------- +// 1. The problem. +// +// A function template can call draw() polymorphically: it works for any T +// with a draw() method. Each instantiation, though, is a distinct function. +// render and render are two different functions with two +// different addresses; there is no single type you could use to store +// "either a Circle or a Square, decided at runtime" and call draw() on it. +// The rest of this file builds one: type erasure. +// --------------------------------------------------------------------------- +namespace section_1 { + +class Circle { + public: + std::string draw() const { return "○"; } +}; + +class Square { + public: + std::string draw() const { return "□"; } +}; + +template +std::string render(const Shape& shape) { + return shape.draw(); +} + +TEST(TypeErasureProblem, TemplatesArentUniform) { + Circle circle; + Square square; + EXPECT_EQ(render(circle), "○"); + EXPECT_EQ(render(square), "□"); + + // render and render are different types once + // instantiated, so no single function pointer type could point at both. + using RenderCircle = std::string (*)(const Circle&); + using RenderSquare = std::string (*)(const Square&); + static_assert(!std::is_same_v); +} + +} // namespace section_1 + +// --------------------------------------------------------------------------- +// 2. A vtable of function pointers. +// +// Circle and Square each get called through a small static object, a +// "vtable", built from exactly the same struct type. The two casts below +// (erasing to const void* at the call site, un-erasing back inside the +// function) are the cast-and-call pattern type erasure relies on. +// --------------------------------------------------------------------------- +namespace section_2 { + +class Circle { + public: + std::string draw() const { return "○"; } +}; + +class Square { + public: + std::string draw() const { return "□"; } +}; + +// One function pointer, one method. A real interface with several methods +// would just repeat this shape once per method it needs to expose. +struct draw_vtable { + std::string (*draw)(const void* erased); +}; + +// One static instance per concrete type. Each is initialized with a +// function that does the two casts: the incoming pointer arrives already +// erased to const void* (the caller did that), and this function un-erases +// it back to the one concrete type it actually knows how to handle, then +// calls draw() on it normally. +template +inline constexpr draw_vtable vtable_for = { + .draw = [](const void* erased) -> std::string { + return static_cast(erased)->draw(); + }}; + +TEST(TypeErasureVtable, SameVtableDifferentTypes) { + Circle circle; + Square square; + + // Two different concrete objects are called through the same vtable + // type (draw_vtable) via the same cast-and-call pattern; only the + // static instance and the erased pointer differ. + EXPECT_EQ(vtable_for.draw(&circle), "○"); + EXPECT_EQ(vtable_for.draw(&square), "□"); + + // Both static vtable instances share one C++ type, unlike the two + // distinct render instantiations from section 1. + static_assert(std::is_same_v), + decltype(vtable_for)>); +} + +} // namespace section_2 + +// --------------------------------------------------------------------------- +// 3. The eraser object. +// +// Package section 2's erased pointer and vtable pointer into one small +// class, with a constructor template that captures whatever concrete type +// it's given. Circle and Square can both be stored behind the result, and both +// go in one std::vector. This is the minimal, one-method analogue of +// xyz::protocol_view: a non-owning, type-erased handle with forwarding +// methods that dispatch through the vtable. +// --------------------------------------------------------------------------- +namespace section_3 { + +class Circle { + public: + std::string draw() const { return "○"; } +}; + +class Square { + public: + std::string draw() const { return "□"; } +}; + +struct draw_vtable { + std::string (*draw)(const void* erased); +}; + +template +inline constexpr draw_vtable vtable_for = { + .draw = [](const void* erased) -> std::string { + return static_cast(erased)->draw(); + }}; + +// erased_shape only stores a pointer to a Shape it doesn't own. The +// concrete object (the Circle or Square passed to the constructor) must +// outlive every erased_shape that refers to it. +class erased_shape { + public: + template + explicit erased_shape(const Shape& shape) + : erased_(&shape), vtable_(&vtable_for) {} + + std::string draw() const { return vtable_->draw(erased_); } + + private: + const void* erased_; + const draw_vtable* vtable_; +}; + +TEST(TypeErasureEraser, UnifiesUnrelatedTypes) { + Circle circle; + Square square; + + erased_shape erased_circle(circle); + erased_shape erased_square(square); + + EXPECT_EQ(erased_circle.draw(), "○"); + EXPECT_EQ(erased_square.draw(), "□"); + + // Both are the same C++ type, unlike Circle and Square themselves. + static_assert( + std::is_same_v); +} + +TEST(TypeErasureEraser, BothTypesInOneVector) { + Circle circle; + Square square; + + // One vector holds both a Circle and a Square, dispatched uniformly; + // neither class was touched or related to the other. + std::vector shapes; + shapes.emplace_back(circle); + shapes.emplace_back(square); + + std::vector drawn; + for (const erased_shape& shape : shapes) { + drawn.push_back(shape.draw()); + } + + EXPECT_EQ(drawn, (std::vector{"○", "□"})); +} + +} // namespace section_3 diff --git a/tutorials/2_vanishing_this_pointer.cc b/tutorials/2_vanishing_this_pointer.cc new file mode 100644 index 0000000..020f47a --- /dev/null +++ b/tutorials/2_vanishing_this_pointer.cc @@ -0,0 +1,300 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ + +// The vanishing `this` pointer. +// +// This technique is adopted from https://ryanjk5.github.io/posts/rjk-duck/. +// +// A technique for giving a data member ordinary call syntax: +// 1. The offset-zero layout guarantee. +// 2. Recovering the owner pointer. +// 3. Ordinary call syntax from a data member. +// 4. What breaks when offset zero is violated, and the guard against it. +// 5. Composing several such members through inheritance. + +#include + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 1. The offset-zero layout guarantee. +// +// A class's first non-static data member starts at the same address as the +// object that contains it. This is called offset zero. +// --------------------------------------------------------------------------- +namespace section_1 { + +struct Wrapper { + int tag = 0; +}; + +struct Owner { + Wrapper wrapper; // first data member + int other_data = 0; +}; + +TEST(ThisPointerOffsetZero, FirstMemberSharesAddress) { + Owner owner; + + EXPECT_EQ(static_cast(&owner), static_cast(&owner.wrapper)); + EXPECT_EQ(offsetof(Owner, wrapper), 0u); + + // other_data is not first, so it does not share the owner's address. + EXPECT_NE(static_cast(&owner), static_cast(&owner.other_data)); + EXPECT_NE(offsetof(Owner, other_data), 0u); +} + +// An empty Wrapper, stored as a data member rather than inherited from, +// still costs no extra storage: [[no_unique_address]] is the data-member +// counterpart of the empty base optimization, and lets an empty member +// overlap entirely with the owner's own storage. +struct EmptyWrapper {}; + +struct OwnerWithEmptyWrapper { + [[no_unique_address]] EmptyWrapper wrapper; + int payload = 0; +}; + +TEST(ThisPointerOffsetZero, EmptyWrapperAddsNoStorage) { + EXPECT_EQ(sizeof(OwnerWithEmptyWrapper), sizeof(int)); + EXPECT_EQ(offsetof(OwnerWithEmptyWrapper, wrapper), 0u); +} + +} // namespace section_1 + +// --------------------------------------------------------------------------- +// 2. Recovering the owner pointer, "the vanishing this pointer" itself. +// +// Because a Wrapper sitting at offset zero of an Owner shares the Owner's +// address (section 1), a method defined on Wrapper can compute a pointer to +// its owner just by casting its own `this`, without Wrapper storing a +// back-pointer anywhere. This is undefined behavior unless Wrapper is the first +// data member of Owner, and Owner is a standard layout type. +// --------------------------------------------------------------------------- +namespace section_2 { + +struct Wrapper { + int recover_owner_value() const; +}; + +struct Owner { + Wrapper wrapper; + int value = 0; +}; + +int Wrapper::recover_owner_value() const { + const auto* owner = static_cast(static_cast(this)); + return owner->value; +} + +TEST(ThisPointerRecoverOwner, ReachesOwnersData) { + Owner owner; + owner.value = 42; + + EXPECT_EQ(owner.wrapper.recover_owner_value(), 42); +} + +TEST(ThisPointerRecoverOwner, NoExtraStorage) { + // sizeof(Wrapper) == 1 (the minimum for any object, empty or not), not 8 + // (a pointer's worth): confirms no back-pointer is stored. + EXPECT_EQ(sizeof(Wrapper), 1u); +} + +} // namespace section_2 + +// --------------------------------------------------------------------------- +// 3. Ordinary call syntax from a data member. +// +// Give Wrapper an operator() instead of an oddly-named method, make it a +// data member of Owner instead of a method, and owner.member_name(...) +// becomes an ordinary function call, even though member_name is a value of +// class type rather than a function. Combined with section 2's owner +// recovery, that call can reach and mutate the real owner. +// --------------------------------------------------------------------------- +namespace section_3 { + +struct GreetWrapper { + std::string operator()(std::string_view visitor) const; // defined below +}; + +struct Owner { + [[no_unique_address]] GreetWrapper greet; + std::string name = "World"; +}; + +std::string GreetWrapper::operator()(std::string_view visitor) const { + const auto* owner = static_cast(static_cast(this)); + return "Hello, " + std::string(visitor) + ", from " + owner->name + "!"; +} + +TEST(ThisPointerCallSyntax, BehavesAsOrdinaryCall) { + Owner owner; + owner.name = "Owner"; + + EXPECT_EQ(owner.greet("Reader"), "Hello, Reader, from Owner!"); +} + +TEST(ThisPointerCallSyntax, IsADataMemberNotMethod) { + Owner owner; + + // owner.greet() calls that member's operator(). + static_assert(std::is_class_v); + static_assert(!std::is_function_v); +} + +} // namespace section_3 + +// --------------------------------------------------------------------------- +// 4. What breaks when offset zero is violated, and the guard against it. +// +// Everything in sections 2-3 depends on Wrapper being Owner's first data +// member. This section shows what happens if it isn't: the address a +// Wrapper method would recover as "the owner" no longer matches the real +// owner's address, off by exactly sizeof(preceding_member): enough to land +// on preceding_member's storage instead of Owner's real first member. +// Every check below computes and compares addresses only; none +// dereferences the miscomputed pointer, since actually reading through it +// would be undefined behavior. +// --------------------------------------------------------------------------- +namespace section_4 { + +struct Wrapper { + int tag = 0; +}; + +// BadOwner puts another member before wrapper, so wrapper is no longer at +// offset zero. +struct BadOwner { + int preceding_member = 0; + [[no_unique_address]] Wrapper wrapper; +}; + +TEST(ThisPointerOffsetViolated, AddressNoLongerMatches) { + BadOwner owner; + + EXPECT_NE(static_cast(&owner), static_cast(&owner.wrapper)); + EXPECT_NE(offsetof(BadOwner, wrapper), 0u); +} + +TEST(ThisPointerOffsetViolated, MisrecoveredAddressLandsOnPrecedingMember) { + BadOwner owner; + + // wrong_owner is the address a Wrapper method would recover as "the + // owner." Only the distance to real_owner is computed; that address is + // never dereferenced. + const auto* wrong_owner = + static_cast(static_cast(&owner.wrapper)); + const auto* real_owner = + static_cast(static_cast(&owner)); + EXPECT_EQ(wrong_owner - real_owner, + static_cast(sizeof(owner.preceding_member))); +} + +// GoodOwner asserts its layout precondition instead of assuming it: a +// static_assert here fails at compile time if a future change moves wrapper +// away from being GoodOwner's first member. +struct GoodOwner { + Wrapper wrapper; + int payload = 0; +}; + +static_assert(offsetof(GoodOwner, wrapper) == 0, + "wrapper must be GoodOwner's first data member for the " + "vanishing-this-pointer cast to be valid"); + +TEST(ThisPointerOffsetViolated, GuardConfirmsGoodCase) { + EXPECT_EQ(offsetof(GoodOwner, wrapper), 0u); +} + +} // namespace section_4 + +// --------------------------------------------------------------------------- +// 5. Composing several such members through inheritance. +// +// Owner in section 3 has one wrapper data member, greet. Giving it a +// second, shout, the same way needs each in its own small base struct, +// GreetBase and ShoutBase below, combined by having Owner inherit from +// both. +// +// Each wrapper's own this only has to recover its own base's address: +// GreetWrapper is GreetBase's sole member, so it sits at offset zero of +// GreetBase unconditionally, exactly as section 2's Wrapper sits at +// offset zero of Owner. Getting from GreetBase's address to Owner's is +// then a base-to-derived static_cast, which the compiler performs +// correctly regardless of GreetBase's actual offset within Owner. +// [[no_unique_address]] on greet and shout still matters for size, same +// as sections 1 and 4, but no longer for correctness: with flat data +// members, an unhonored [[no_unique_address]] misplaces every member +// after the first (section 4's failure mode); here it only costs Owner +// some otherwise-unnecessary bytes. This is the technique Duck (cited +// above) uses to build its vtable_wrapper, one base per interface member. +// --------------------------------------------------------------------------- +namespace section_5 { + +struct GreetWrapper { + std::string operator()(std::string_view visitor) const; +}; + +struct GreetBase { + [[no_unique_address]] GreetWrapper greet; +}; + +struct ShoutWrapper { + std::string operator()(std::string_view visitor) const; +}; + +struct ShoutBase { + [[no_unique_address]] ShoutWrapper shout; +}; + +struct Owner : GreetBase, ShoutBase { + std::string name = "World"; +}; + +std::string GreetWrapper::operator()(std::string_view visitor) const { + const auto* base = + static_cast(static_cast(this)); + const auto* owner = static_cast(base); + return "Hello, " + std::string(visitor) + ", from " + owner->name + "!"; +} + +std::string ShoutWrapper::operator()(std::string_view visitor) const { + const auto* base = + static_cast(static_cast(this)); + const auto* owner = static_cast(base); + return std::string(visitor) + ", " + owner->name + " SHOUTS HELLO!"; +} + +TEST(ThisPointerInheritance, EachMemberReachesTheSameOwner) { + Owner owner; + owner.name = "Owner"; + + EXPECT_EQ(owner.greet("Reader"), "Hello, Reader, from Owner!"); + EXPECT_EQ(owner.shout("Reader"), "Reader, Owner SHOUTS HELLO!"); +} + +TEST(ThisPointerInheritance, NoExtraStorage) { + EXPECT_EQ(sizeof(Owner), sizeof(std::string)); +} + +} // namespace section_5 diff --git a/tutorials/3_reflection.cc b/tutorials/3_reflection.cc new file mode 100644 index 0000000..963d3ad --- /dev/null +++ b/tutorials/3_reflection.cc @@ -0,0 +1,661 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ + +// C++26 reflection, from std::meta::info to synthesizing a type at compile +// time: +// 1. What std::meta::info is. +// 2. Splicing an info back into a type. +// 3. Splicing an info as a call target. +// 4. Enumerating a type's members. +// 5. Writing your own consteval helpers about a member. +// 6. Synthesizing a type's data members with define_aggregate. +// 7. Converting an enum value to its name with template for. +// 8. Instantiating a template from infos with substitute. +// 9. Reading a constant out of an info with extract. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 1. What std::meta::info is. +// +// C++26 reflection (P2996) lets code ask questions about the program at +// compile time, such as what a type's members are or whether a function is +// const, and so on. The reflection operator ^^ takes something in the program +// (a type, a variable, a function, a member...) and lifts it into a +// std::meta::info value: an opaque, compile-time value describing that +// thing, distinct from the thing itself (^^int is a value you can pass +// around, compare, and query; it is not int). This section only +// establishes that fact; turning an info back into usable code +// ("splicing") is sections 2 and 3. +// --------------------------------------------------------------------------- +namespace section_1 { + +struct Point { + int x = 0; + int y = 0; +}; + +TEST(ReflectionMetaInfo, SameEntityEqualValues) { + constexpr std::meta::info reflection_of_int = ^^int; + + // ^^int, evaluated twice, describes the same entity both times. + static_assert(reflection_of_int == ^^int); + + // Different entities reflect to different, unequal info values. + static_assert(^^int != ^^double); + static_assert(^^Point != ^^int); + + // std::meta::info values are ordinary compile-time values: they can be + // stored in constexpr variables and compared with ==, exactly like any + // other type in this respect. +} + +using AliasForPoint = Point; + +TEST(ReflectionMetaInfo, AliasesCompareDistinctUntilDealiased) { + // ^^ reflects the name as written, not what it eventually names: an + // alias and its underlying type are different entities to ^^, even + // though AliasForPoint is nothing more than another name for Point. + static_assert(^^Point != ^^AliasForPoint); + + // std::meta::dealias strips away any aliasing, giving back the info + // you'd get by reflecting the underlying type directly. + static_assert(dealias(^^AliasForPoint) == ^^Point); +} + +// ^^ isn't limited to types. It can also reflect a namespace, a function, a +// specific member, and more. This tutorial mostly needs "reflect a type" and +// "reflect a member," both shown here just to establish that both work with +// the same ^^ operator; later sections do more with each. +int free_function() { return 1; } + +TEST(ReflectionMetaInfo, OtherEntityKinds) { + constexpr std::meta::info reflected_namespace = ^^std; + constexpr std::meta::info reflected_function = ^^free_function; + constexpr std::meta::info reflected_member = ^^Point::x; + + // Each ^^ result carries its own entity kind, queryable with a predicate + // like std::meta::is_namespace. What you can do with each (splice it as a + // type, call it, read its name) depends on this kind, covered starting in + // section 2. + static_assert(is_namespace(reflected_namespace)); + static_assert(is_function(reflected_function)); + static_assert(is_nonstatic_data_member(reflected_member)); +} + +} // namespace section_1 + +// --------------------------------------------------------------------------- +// 2. Splicing an info back into a type. +// +// Section 1 lifted a type into an opaque std::meta::info with ^^. This +// section lowers it back: typename [:some_info:] names the type that info +// reflects, wherever a type is expected: a variable declaration, a +// template argument, anywhere. typename is required because [:some_info:] +// alone is an expression (the form section 3 uses to splice a value); +// nothing about info's own type says whether it reflects a type or a +// value, so the parser needs typename to pick the type-naming parse. This +// round trip (^^ then [: :]) is called "splicing," and it's how reflection +// turns a compile-time value describing a type back into an ordinary, +// usable type. +// --------------------------------------------------------------------------- +namespace section_2 { + +struct Point { + int x = 0; + int y = 0; +}; + +TEST(ReflectionSpliceType, RoundTripsToOriginal) { + constexpr std::meta::info reflection_of_int = ^^int; + + // typename [:reflection_of_int:] names int again, so this line declares an + // ordinary int variable, just spelled through a splice. + typename[:reflection_of_int:] x = 5; + EXPECT_EQ(x, 5); + + // Splicing a struct's reflection produces the exact same type, not merely + // a look-alike: std::is_same_v confirms it. + static_assert(std::is_same_v); +} + +// A spliced type is usable everywhere an ordinary type name would be, +// including as a template argument. +template +consteval bool is_class_type() { + return std::is_class_v; +} + +TEST(ReflectionSpliceType, WorksAsTemplateArgument) { + // Storing each info before splicing it, rather than splicing ^^Point + // itself, stands in for the ordinary case: the info comes from somewhere + // else (a parameter, a search result, ...) and only gets spliced at its + // point of use. + constexpr std::meta::info point_info = ^^Point; + constexpr std::meta::info int_info = ^^int; + + static_assert(is_class_type()); + static_assert(!is_class_type()); +} + +} // namespace section_2 + +// --------------------------------------------------------------------------- +// 3. Splicing an info as a call target. +// +// find_member, below, searches a type's members at compile time and +// returns the one it finds, as a std::meta::info identifying that member. +// +// A splice can stand in for a member's name in ordinary member access: +// greeter.[:member_info:] accesses whichever member member_info reflects, +// exactly as writing that member's name after the dot would. Appending +// (args) calls it, if that member is a member function; used on its own, +// with no call, it reads or writes it, if that member is a data member. +// --------------------------------------------------------------------------- +namespace section_3 { + +struct Greeter { + std::string greet(std::string_view name) const { + return "Hello, " + std::string(name); + } + + int value = 0; +}; + +// members_of takes an access_context controlling which members it is +// allowed to see. access_context::current() applies ordinary C++ access +// rules as they hold at this point in the source (a member or friend +// function sees private members here; free-standing code like find_member +// does not, so private members stay invisible to it, just as they would to +// ordinary code written at this call site). access_context::unprivileged() +// always restricts the query to public members, however privileged the +// calling code actually is, useful for reflecting on a type the way an +// arbitrary external caller would see it. access_context::unchecked() +// removes access checking entirely, useful for tooling that must reach +// every member regardless of access, such as a serializer. +consteval std::meta::info find_member(std::meta::info type, + std::string_view name) { + auto const members = members_of(type, std::meta::access_context::current()); + auto const is_member = [&](std::meta::info member) { + return has_identifier(member) && identifier_of(member) == name; + }; + + if (auto const member = std::ranges::find_if(members, is_member); + member != members.end()) { + return *member; + } + // A default-constructed std::meta::info is a valid, comparable sentinel value + // that specifically means "nothing here," rather than describing any real + // type, function, variable, or namespace. + return {}; +} + +TEST(ReflectionSpliceCall, MemberFunctionAsTarget) { + Greeter greeter; + constexpr std::meta::info greet_member = find_member(^^Greeter, "greet"); + + // greeter.[:greet_member:](...) calls greet(), found by reflection, + // not by writing its name at the call site. + EXPECT_EQ(greeter.[:greet_member:]("Reader"), "Hello, Reader"); + EXPECT_EQ(greeter.[:greet_member:]("Reader"), greeter.greet("Reader")); +} + +TEST(ReflectionSpliceCall, DataMemberAccess) { + Greeter greeter; + constexpr std::meta::info value_member = find_member(^^Greeter, "value"); + + greeter.[:value_member:] = 42; + EXPECT_EQ(greeter.value, 42); + EXPECT_EQ(greeter.[:value_member:], 42); +} + +} // namespace section_3 + +// --------------------------------------------------------------------------- +// 4. Enumerating a type's members. +// +// std::meta::members_of and std::meta::nonstatic_data_members_of enumerate a +// type's members as a std::vector, but that vector exists +// only during constant evaluation, so it can only be built inside a +// consteval function. std::define_static_array copies the vector into +// static storage as a constexpr array, extending its lifetime past the +// return of that consteval function. +// --------------------------------------------------------------------------- +namespace section_4 { + +struct Point { + int x = 0; + int y = 0; + int z = 0; +}; + +consteval std::vector data_members_of(std::meta::info type) { + return nonstatic_data_members_of(type, std::meta::access_context::current()); +} + +// define_static_array is what makes the enumerated members usable as a +// constexpr array outside the consteval function that found them. +constexpr auto point_data_members = + std::define_static_array(data_members_of(^^Point)); + +TEST(ReflectionEnumerate, MembersInDeclarationOrder) { + static_assert(point_data_members.size() == 3); + static_assert(identifier_of(point_data_members[0]) == "x"); + static_assert(identifier_of(point_data_members[1]) == "y"); + static_assert(identifier_of(point_data_members[2]) == "z"); +} + +struct Widget { + int value() const { return 1; } + + void set_value(int) {} + + int payload = 0; +}; + +consteval std::vector member_functions_of( + std::meta::info type) { + // is_function alone would also match Widget's implicit special members + // (default constructor, destructor, ...); is_special_member_function + // excludes those, leaving just the two ordinary methods below. + const auto is_member_function = [](std::meta::info member) { + return is_function(member) && !is_special_member_function(member); + }; + return members_of(type, std::meta::access_context::current()) | + std::ranges::views::filter(is_member_function) | + std::ranges::to>(); +} + +constexpr auto widget_member_functions = + std::define_static_array(member_functions_of(^^Widget)); + +TEST(ReflectionEnumerate, FilteredToFunctionsOnly) { + // payload (a data member) is excluded; value() and set_value(int) (member + // functions) are the only two results. + static_assert(widget_member_functions.size() == 2); + static_assert(identifier_of(widget_member_functions[0]) == "value"); + static_assert(identifier_of(widget_member_functions[1]) == "set_value"); +} + +} // namespace section_4 + +// --------------------------------------------------------------------------- +// 5. Writing your own consteval helpers about a member. +// +// The standard library's reflection queries (std::meta::is_const, +// std::meta::parameters_of, std::meta::return_type_of, ...) are small, +// single-fact primitives. Answering richer questions means building +// consteval helpers on top of them. This section writes +// two such helpers: one classifying constness, and one building a +// signature string that distinguishes a member from other overloads of the +// same name. +// --------------------------------------------------------------------------- +namespace section_5 { + +struct Widget { + int read() const { return 1; } + + void write(int) {} + + void write(double) {} +}; + +// Unlike section 4's member_functions_of, this doesn't also filter out +// special member functions. The name check below already excludes them, +// since none of Widget's constructors, destructor, or assignment operators +// is named "read" or "write". +consteval std::vector members_named(std::meta::info type, + std::string_view name) { + std::vector result; + for (std::meta::info member : + members_of(type, std::meta::access_context::current())) { + if (is_function(member) && has_identifier(member) && + identifier_of(member) == name) { + result.push_back(member); + } + } + return result; +} + +TEST(ReflectionHelpers, ClassifiesConstCorrectly) { + constexpr auto read_candidates = + std::define_static_array(members_named(^^Widget, "read")); + constexpr auto write_candidates = + std::define_static_array(members_named(^^Widget, "write")); + + static_assert(read_candidates.size() == 1); + static_assert(is_const(read_candidates[0])); + + static_assert(write_candidates.size() == 2); + static_assert(!is_const(write_candidates[0])); + static_assert(!is_const(write_candidates[1])); +} + +// A simplified signature-string builder: name plus each parameter type's +// display string, joined together, turning a member's signature into a +// distinguishable string. A real version would go on to escape that string +// into a valid C++ identifier, useful anywhere generated code needs a +// unique name per overload. +// +// A parameter's type isn't a named declaration, so std::meta::identifier_of +// (which reads a declaration's own name) doesn't apply to it. +// std::meta::display_string_of is the primitive for turning a type into a +// readable string. The output of std::meta::display_string_of is +// implementation-defined and should not be used outside of debug output. +consteval std::string simple_signature_string(std::meta::info member) { + std::string result(identifier_of(member)); + result += "("; + bool first = true; + for (std::meta::info parameter : parameters_of(member)) { + if (!first) result += ","; + first = false; + result += display_string_of(dealias(type_of(parameter))); + } + result += ")"; + return result; +} + +TEST(ReflectionHelpers, SignatureDistinguishesOverloads) { + constexpr auto write_candidates = + std::define_static_array(members_named(^^Widget, "write")); + + // define_static_array returns a span sized to exactly the string's own + // length, with no guarantee of a trailing null byte, so wrap it in a + // string_view (which carries its own length) rather than treating .data() + // as a C-string, so comparisons never read past the span's actual bounds. + constexpr auto int_overload_signature = + std::define_static_array(simple_signature_string(write_candidates[0])); + constexpr auto double_overload_signature = + std::define_static_array(simple_signature_string(write_candidates[1])); + std::string_view int_signature(int_overload_signature.data(), + int_overload_signature.size()); + std::string_view double_signature(double_overload_signature.data(), + double_overload_signature.size()); + + EXPECT_EQ(int_signature, "write(int)"); + EXPECT_EQ(double_signature, "write(double)"); + EXPECT_NE(int_signature, double_signature); +} + +} // namespace section_5 + +// --------------------------------------------------------------------------- +// 6. Synthesizing a type's data members with define_aggregate. +// +// Every section so far has queried an existing type; this one builds one: +// std::meta::define_aggregate takes a type that's only been forward-declared +// so far (no members, incomplete) plus a list of std::meta::data_member_spec +// values, and gives that type exactly those data members, decided at +// compile time, not written out as a struct definition in source. This is +// useful anywhere generated code needs a struct whose members (their count, +// types, and names) aren't known until compile time, such as a vtable of +// function pointers with one entry per member some other type happens to +// declare. +// +// define_aggregate is called from inside a `consteval { ... }` block: a +// statement, not a function, that always runs during translation regardless +// of where it appears, even directly inside an ordinary, non-constexpr +// function body, as the first test below does. +// +// In C++26, define_aggregate is the only avenue available for code generation. +// --------------------------------------------------------------------------- +namespace section_6 { + +// Adapted from +// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html +// +// struct data_member_options { +// optional name; // name_type is a string +// optional alignment; +// optional bit_width; +// bool no_unique_address = false; +// }; + +consteval std::vector count_ratio_specs() { + std::vector specs; + // clang-format off + specs.push_back(data_member_spec(^^int, {.name = "count"})); + specs.push_back(data_member_spec(^^double, {.name = "ratio"})); + // clang-format on + return specs; +} + +TEST(ReflectionSynthesize, SynthesizedMembersEnumerable) { + struct Incomplete; + consteval { define_aggregate(^^Incomplete, count_ratio_specs()); } + + // The consteval block above already ran during translation, so by the + // time this ordinary, runtime TEST body executes, Incomplete already has + // its two members. Enumerating it works exactly like enumerating an + // ordinary, hand-written type (section 4). define_aggregate produces an + // ordinary type, usable the same way as a hand-written one. + constexpr auto members = std::define_static_array(nonstatic_data_members_of( + ^^Incomplete, std::meta::access_context::current())); + static_assert(members.size() == 2); + static_assert(identifier_of(members[0]) == "count"); + static_assert(identifier_of(members[1]) == "ratio"); +} + +// This test goes further than the one above: it constructs an instance and +// reads its members back, not just enumerates them. Declaring that instance +// constexpr forces its construction to happen in a constant expression, +// showing the synthesized type supports compile-time use like an ordinary +// type. +TEST(ReflectionSynthesize, MembersFromSpecList) { + struct Incomplete; + consteval { define_aggregate(^^Incomplete, count_ratio_specs()); } + constexpr Incomplete instance{.count = 3, .ratio = 1.5}; + static_assert(instance.count == 3 && instance.ratio == 1.5); +} + +} // namespace section_6 + +// --------------------------------------------------------------------------- +// 7. Converting an enum value to its name with template for. +// +// std::meta::enumerators_of(type) returns every enumerator of an enum +// type, in declaration order. This is the same shape of query section 4 +// used for a struct's data members, applied to an enum instead. Splicing an +// enumerator's info ([:e:]) gives back the enum value itself, so a +// to_string function can compare a runtime value against it directly. +// +// `template for` is a compile-time loop: for (constexpr auto e : range) +// unrolls into ordinary code, once per element, entirely during +// compilation. This is unlike every for loop earlier in this file, which either +// ran inside a consteval function (computing a vector for +// define_static_array to later hand out) or walked an already-computed +// span at runtime. Here the loop body itself becomes runtime code once per +// enumerator. +// +// enum_to_string below is P2996's own example (Reflection for C++26, +// "Enum to String"), with one adaptation: enumerators_of returns a +// std::vector, and only a copy in static storage is +// usable as template for's range. This is the same reason section 4 needed +// define_static_array. +// --------------------------------------------------------------------------- +namespace section_7 { + +enum class Color { Red, Green, Blue }; + +template + requires std::is_enum_v +std::string enum_to_string(E value) { + template for (constexpr std::meta::info e : + std::define_static_array(enumerators_of(^^E))) { + if (value == [:e:]) return std::string(identifier_of(e)); + } + return ""; +} + +TEST(ReflectionEnumToString, NamesEachEnumerator) { + EXPECT_EQ(enum_to_string(Color::Red), "Red"); + EXPECT_EQ(enum_to_string(Color::Green), "Green"); + EXPECT_EQ(enum_to_string(Color::Blue), "Blue"); +} + +TEST(ReflectionEnumToString, UnmatchedValueFallsThrough) { + // A value with no matching enumerator falls through every generated + // comparison to the fallback return, reached here via an explicit + // cast, since Color itself declares no such value. + EXPECT_EQ(enum_to_string(static_cast(99)), ""); +} + +} // namespace section_7 + +// --------------------------------------------------------------------------- +// 8. Instantiating a template from infos with substitute. +// +// A splice like typename[:int_info:] needs its operand to be a constant +// expression, e.g. a constexpr variable, not just any info that happens to +// exist while a consteval function runs. A range-based for loop over a +// std::vector doesn't give you that: the loop variable is +// rebound on each iteration, so it isn't a constant expression, even though +// the whole loop runs at compile time. A template specialization whose +// argument list is only known as such a range can't be built one splice at +// a time. std::meta::substitute(template_info, argument_infos) takes +// the whole range as an ordinary function argument instead, and returns an +// info for the resulting specialization directly. Splicing that result, as +// in section 2, turns it into a usable type. +// --------------------------------------------------------------------------- +namespace section_8 { + +template +struct Boxed { + T value = T(); +}; + +TEST(ReflectionSubstitute, InstantiatesTemplateFromInfos) { + // clang-format off + constexpr std::meta::info boxed_int_info = substitute(^^Boxed, {^^int}); + // clang-format on + + // The substitution names the same type as writing Boxed directly. + static_assert(std::is_same_v>); + + typename[:boxed_int_info:] boxed{.value = 5}; + EXPECT_EQ(boxed.value, 5); +} + +template +struct Sized {}; + +TEST(ReflectionSubstitute, SubstitutesANonTypeTemplateArgument) { + // substitute's argument list isn't limited to type infos: reflect_constant + // turns an ordinary value into an info for a non-type template argument. + // clang-format off + constexpr std::meta::info sized_five_info = + substitute(^^Sized, {std::meta::reflect_constant(5)}); + // clang-format on + static_assert(std::is_same_v>); +} + +struct Widget { + int compute(double) const { return 1; } +}; + +template +using fn_ptr_t = R (*)(Args...); + +consteval std::meta::info find_member(std::meta::info type, + std::string_view name) { + for (std::meta::info member : + members_of(type, std::meta::access_context::current())) { + if (has_identifier(member) && identifier_of(member) == name) { + return member; + } + } + return std::meta::info{}; +} + +// A member's return type plus its parameter types, gathered by reflection +// rather than named in source, are exactly the pieces substitute needs to +// build the matching function pointer type below. +consteval std::vector fn_ptr_arguments( + std::meta::info member) { + std::vector types{dealias(return_type_of(member))}; + for (std::meta::info parameter : parameters_of(member)) { + types.push_back(dealias(type_of(parameter))); + } + return types; +} + +TEST(ReflectionSubstitute, BuildsFunctionPointerTypeFromAMember) { + constexpr std::meta::info compute_member = find_member(^^Widget, "compute"); + constexpr auto arguments = + std::define_static_array(fn_ptr_arguments(compute_member)); + constexpr std::meta::info fn_ptr_info = substitute(^^fn_ptr_t, arguments); + + static_assert(std::is_same_v); +} + +} // namespace section_8 + +// --------------------------------------------------------------------------- +// 9. Reading a constant out of an info with extract. +// +// std::meta::extract(info) converts a std::meta::info that reflects a +// value back into an ordinary T (a real value you can return, store, or +// pass around), rather than something you can only splice into source +// code at a fixed spot. +// +// member_array_extent, below, takes a struct's info and a member name, +// finds that member, and reads N off its array type: std::array. +// std::meta::template_arguments_of gives back N only as an info, held in +// an ordinary local variable rather than spelled at a splice site, so it +// can't be spliced. extract is what turns it into an actual size_t. +// --------------------------------------------------------------------------- +namespace section_9 { + +struct Row { + std::array values{}; +}; + +consteval std::meta::info find_member(std::meta::info type, + std::string_view name) { + for (std::meta::info member : + members_of(type, std::meta::access_context::current())) { + if (has_identifier(member) && identifier_of(member) == name) { + return member; + } + } + return std::meta::info{}; +} + +consteval size_t member_array_extent(std::meta::info type, + std::string_view name) { + std::meta::info member_type = type_of(find_member(type, name)); + std::meta::info extent = template_arguments_of(member_type)[1]; + return extract(extent); +} + +TEST(ReflectionExtract, ReadsANonTypeTemplateArgument) { + static_assert(member_array_extent(^^Row, "values") == 4); +} + +} // namespace section_9 diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 0000000..a2f45b0 --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,7 @@ +# Tutorials + +Three standalone tutorials. Each is a single GoogleTest file whose sections build up one technique step by step. + +- [`1_type_erasure.cc`](1_type_erasure.cc) — manual type erasure from first principles: a vtable of function pointers, and an eraser object built from an erased pointer plus a vtable pointer. +- [`2_vanishing_this_pointer.cc`](2_vanishing_this_pointer.cc) — a layout/casting technique for giving a data member ordinary call syntax, by recovering the address of its owning object from `this`. +- [`3_reflection.cc`](3_reflection.cc) — C++26 reflection, from `std::meta::info` up to synthesizing a type's members at compile time. Requires a P2996 compiler (`scripts/cmake.sh --reflection`).