I'm trying to serialize a templated class MState<T>
more or less generically. To do so, I have a parent abstract class MVariable
which implements several serialization functions with this form:
template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SOME_SPECIFIC_TYPE &t) const;
I want to allow T
to be almost anything. Serialization is done in JSON through RapidJSON::Writer. Because of that, I need to use specific member functions (e.g. Writer::String
, Writer::Bool
, Writer::Uint
...) in order to get the proper formatting for each type T
.
Serialization of basic types and STL-containers will be provided by MVariable
. However, instead of providing every single type (e.g. replacing SOME_SPECIFIC_TYPE
by float
, double
, bool
, etc.) I tried to implement a SFINAE-based solution that seems to have some flaws.
I have a set of typedef definitions and serialization functions like this:
class MVariable
{
template <class SerT> using SerializedFloating =
typename std::enable_if<std::is_floating_point<SerT>::value, SerT>::type;
template <class SerT> using SerializedSeqCntr =
typename std::enable_if<is_stl_sequential_container<SerT>::value, SerT>::type;
/* ... and many others. */
/* Serialization of float, double, long double... */
template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SerializedFloating<SerializedType> &t) const {
s.Double(t);
}
/* Serialization of vector<>, dequeue<>, list<> and forward_list<> */
template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SerializedSeqCntr<SerializedType> &t) const {
/* Let's assume we want to serialize them as JSON arrays: */
s.StartArray();
for(auto const& i : t) {
serialize(s, i); // ----> this fails to instantiate correctly.
}
s.EndArray();
}
/* If the previous templates could not be instantiated, check
* whether the SerializedType is a class with a proper serialize
* function:
**/
template <class Serializer, class SerializedType>
void serialize(Serializer&, SerializedType) const
{
/* Check existance of:
* void SerializedType::serialize(Serializer&) const;
**/
static_assert(has_serialize<
SerializedType,
void(Serializer&)>::value, "error message");
/* ... if it exists then we use it. */
}
};
template <class T>
class MState : public MVariable
{
T m_state;
template <class Serializer>
void serialize(Serializer& s) const {
s.Key(m_variable_name);
MVariable::serialize<Serializer, T>(s, m_state);
}
};
The implementation of is_stl_sequential_container
is based on this and the implementation of has_serialize
is borrowed from here. Both have been checked and seem to work properly:
MState<float> tvar0;
MState<double> tvar1;
MState<std::vector<float> > tvar2;
rapidjson::StringBuffer str_buf;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(str_buf);
writer.StartObject();
tvar0.serialize(writer); /* --> First function is used. Ok! */
tvar1.serialize(writer); /* --> First function is used. Ok! */
tvar2.serialize(writer); /* --> Second function is used, but there's
* substitution failure in the inner call.
**/
writer.EndObject();
However, the recursive serialize
call inside the second function, fails to be instantiated. Compiler complains starting with this:
In instantiation of ‘void MVariable::serialize(Serializer&, SerializedType) const
[with Serializer = rapidjson::PrettyWriter<... blah, blah, blah>;
SerializedType = float]’:
The message continues with the static assert error, suggesting that all the previous overloaded template functions failed in their substitutions or that the last one was the best option.
Why is substitution "failing" here for float
and not when I try to serialize tvar0
or tvar1
?
There are at least two issues in your code.
Firstly, you're explicitly specifying the template arguments in MState::serialize()
:
MVariable::serialize<Serializer, T>(s, m_state);
but then you're invoking template type deduction inside the SerializedSeqCntr
-constrained overload (via serialize(s, i);
); this won't work, because those SFINAE checks are non-deduced contexts(*), that is, they do not partecipate in type deduction, the compiler has no way of deducing the SerializedType
type.
Either pass the arguments explicitly, as in
serialize<Serializer,std::decay_t<decltype(i)>>(s, i);
or add a deduced SerializedType const&
parameter and a sfinae constrained dummy default argument or return type(**).
The second problem is that the 'fallback' overload should precede the constrained overloads possibly invoking it:
template <class Serializer, class SerializedType>
void serialize(Serializer&, SerializedType) const:
template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SerializedSeqCntr<SerializedType> &t);
...
otherwise, name look-up won't find the right serialize()
inside the SerializedSeqCntr
-constrained overload. Yes, being the function a dependent name, name look-up does happen at instantiation point; however, only names visible in the function body context are considered (unless ADL kicks in).
There could be also a third problem too; the fallback overload is not preferred over the constrained overload just because the former takes SerializedType by value; if this is not the intent, you'll need to further constrain the fallback too.
(*) to elaborate a bit, when you invoke a function template, you either pass template arguments explicitly (as in foo<bar>()
) or let the compiler deduce them from the types of the function arguments (as in foo(some_bar)
). Sometimes, this process cannot succeed.
This can happen for three reasons:
there is a substitution failure; that is, a template parameter T has been successfully deduced or given, but it also appears in an expression for which an error would have occurred if spelled out outside the function signature; the function overload is simply ignored; this is what SFINAE is all about.
there is an error while instantiating the types and functions needed to perform substitution; the function is not ignored, the program is ill-formed (if this sounds confusing, this answer may help).
the template argument cannot be deduced, the function overload is ignored; an obvious example is when the template parameter does not appear in any function argument yet is not explicitly specified; another example is when a function argument in which it appears happens to be a non-deduced context, see this answer for an explanation; you'll see that the argument, say, const SerializedFloating<SerializedType>&
is indeed non-deduced.
(**) as already said, SFINAE constraints are typically non-deduced; so, if you need type deduction to work, you should pass the to-be-deduced parameter on its own, deducible argument; this is typically done either by adding a dummy default argument or via the return type:
template<typename T>
result_type
foo( T arg, std::enable_if_t<std::is_floating_point<T>::value>* = 0 );
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value, result_type>
foo( T arg );