From add6f7d0aa1dd0baa8ccd49dd6b638a4650ef8f5 Mon Sep 17 00:00:00 2001 From: anothersimulacrum Date: Sat, 3 Jul 2021 16:44:18 -0700 Subject: [PATCH] generic_factory: support relative & proportional (#43144) 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/cata_void.h | 13 +++ src/generic_factory.h | 232 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 src/cata_void.h diff --git a/src/cata_void.h b/src/cata_void.h new file mode 100644 index 0000000000000..2ee8d8d152329 --- /dev/null +++ b/src/cata_void.h @@ -0,0 +1,13 @@ +#ifndef CATA_SRC_CATA_VOID_H +#define CATA_SRC_CATA_VOID_H + +// For some template magic in generic_factory.h, it's much easier to use void_t +// However, C++14 does not have this, so we need our own. +// It's nothing complex, just strip it out when C++17 comes. +namespace cata +{ +template +using void_t = void; +} // namespace cata + +#endif // CATA_SRC_CATA_VOID_H diff --git a/src/generic_factory.h b/src/generic_factory.h index ae38399e26f95..8fdf757e52186 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" @@ -612,11 +613,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 string ids 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 int ids 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(); } @@ -637,7 +803,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; + } + + // Type supports relative + 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; + } + + template + bool read_normal( const JsonObject &jo, const std::string &name, C &member ) const { + if( jo.has_member( name ) ) { + const Derived &derived = static_cast( *this ); + member = derived.get_next( *jo.get_raw( name ) ); + return true; + } + return false; + } + /** * Implements the reader interface, handles a simple data member. */ @@ -917,12 +1136,9 @@ class generic_typed_reader int >::type = 0 > 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 ) ) { - return false; - } - member = derived.get_next( *jo.get_raw( member_name ) ); - return true; + return read_normal( jo, member_name, member ) || + handle_proportional( jo, member_name, member ) || + do_relative( jo, member_name, member ); } };