From 3ed17eaea4229af5fba76ee80ec6c53f030224d6 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Jul 2025 14:25:08 -0600 Subject: [PATCH 1/4] add built-in support for custom visitation contexts follows plan in issue #34 building but still needs tests --- include/visit_struct/visit_struct.hpp | 273 +++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 6 deletions(-) diff --git a/include/visit_struct/visit_struct.hpp b/include/visit_struct/visit_struct.hpp index 19f51d9..cfbc87a 100644 --- a/include/visit_struct/visit_struct.hpp +++ b/include/visit_struct/visit_struct.hpp @@ -18,8 +18,8 @@ // Library version #define VISIT_STRUCT_VERSION_MAJOR 1 -#define VISIT_STRUCT_VERSION_MINOR 1 -#define VISIT_STRUCT_VERSION_PATCH 2 +#define VISIT_STRUCT_VERSION_MINOR 2 +#define VISIT_STRUCT_VERSION_PATCH 0 #define VISIT_STRUCT_STRING_HELPER(X) #X #define VISIT_STRUCT_STRING(X) VISIT_STRUCT_STRING_HELPER(X) @@ -61,16 +61,19 @@ namespace visit_struct { namespace traits { // Primary template which is specialized to register a type -template +// The context parameter is set when a user wants to register multiple visitation patterns, +// to include or exclude some field in different contexts. +template struct visitable; // Helper template which checks if a type is registered -template +template struct is_visitable : std::false_type {}; -template +template struct is_visitable::value>::type> + CONTEXT, + typename std::enable_if::value>::type> : std::true_type {}; // Helper template which removes cv and reference from a type (saves some typing) @@ -298,6 +301,204 @@ VISIT_STRUCT_CONSTEXPR auto get_name(S &&) -> decltype(get_name()) { return get_name(); } +// Alternate visitation patterns can be registered using VISITABLE_STRUCT_IN_CONTEXT. +// Then, use visit_struct::context::for_each and similar to refer to special contexts. +template +struct context { + + // Return number of fields in a visitable struct + template + VISIT_STRUCT_CONSTEXPR std::size_t field_count() + { + return traits::visitable, CONTEXT>::field_count; + } + + template + VISIT_STRUCT_CONSTEXPR std::size_t field_count(S &&) { return field_count(); } + + + // apply_visitor (one struct instance) + template + VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v, S && s) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + traits::visitable, CONTEXT>::apply(std::forward(v), std::forward(s)); + } + + // apply_visitor (two struct instances) + template + VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v, S1 && s1, S2 && s2) -> + typename std::enable_if< + traits::is_visitable< + traits::clean_t::type>, + CONTEXT + >::value + >::type + { + using common_S = typename traits::common_type::type; + traits::visitable, CONTEXT>::apply(std::forward(v), + std::forward(s1), + std::forward(s2)); + } + + // for_each (Alternate syntax for apply_visitor, reverses order of arguments) + template + VISIT_STRUCT_CXX14_CONSTEXPR auto for_each(S && s, V && v) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + traits::visitable, CONTEXT>::apply(std::forward(v), std::forward(s)); + } + + // for_each with two structure instances + template + VISIT_STRUCT_CXX14_CONSTEXPR auto for_each(S1 && s1, S2 && s2, V && v) -> + typename std::enable_if< + traits::is_visitable< + traits::clean_t::type>, + CONTEXT + >::value + >::type + { + using common_S = typename traits::common_type::type; + traits::visitable, CONTEXT>::apply(std::forward(v), + std::forward(s1), + std::forward(s2)); + } + + // Visit the types (visit_struct::type_c<...>) of the registered members + template + VISIT_STRUCT_CXX14_CONSTEXPR auto visit_types(V && v) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + traits::visitable, CONTEXT>::visit_types(std::forward(v)); + } + + // Visit the member pointers (&S::a) of the registered members + template + VISIT_STRUCT_CXX14_CONSTEXPR auto visit_pointers(V && v) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + traits::visitable, CONTEXT>::visit_pointers(std::forward(v)); + } + + // Visit the accessors (function objects) of the registered members + template + VISIT_STRUCT_CXX14_CONSTEXPR auto visit_accessors(V && v) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + traits::visitable, CONTEXT>::visit_accessors(std::forward(v)); + } + + + // Apply visitor (with no instances) + // This calls visit_pointers, for backwards compat reasons + template + VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v) -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value + >::type + { + visit_struct::visit_pointers(std::forward(v)); + } + + + // Get value by index (like std::get for tuples) + template + VISIT_STRUCT_CONSTEXPR auto get(S && s) -> + typename std::enable_if< + traits::is_visitable>::value, + decltype(traits::visitable, CONTEXT>::get_value(std::integral_constant{}, std::forward(s))) + >::type + { + return traits::visitable, CONTEXT>::get_value(std::integral_constant{}, std::forward(s)); + } + + // Get name of field, by index + template + VISIT_STRUCT_CONSTEXPR auto get_name() -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value, + decltype(traits::visitable, CONTEXT>::get_name(std::integral_constant{})) + >::type + { + return traits::visitable, CONTEXT>::get_name(std::integral_constant{}); + } + + template + VISIT_STRUCT_CONSTEXPR auto get_name(S &&) -> decltype(get_name()) { + return get_name(); + } + + // Get member pointer, by index + template + VISIT_STRUCT_CONSTEXPR auto get_pointer() -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value, + decltype(traits::visitable, CONTEXT>::get_pointer(std::integral_constant{})) + >::type + { + return traits::visitable, CONTEXT>::get_pointer(std::integral_constant{}); + } + + template + VISIT_STRUCT_CONSTEXPR auto get_pointer(S &&) -> decltype(get_pointer()) { + return get_pointer(); + } + + // Get member accessor, by index + template + VISIT_STRUCT_CONSTEXPR auto get_accessor() -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value, + decltype(traits::visitable, CONTEXT>::get_accessor(std::integral_constant{})) + >::type + { + return traits::visitable, CONTEXT>::get_accessor(std::integral_constant{}); + } + + template + VISIT_STRUCT_CONSTEXPR auto get_accessor(S &&) -> decltype(get_accessor()) { + return get_accessor(); + } + + // Get type, by index + template + struct type_at_s { + using type_c = decltype(traits::visitable, CONTEXT>::type_at(std::integral_constant{})); + using type = typename type_c::type; + }; + + template + using type_at = typename type_at_s::type; + + // Get name of structure + template + VISIT_STRUCT_CONSTEXPR auto get_name() -> + typename std::enable_if< + traits::is_visitable, CONTEXT>::value, + decltype(traits::visitable, CONTEXT>::get_name()) + >::type + { + return traits::visitable, CONTEXT>::get_name(); + } + + template + VISIT_STRUCT_CONSTEXPR auto get_name(S &&) -> decltype(get_name()) { + return get_name(); + } +}; + + /*** * To implement the VISITABLE_STRUCT macro, we need a map-macro, which can take * the name of a macro and some other arguments, and apply that macro to each other argument. @@ -561,6 +762,66 @@ struct visitable { } \ static_assert(true, "") +#define VISITABLE_STRUCT_IN_CONTEXT(CONTEXT, STRUCT_NAME, ...) \ +namespace visit_struct { \ +namespace traits { \ + \ +template <> \ +struct visitable { \ + \ + using this_type = STRUCT_NAME; \ + \ + static VISIT_STRUCT_CONSTEXPR auto get_name() \ + -> decltype(#STRUCT_NAME) { \ + return #STRUCT_NAME; \ + } \ + \ + static VISIT_STRUCT_CONSTEXPR const std::size_t field_count = 0 \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_FIELD_COUNT, __VA_ARGS__); \ + \ + template \ + VISIT_STRUCT_CXX14_CONSTEXPR static void apply(V && visitor, S && struct_instance) \ + { \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MEMBER_HELPER, __VA_ARGS__) \ + } \ + \ + template \ + VISIT_STRUCT_CXX14_CONSTEXPR static void apply(V && visitor, S1 && s1, S2 && s2) \ + { \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MEMBER_HELPER_PAIR, __VA_ARGS__) \ + } \ + \ + template \ + VISIT_STRUCT_CXX14_CONSTEXPR static void visit_pointers(V && visitor) \ + { \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MEMBER_HELPER_PTR, __VA_ARGS__) \ + } \ + \ + template \ + VISIT_STRUCT_CXX14_CONSTEXPR static void visit_types(V && visitor) \ + { \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MEMBER_HELPER_TYPE, __VA_ARGS__) \ + } \ + \ + template \ + VISIT_STRUCT_CXX14_CONSTEXPR static void visit_accessors(V && visitor) \ + { \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MEMBER_HELPER_ACC, __VA_ARGS__) \ + } \ + \ + struct fields_enum { \ + enum index { __VA_ARGS__ }; \ + }; \ + \ + VISIT_STRUCT_PP_MAP(VISIT_STRUCT_MAKE_GETTERS, __VA_ARGS__) \ + \ + static VISIT_STRUCT_CONSTEXPR const bool value = true; \ +}; \ + \ +} \ +} \ +static_assert(true, "") + } // end namespace visit_struct #endif // VISIT_STRUCT_HPP_INCLUDED From a4d02e3f98d91cb96f0c0647f4e39884de027ed1 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Jul 2025 16:43:35 -0600 Subject: [PATCH 2/4] fix previous --- include/visit_struct/visit_struct.hpp | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/include/visit_struct/visit_struct.hpp b/include/visit_struct/visit_struct.hpp index cfbc87a..4d4bbc6 100644 --- a/include/visit_struct/visit_struct.hpp +++ b/include/visit_struct/visit_struct.hpp @@ -308,18 +308,18 @@ struct context { // Return number of fields in a visitable struct template - VISIT_STRUCT_CONSTEXPR std::size_t field_count() + VISIT_STRUCT_CONSTEXPR static std::size_t field_count() { return traits::visitable, CONTEXT>::field_count; } template - VISIT_STRUCT_CONSTEXPR std::size_t field_count(S &&) { return field_count(); } + VISIT_STRUCT_CONSTEXPR static std::size_t field_count(S &&) { return field_count(); } // apply_visitor (one struct instance) template - VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v, S && s) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto apply_visitor(V && v, S && s) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -329,7 +329,7 @@ struct context { // apply_visitor (two struct instances) template - VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v, S1 && s1, S2 && s2) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto apply_visitor(V && v, S1 && s1, S2 && s2) -> typename std::enable_if< traits::is_visitable< traits::clean_t::type>, @@ -345,7 +345,7 @@ struct context { // for_each (Alternate syntax for apply_visitor, reverses order of arguments) template - VISIT_STRUCT_CXX14_CONSTEXPR auto for_each(S && s, V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto for_each(S && s, V && v) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -355,7 +355,7 @@ struct context { // for_each with two structure instances template - VISIT_STRUCT_CXX14_CONSTEXPR auto for_each(S1 && s1, S2 && s2, V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto for_each(S1 && s1, S2 && s2, V && v) -> typename std::enable_if< traits::is_visitable< traits::clean_t::type>, @@ -371,7 +371,7 @@ struct context { // Visit the types (visit_struct::type_c<...>) of the registered members template - VISIT_STRUCT_CXX14_CONSTEXPR auto visit_types(V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto visit_types(V && v) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -381,7 +381,7 @@ struct context { // Visit the member pointers (&S::a) of the registered members template - VISIT_STRUCT_CXX14_CONSTEXPR auto visit_pointers(V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto visit_pointers(V && v) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -391,7 +391,7 @@ struct context { // Visit the accessors (function objects) of the registered members template - VISIT_STRUCT_CXX14_CONSTEXPR auto visit_accessors(V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto visit_accessors(V && v) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -403,7 +403,7 @@ struct context { // Apply visitor (with no instances) // This calls visit_pointers, for backwards compat reasons template - VISIT_STRUCT_CXX14_CONSTEXPR auto apply_visitor(V && v) -> + VISIT_STRUCT_CXX14_CONSTEXPR static auto apply_visitor(V && v) -> typename std::enable_if< traits::is_visitable, CONTEXT>::value >::type @@ -414,7 +414,7 @@ struct context { // Get value by index (like std::get for tuples) template - VISIT_STRUCT_CONSTEXPR auto get(S && s) -> + VISIT_STRUCT_CONSTEXPR static auto get(S && s) -> typename std::enable_if< traits::is_visitable>::value, decltype(traits::visitable, CONTEXT>::get_value(std::integral_constant{}, std::forward(s))) @@ -425,7 +425,7 @@ struct context { // Get name of field, by index template - VISIT_STRUCT_CONSTEXPR auto get_name() -> + VISIT_STRUCT_CONSTEXPR static auto get_name() -> typename std::enable_if< traits::is_visitable, CONTEXT>::value, decltype(traits::visitable, CONTEXT>::get_name(std::integral_constant{})) @@ -435,13 +435,13 @@ struct context { } template - VISIT_STRUCT_CONSTEXPR auto get_name(S &&) -> decltype(get_name()) { + VISIT_STRUCT_CONSTEXPR static auto get_name(S &&) -> decltype(get_name()) { return get_name(); } // Get member pointer, by index template - VISIT_STRUCT_CONSTEXPR auto get_pointer() -> + VISIT_STRUCT_CONSTEXPR static auto get_pointer() -> typename std::enable_if< traits::is_visitable, CONTEXT>::value, decltype(traits::visitable, CONTEXT>::get_pointer(std::integral_constant{})) @@ -451,13 +451,13 @@ struct context { } template - VISIT_STRUCT_CONSTEXPR auto get_pointer(S &&) -> decltype(get_pointer()) { + VISIT_STRUCT_CONSTEXPR static auto get_pointer(S &&) -> decltype(get_pointer()) { return get_pointer(); } // Get member accessor, by index template - VISIT_STRUCT_CONSTEXPR auto get_accessor() -> + VISIT_STRUCT_CONSTEXPR static auto get_accessor() -> typename std::enable_if< traits::is_visitable, CONTEXT>::value, decltype(traits::visitable, CONTEXT>::get_accessor(std::integral_constant{})) @@ -467,7 +467,7 @@ struct context { } template - VISIT_STRUCT_CONSTEXPR auto get_accessor(S &&) -> decltype(get_accessor()) { + VISIT_STRUCT_CONSTEXPR static auto get_accessor(S &&) -> decltype(get_accessor()) { return get_accessor(); } @@ -483,7 +483,7 @@ struct context { // Get name of structure template - VISIT_STRUCT_CONSTEXPR auto get_name() -> + VISIT_STRUCT_CONSTEXPR static auto get_name() -> typename std::enable_if< traits::is_visitable, CONTEXT>::value, decltype(traits::visitable, CONTEXT>::get_name()) @@ -493,7 +493,7 @@ struct context { } template - VISIT_STRUCT_CONSTEXPR auto get_name(S &&) -> decltype(get_name()) { + VISIT_STRUCT_CONSTEXPR static auto get_name(S &&) -> decltype(get_name()) { return get_name(); } }; From 82b0501c2594cd709f395fe7e83dd8c4f47eed62 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Jul 2025 15:16:15 -0600 Subject: [PATCH 3/4] add tests --- test_visit_struct.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test_visit_struct.cpp b/test_visit_struct.cpp index 9a913f6..2c42209 100644 --- a/test_visit_struct.cpp +++ b/test_visit_struct.cpp @@ -35,6 +35,13 @@ VISITABLE_STRUCT(test_struct_two, d, i, b); static_assert(visit_struct::traits::is_visitable::value, "WTF"); static_assert(visit_struct::field_count() == 3, "WTF"); +// Make test struct two have a special context for visitation +struct MyContext {}; +VISITABLE_STRUCT_IN_CONTEXT(MyContext, test_struct_two, b, i, d, s); + +static_assert(visit_struct::traits::is_visitable::value, "WTF"); +static_assert(visit_struct::context::field_count() == 4, "WTF"); + /*** * Test visitors */ @@ -312,10 +319,25 @@ int main() { visit_struct::apply_visitor(vis2, s); assert(vis2.result.size() == 3); + assert(vis2.result[0].first == std::string{"d"}); assert(vis2.result[0].second == &s.d); + assert(vis2.result[1].first == std::string{"i"}); assert(vis2.result[1].second == &s.i); + assert(vis2.result[2].first == std::string{"b"}); assert(vis2.result[2].second == &s.b); + test_visitor_two vis2_context; + visit_struct::context::apply_visitor(vis2_context, s); + + assert(vis2_context.result.size() == 4); + assert(vis2_context.result[0].first == std::string{"b"}); + assert(vis2_context.result[0].second == &s.b); + assert(vis2_context.result[1].first == std::string{"i"}); + assert(vis2_context.result[1].second == &s.i); + assert(vis2_context.result[2].first == std::string{"d"}); + assert(vis2_context.result[2].second == &s.d); + assert(vis2_context.result[3].first == std::string{"s"}); + assert(vis2_context.result[3].second == &s.s); test_struct_two t{ true, -14, .75, "bar" }; From 9de8e07e5a1b9b12a4de86ffcf326069636a4286 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Tue, 22 Jul 2025 12:14:36 -0600 Subject: [PATCH 4/4] add docu about visitation contexts --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebe8120..ccad1c2 100644 --- a/README.md +++ b/README.md @@ -543,7 +543,53 @@ So, nowadays I prefer and recommend `for_each`. The original `apply_visitor` sy visit_struct::traits::is_visitable::value ``` -This type trait can be used to check if a structure is visitable. The above expression should resolve to boolean true or false. I consider it part of the forward-facing interface, you can use it in SFINAE to easily select types that `visit_struct` knows how to use. +This type trait can be used to check if a structure is visitable. The above expression should resolve to boolean true or false. I consider it part of the public API. You can use it in SFINAE to easily select types that `visit_struct` knows how to use. + +## Advanced Features + +### Visitation contexts + +In a larger project, sometimes you want to visit a different set of members in each of your structures for different purposes. +For example, for serialization you might want to visit everything. For logging you might want to omit some of the elements. For scripting access you might want a different subset still. + +It's possible to use inheritance to create proxy objects which can be declared visitable in different ways from the base object. But it has ergonomic problems and may force unnecessary copies, which is particularly harmful when walking large nested structures. + +As an alternative, visit struct supports creating custom visitation "contexts". A context can be represented by any type, but usually a tag type in your program. Structs can be registered as visitable against each of these tags. + +``` +struct Logging {}; +struct Scripting {}; + +struct Foo { + int i; + double d; + bool b; + string s; +}; + +VISITABLE_STRUCT(Foo, i, d, b, s); +VISITABLE_STRUCT_IN_CONTEXT(Logging, Foo, i, s); +VISITABLE_STRUCT_IN_CONTEXT(Scripting, Foo, d, b, s); +``` + +Then, you can visit it in a special context by using calls such as + +``` +visit_struct::context::for_each(foo, visitor); +``` + +The whole API of free functions is reproduced under `context` to invoke those functions within the given context. + +If you need to use the same context repeatedly, you can shorten this like so + +``` +using C = visit_struct::Context; +C::for_each(foo, visitor); +C::for_each(bar, visitor); +``` + +This also allows you to write functions that are generic over the context parameter if you like. +The "default" context corresponds to the type `void`. ## Limits