Skip to content

Commit

Permalink
generic_factory: support relative & proportional (#43144)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
anothersimulacrum authored Jul 3, 2021
1 parent ee1873d commit add6f7d
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 8 deletions.
13 changes: 13 additions & 0 deletions src/cata_void.h
Original file line number Diff line number Diff line change
@@ -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<typename...>
using void_t = void;
} // namespace cata

#endif // CATA_SRC_CATA_VOID_H
232 changes: 224 additions & 8 deletions src/generic_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include "assign.h"
#include "catacharset.h"
#include "cata_void.h"
#include "debug.h"
#include "enum_bitset.h"
#include "init.h"
Expand Down Expand Up @@ -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<typename T, typename = cata::void_t<>>
struct supports_proportional : std::false_type { };

template<typename T>
struct supports_proportional<T, cata::void_t<decltype( std::declval<T &>() *= std::declval<float>() )>> :
std::true_type {};

template<typename T, typename = cata::void_t<>>
struct supports_relative : std::false_type { };

template<typename T>
struct supports_relative < T, cata::void_t < decltype( std::declval<T &>() += std::declval<T &>() )
>> : 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<bool> : std::false_type {};

template<>
struct supports_relative<bool> : std::false_type {};

template<>
struct supports_relative<std::string> : std::false_type {};

// This checks that all units:: types will support relative and proportional
static_assert( supports_relative<units::energy>::value, "units should support relative" );
static_assert( supports_proportional<units::energy>::value, "units should support proportional" );

static_assert( supports_relative<int>::value, "ints should support relative" );
static_assert( supports_proportional<int>::value, "ints should support proportional" );

static_assert( !supports_relative<bool>::value, "bools should not support relative" );
static_assert( !supports_proportional<bool>::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<string_id<int>>::value,
"string ids should not support relative" );
static_assert( !supports_proportional<string_id<int>>::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<int_id<int>>::value,
"int ids should not support relative" );
static_assert( !supports_proportional<int_id<int>>::value,
"int ids should not support proportional" );

static_assert( !supports_relative<std::string>::value, "strings should not support relative" );
static_assert( !supports_proportional<std::string>::value,
"strings should not support proportional" );

// Grab an enum class from debug.h
static_assert( !supports_relative<DebugOutput>::value, "enum classes should not support relative" );
static_assert( !supports_proportional<DebugOutput>::value,
"enum classes should not support proportional" );

// Grab a normal enum from there too
static_assert( !supports_relative<DebugLevel>::value, "enums should not support relative" );
static_assert( !supports_proportional<DebugLevel>::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<MemberType>::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<typename MemberType, std::enable_if_t<supports_proportional<MemberType>::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<MemberType>::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<typename MemberType, std::enable_if_t<supports_relative<MemberType>::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<typename MemberType>
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();
}
Expand All @@ -637,7 +803,8 @@ template<typename MemberType, typename DefaultType = MemberType,
inline void optional( const JsonObject &jo, const bool was_loaded, const std::string &name,
MemberType &member, const DefaultType &default_value )
{
if( !jo.read( name, member ) ) {
if( !jo.read( name, member ) && !handle_proportional( jo, name, member ) &&
!handle_relative( jo, name, member ) ) {
if( !was_loaded ) {
member = default_value;
}
Expand Down Expand Up @@ -908,6 +1075,58 @@ class generic_typed_reader
}
}

/*
* These two functions are effectively handle_relative but they need to
* use the reader, so they must be here.
* proportional does not need these, because it's only reading a float
* whereas these are reading values of the same type.
*/
// Type does not support relative
template < typename C, typename std::enable_if < !reader_detail::handler<C>::is_container,
int >::type = 0,
std::enable_if_t < !supports_relative<C>::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<C>::is_container,
int >::type = 0, std::enable_if_t<supports_relative<C>::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<const Derived &>( *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<typename C>
bool read_normal( const JsonObject &jo, const std::string &name, C &member ) const {
if( jo.has_member( name ) ) {
const Derived &derived = static_cast<const Derived &>( *this );
member = derived.get_next( *jo.get_raw( name ) );
return true;
}
return false;
}

/**
* Implements the reader interface, handles a simple data member.
*/
Expand All @@ -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<const Derived &>( *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 );
}
};

Expand Down

0 comments on commit add6f7d

Please sign in to comment.