【问题标题】:SFINAE-based serialization solution failing to instantiate an overloaded templated function in C++基于 SFINAE 的序列化解决方案无法在 C++ 中实例化重载模板函数
【发布时间】:2018-05-31 00:12:25
【问题描述】:

我正在尝试或多或少地序列化模板类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 时?

【问题讨论】:

    标签: c++ templates serialization sfinae rapidjson


    【解决方案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&amp; 参数和 sfinae 约束的虚拟默认参数或返回类型 (**)。


    第二个问题是“后备”重载应该在可能调用它的约束重载之前:

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

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


    也可能还有第三个问题;回退重载并不优于约束重载,因为前者按值采用 SerializedType;如果这不是意图,您还需要进一步限制回退。


    ...和一些理论:

    (*) 详细说明一下,当您调用函数模板时,您要么显式传递模板参数(如foo&lt;bar&gt;()),要么让编译器从函数参数的类型中推断出它们(如foo(some_bar) )。有时,此过程无法成功。

    这可能出于三个原因:

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

    • 在实例化类型和函数需要以执行替换时出现错误;该函数没有被忽略,程序格式错误(如果这听起来令人困惑,这个answer 可能会有所帮助)。

    • 模板参数无法推导,函数重载被忽略;一个明显的例子是模板参数没有出现在任何函数参数中但没有明确指定时;另一个例子是当一个函数参数恰好是一个非推导上下文时,请参阅answer 以获得解释;你会看到这个论点,比如const SerializedFloating&lt;SerializedType&gt;&amp; 确实是非演绎的。

    (**) 如前所述,SFINAE 约束通常是非推导的;所以,如果你需要类型推导工作,你应该将要推导的参数传递给它自己的可推导参数;这通常通过添加一个虚拟默认参数或通过返回类型来完成:

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

    【讨论】:

    • 谢谢!您的解决方案刚刚解决了问题。然而,我并没有完全理解它。我是 SFINAE 方法的新手,这是我第一次尝试类似的方法。您是否愿意详细说明“SFINAE 检查是非推断上下文”的含义?此外,什么可能是“sfinae 约束虚拟默认参数或返回类型”,或者我为什么需要一个? :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-01
    • 2016-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-25
    • 1970-01-01
    相关资源
    最近更新 更多