Решение для сериализации на основе SFINAE не может создать экземпляр перегруженной шаблонной функции в C++

Я пытаюсь сериализовать шаблонный класс MState<T> более или менее в общем виде. Для этого у меня есть родительский абстрактный класс MVariable, который реализует несколько функций сериализации в этой форме:

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

Я хочу разрешить T быть почти любым. Сериализация выполняется в формате JSON через RapidJSON:: Писатель. Из-за этого мне нужно использовать определенные функции-члены (например, Writer::String, Writer::Bool, Writer::Uint...), чтобы получить правильное форматирование для каждого типа T.

Сериализацию базовых типов и STL-контейнеров обеспечит MVariable. Однако вместо предоставления каждого отдельного типа (например, замены SOME_SPECIFIC_TYPE на float, double, bool и т. д.) я попытался реализовать решение на основе 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 основана на этом, а реализация has_serialize заимствована из здесь. Оба были проверены и, кажется, работают правильно:

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, а не при попытке сериализовать tvar0 или tvar1?


person Carles Araguz    schedule 17.12.2017    source источник


Ответы (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 или возвращаемый тип (**).


Вторая проблема заключается в том, что «запасная» перегрузка должна предшествовать ограниченным перегрузкам, которые могут ее вызывать:

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>()), либо позволяете компилятору вывести их из типов аргументов функции (как в foo(some_bar)). Иногда этот процесс не может быть успешным.

Это может произойти по трем причинам:

  • происходит сбой замещения; то есть параметр шаблона T был успешно выведен или задан, но он также появляется в выражении, для которого возникла бы ошибка, если бы оно было указано вне сигнатуры функции; перегрузка функции просто игнорируется; это то, что SFINAE все о.

  • возникла ошибка при создании экземпляров типов и функций, необходимых для выполнения подстановки; функция не игнорируется, программа имеет неправильный формат (если это звучит странно, этот ответ может поможет).

  • аргумент шаблона не может быть выведен, перегрузка функции игнорируется; очевидным примером является случай, когда параметр шаблона не появляется ни в одном аргументе функции, но не указан явно; другой пример: когда аргумент функции, в котором он появляется, оказывается невыведенным контекстом, см. этот ответ для объяснения; вы увидите, что аргумент, скажем, const SerializedFloating<SerializedType>& действительно не выводится.

(**) как уже было сказано, ограничения SFINAE обычно не выводятся; поэтому, если вам нужно, чтобы дедукция типа работала, вы должны передать параметр to-be-deduced в виде собственного выводимого аргумента; обычно это делается либо путем добавления фиктивного аргумента по умолчанию, либо через тип возвращаемого значения:

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 );
person Massimiliano Janes    schedule 17.12.2017
comment
Благодарю вас! Ваше решение только что решило проблему. Однако я не совсем это понял. Я новичок в подходах SFINAE и впервые пробую что-то подобное. Не могли бы вы уточнить, что вы подразумеваете под проверками SFINAE, которые не являются дедуцируемыми контекстами? Более того, что может быть фиктивным аргументом по умолчанию или типом возвращаемого значения, ограниченным sfinae, или зачем он мне нужен? :) - person Carles Araguz; 18.12.2017