基于SFINAE的序列化解决方案无法在C ++中实例化重载的模板化函数

时间:2017-12-17 12:32:14

标签: c++ templates serialization sfinae rapidjson

我正在尝试或多或少一般地序列化模板化的类MState<T>。为此,我有一个父抽象类MVariable,它使用以下形式实现了几个序列化函数:

template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SOME_SPECIFIC_TYPE &t) const;

我想允许T 几乎任何。序列化通过RapidJSON::Writer在JSON中完成。因此,我需要使用特定的成员函数(例如Writer::StringWriter::BoolWriter::Uint ...),以便为每种类型T获取正确的格式。

基本类型和STL容器的序列化将由MVariable提供。但是,我没有提供每一种类型(例如,SOME_SPECIFIC_TYPE替换floatdoublebool等等,而是尝试实施基于SFINAE的解决方案,而不是有一些缺陷。

我有一组typedef定义和序列化函数,如下所示:

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);
    }
};

is_stl_sequential_container的实施基于thishas_serialize的实施借鉴here。两者都经过检查,似乎运作正常:

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();

但是,第二个函数内的递归serialize调用无法实例化。编译器抱怨从这开始:

In instantiation of ‘void MVariable::serialize(Serializer&, SerializedType) const 
[with Serializer = rapidjson::PrettyWriter<... blah, blah, blah>; 
      SerializedType = float]’:

消息继续静态断言错误,表明所有先前重载的模板函数在其替换中失败,或者最后一个是最佳选项。

为什么 float 的替换“失败”,而不是在我尝试序列化tvar0tvar1时?

1 个答案:

答案 0 :(得分:1)

问题......

您的代码中至少存在两个问题。

首先,您在MState::serialize()中明确指定了模板参数:

MVariable::serialize<Serializer, T>(s, m_state);

然后你在SerializedSeqCntr - 约束重载(通过serialize(s, i);)中调用模板类型推导;这不起作用,因为那些SFINAE检查是非推导的上下文(*),也就是说,它们不会在类型推导中分离,编译器无法推断出SerializedType类型。

显式传递参数,如

serialize<Serializer,std::decay_t<decltype(i)>>(s, i);

或添加推导的SerializedType const&参数和sfinae约束虚拟默认参数或返回类型(**)。

第二个问题是'fallback'重载应该在可能调用它的约束重载之前:

template <class Serializer, class SerializedType>
void serialize(Serializer&, SerializedType) const:

template <class Serializer, class SerializedType>
void serialize(Serializer& s, const SerializedSeqCntr<SerializedType> &t);

...

否则,名称查找将无法在serialize() - 约束重载内找到正确的SerializedSeqCntr。是的,作为一个从属名称的函数,名称查找确实发生在实例化点;但是,只考虑在函数体上下文中可见的名称(除非ADL启动)。

也可能存在第三个问题;因为前者按值采用SerializedType,所以后备重载不优于约束重载;如果这不是意图,你还需要进一步限制后备。

...和一些理论:

(*)详细说明一下,当你调用一个函数模板时,你要么显式传递模板参数(如在foo<bar>()中),要么让编译器从函数参数的类型中推导出它们(如{ {1}})。有时候,这个过程不会成功。

这可能有三个原因:

  • 替换失败;也就是说,模板参数T 已成功推导或给出,但它出现在一个表达式中,如果在函数签名之外拼写,则会发生错误;简单地忽略函数重载;这就是SFINAE的全部内容。

  • 实例化所需的类型和函数执行替换时出现错误;该函数被忽略,程序格式不正确(如果这听起来很混乱,answer可能有帮助。)

  • 无法推导出模板参数,忽略函数重载;一个明显的例子是当模板参数没有出现在任何函数参数中但尚未明确指定时;另一个例子是当它出现的函数参数碰巧是非推导的上下文时,请参阅此answer以获得解释;你会看到,foo(some_bar)这个论点确实是非推论的。

(**)如前所述,SFINAE约束通常是非推断的;所以,如果你需要进行类型推导,你应该在自己的可推导论证中传递待推导的参数;这通常通过添加伪默认参数或通过返回类型来完成:

const SerializedFloating<SerializedType>&