From 514a9d8a70827df054ce9622af46e4fa1b2c14a2 Mon Sep 17 00:00:00 2001 From: anothersimulacrum <42699974+anothersimulacrum@users.noreply.github.com> Date: Sat, 22 Aug 2020 21:56:55 -0700 Subject: [PATCH] generic_factory: support relative & proportional In the hope of eventually removing the assign() functions, and expanding support for generic factory (and continuing to support existing use cases), generic_factory needs to support relative and proportional. To do this, do some template vodoo. Create two templated structs, and take advantage of SFINAE (yeah, I had to look that up) with these to determine which template function to use. Three basically discriminate between whether or not the type can specify value proportionally or relatively. For proportionally, this is whether or not the type can be multiplied by a float, and for relative, this is whether or not the type can use the += operator with itself. Also, add some error checking for when a proportional value is inappropriately specified. Containers using the reader functions do not have proportional and relative support, because I'm not sure of a use case where that ever makes sense. Huge thanks to jbytheway for helping me use template vodoo to accomplish this. --- src/generic_factory.h | 212 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 3 deletions(-) diff --git a/src/generic_factory.h b/src/generic_factory.h index ad5497b99c6ec..19c23949c84f6 100644 --- a/src/generic_factory.h +++ b/src/generic_factory.h @@ -10,6 +10,7 @@ #include "assign.h" #include "catacharset.h" +#include "cata_void.h" #include "debug.h" #include "enum_bitset.h" #include "init.h" @@ -548,11 +549,176 @@ inline void mandatory( const JsonObject &jo, const bool was_loaded, const std::s } } +/* + * Template vodoo: + * The compiler will construct the appropriate one of these based on if the + * type can support the operations being done. + * So, it defaults to the false_type, but if it can use the *= operator + * against a float, it then supports proportional, and the handle_proportional + * template that isn't just a dummy is constructed. + * Similarly, if it can use a += operator against it's own type, the non-dummy + * handle_relative template is constructed. + */ +template> +struct supports_proportional : std::false_type { }; + +template +struct supports_proportional() *= std::declval() )>> : +std::true_type {}; + +template> +struct supports_relative : std::false_type { }; + +template +struct supports_relative < T, cata::void_t < decltype( std::declval() += std::declval() ) +>> : std::true_type {}; + +// Explicitly specialize these templates for a couple types +// So the compiler does not attempt to use a template that it should not +template<> +struct supports_proportional : std::false_type {}; + +template<> +struct supports_relative : std::false_type {}; + +template<> +struct supports_relative : std::false_type {}; + +// This checks that all units:: types will support relative and proportional +static_assert( supports_relative::value, "units should support relative" ); +static_assert( supports_proportional::value, "units should support proportional" ); + +static_assert( supports_relative::value, "ints should support relative" ); +static_assert( supports_proportional::value, "ints should support proportional" ); + +static_assert( !supports_relative::value, "bools should not support relative" ); +static_assert( !supports_proportional::value, "bools should not support proportional" ); + +// Using these with ints doesn't make sense in practice, but it doesn't matter here +// The type that it is templated with does not change it's behavior +static_assert( !supports_relative>::value, + "string ids should not support relative" ); +static_assert( !supports_proportional>::value, + "string ids should not support proportional" ); + +// Using these with ints doesn't make sense in practice, but it doesn't matter here +// The type that it is templated with does not change it's behavior +static_assert( !supports_relative>::value, + "int ids should not support relative" ); +static_assert( !supports_proportional>::value, + "int ids should not support proportional" ); + +static_assert( !supports_relative::value, "strings should not support relative" ); +static_assert( !supports_proportional::value, + "strings should not support proportional" ); + +// Grab an enum class from debug.h +static_assert( !supports_relative::value, "enum classes should not support relative" ); +static_assert( !supports_proportional::value, + "enum classes should not support proportional" ); + +// Grab a normal enum from there too +static_assert( !supports_relative::value, "enums should not support relative" ); +static_assert( !supports_proportional::value, "enums should not support relative" ); + +// Dummy template: +// Warn if it's trying to use proportional where it cannot, but otherwise just +// return. +template < typename MemberType, std::enable_if_t < !supports_proportional::value > * = + nullptr > +inline bool handle_proportional( const JsonObject &jo, const std::string &name, MemberType & ) +{ + if( jo.has_object( "proportional" ) ) { + JsonObject proportional = jo.get_object( "proportional" ); + proportional.allow_omitted_members(); + if( proportional.has_member( name ) ) { + debugmsg( "Member %s of type %s does not support proportional", name, typeid( MemberType ).name() ); + } + } + return false; +} + +// Real template: +// Copy-from makes it so the thing we're inheriting from is used to construct +// this, so member will contain the value of the thing we inherit from +// So, check if there is a proportional entry, check if it's got a valid value +// and if it does, multiply the member by it. +template::value>* = nullptr> +inline bool handle_proportional( const JsonObject &jo, const std::string &name, MemberType &member ) +{ + if( jo.has_object( "proportional" ) ) { + JsonObject proportional = jo.get_object( "proportional" ); + proportional.allow_omitted_members(); + // We need to check this here, otherwise we get problems with unvisited members + if( !proportional.has_member( name ) ) { + return false; + } + if( proportional.has_float( name ) ) { + double scalar = proportional.get_float( name ); + if( scalar <= 0 || scalar == 1 ) { + debugmsg( "Invalid scalar %g for %s", scalar, name ); + return false; + } + member *= scalar; + return true; + } else { + jo.throw_error( "Invalid scalar for %s", name ); + } + } + return false; +} + +// Dummy template: +// Warn when trying to use relative when it's not supported, but otherwise, +// return +template < typename MemberType, + std::enable_if_t < !supports_relative::value > * = nullptr + > +inline bool handle_relative( const JsonObject &jo, const std::string &name, MemberType & ) +{ + if( jo.has_object( "relative" ) ) { + JsonObject relative = jo.get_object( "relative" ); + relative.allow_omitted_members(); + if( !relative.has_member( name ) ) { + return false; + } + debugmsg( "Member %s of type %s does not support relative", name, typeid( MemberType ).name() ); + } + return false; +} + +// Real template: +// Copy-from makes it so the thing we're inheriting from is used to construct +// this, so member will contain the value of the thing we inherit from +// So, check if there is a relative entry, then add it to our member +template::value>* = nullptr> +inline bool handle_relative( const JsonObject &jo, const std::string &name, MemberType &member ) +{ + if( jo.has_object( "relative" ) ) { + JsonObject relative = jo.get_object( "relative" ); + relative.allow_omitted_members(); + // This needs to happen here, otherwise we get unvisited members + if( !relative.has_member( name ) ) { + return false; + } + MemberType adder; + if( relative.read( name, adder ) ) { + member += adder; + return true; + } else { + jo.throw_error( "Invalid adder for %s", name ); + } + } + return false; +} + +// No template magic here, yay! template inline void optional( const JsonObject &jo, const bool was_loaded, const std::string &name, MemberType &member ) { - if( !jo.read( name, member ) ) { + if( !jo.read( name, member ) && !handle_proportional( jo, name, member ) && + !handle_relative( jo, name, member ) ) { if( !was_loaded ) { member = MemberType(); } @@ -573,7 +739,8 @@ template::is_container, + int >::type = 0, + std::enable_if_t < !supports_relative::value > * = nullptr + > + bool do_relative( const JsonObject &jo, const std::string &name, C & ) const { + if( jo.has_object( "relative" ) ) { + JsonObject relative = jo.get_object( "relative" ); + relative.allow_omitted_members(); + if( !relative.has_member( name ) ) { + return false; + } + debugmsg( "Member %s of type %s does not support relative", name, typeid( C ).name() ); + } + return false; + } + template < typename C, typename std::enable_if < !reader_detail::handler::is_container, + int >::type = 0, std::enable_if_t::value> * = nullptr > + bool do_relative( const JsonObject &jo, const std::string &name, C &member ) const { + if( jo.has_object( "relative" ) ) { + JsonObject relative = jo.get_object( "relative" ); + relative.allow_omitted_members(); + const Derived &derived = static_cast( *this ); + // This needs to happen here, otherwise we get unvisited members + if( !relative.has_member( name ) ) { + return false; + } + C adder = derived.get_next( *relative.get_raw( name ) ); + member += adder; + return true; + } + return false; + } + /** * Implements the reader interface, handles a simple data member. */ @@ -854,10 +1056,14 @@ class generic_typed_reader bool operator()( const JsonObject &jo, const std::string &member_name, C &member, bool /*was_loaded*/ ) const { const Derived &derived = static_cast( *this ); + if( jo.has_member( member_name ) && !handle_proportional( jo, member_name, member ) && + !do_relative( jo, member_name, member ) ) { + member = derived.get_next( *jo.get_raw( member_name ) ); + return true; + } if( !jo.has_member( member_name ) ) { return false; } - member = derived.get_next( *jo.get_raw( member_name ) ); return true; } };