Как выполняется специализация вложенных шаблонов C++

У меня есть шаблонная функция, определенная как:

template<typename TObject> TObject Deserialize(long version, const Value &value)

что мне нужно сделать, так это написать специализацию, которая будет принимать вектор, определенный как:

template<typename TNum, int cnt> class Vec

и по-прежнему имеет доступ к cnt и TNum.

я безуспешно пытался

template<typename TNum, int cnt> Vec<TNum, cnt> Deserialize<Vec<TNum, cnt>>(long version, Value &value)

приводит к ошибке: незаконное использование явных аргументов шаблона

Как правильно это сделать?


person Tomáš Dřínovský    schedule 28.08.2017    source источник
comment
Вы не можете частично специализировать функции. Таким образом, вам нужно скрыть фактическую реализацию в статическом методе фиктивной структуры шаблона и специализировать всю эту структуру.   -  person HolyBlackCat    schedule 28.08.2017
comment
Только вектор или любой контейнер?   -  person rustyx    schedule 28.08.2017
comment
В моем случае Vec — это математический вектор из внешней библиотеки.   -  person Tomáš Dřínovský    schedule 28.08.2017
comment
@HolyBlackCat Я бы хотел, чтобы этот совет не давался так быстро. 9/10 раз правильный ответ - перегрузить, а не пересылать структуру. Функции не имеют частичной специализации отчасти потому, что она им просто не нужна.   -  person Nir Friedman    schedule 28.08.2017
comment
@NirFriedman Вы правы, но функция OP не имеет ни одного аргумента в зависимости от параметра шаблона. IIRC, если они не хотят использовать отправку тегов, они должны специализироваться на структуре.   -  person HolyBlackCat    schedule 28.08.2017
comment
@HolyBlackCat Конечно, но даже в этом случае, как правило, лучше реализовать с помощью функций, у вас больше гибкости, и это чище, чтобы разрешить специализацию пользователя.   -  person Nir Friedman    schedule 29.08.2017
comment
@HolyBlackCat В моем ответе используются теги, но, возможно, это не совсем та отправка тегов, о которой вы думали; он по-прежнему использует механизмы частичной специализации, в отличие от действительно разных перегрузок, использующих true_type и false_type на основе некоторой информации о признаках.   -  person Nir Friedman    schedule 29.08.2017
comment
@NirFriedman Это та отправка тегов, о которой я думал. Вы правы, в данном случае это выглядит лучше, чем специализация структуры.   -  person HolyBlackCat    schedule 29.08.2017


Ответы (3)


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

#include <functional>
#include <iostream>
#include <type_traits>
#include <vector>
#include <array>

template <class T>
struct tag{};

template<typename TObject> 
TObject Deserialize_impl(long version, tag<TObject>) {
    std::cerr << "generic\n";
    return {};
}

template<typename T, std::size_t N> 
std::array<T,N> Deserialize_impl(long version, tag<std::array<T,N>>) {
    std::cerr << "special\n";
    return {};
}

template<typename TObject> 
TObject Deserialize(long version) {
    return Deserialize_impl(version, tag<TObject>{});
}


int main() {
    Deserialize<int>(0);
    Deserialize<std::array<int,3>>(0);

    return 0;
}

Живой пример: http://coliru.stacked-crooked.com/a/9c4fa84d2686997a

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

person Nir Friedman    schedule 28.08.2017
comment
Я бы даже назвал импл одним и тем же именем: разное количество аргументов делает его довольно безопасным и делает расширение ADL менее уродливым. - person Yakk - Adam Nevraumont; 29.08.2017
comment
@Yakk Справедливое замечание; Я думаю, что оставлю ответ как есть для максимальной ясности для людей, немного менее знакомых с перегрузкой (чтобы они не приняли его за рекурсию), но я согласен с вами. - person Nir Friedman; 29.08.2017

Хотя функциональная отправка тегов — хороший подход, вот версия специализации класса для сравнения. И то, и другое имеет свое применение, и я не думаю, что какое-либо из этих решений является прискорбным по своей сути, но, возможно, одно из них больше соответствует вашему личному стилю. Для любого написанного вами класса, которому требуется собственный обработчик десериализации, просто напишите специализацию класса Deserializer:

#include <iostream>
#include <string>
using namespace std;

using Value = std::string;

// default deserialize function 
template <typename TObject>
struct Deserializer {
    static TObject deserialize(long version, const Value &value) {
        std::cout << "default impl\n";
        return TObject();
    }
};

// free standing function (if you want it) to forward into the classes
template <typename TObject>
TObject deserialize(long version, const Value &value) {
    return Deserializer<TObject>::deserialize(version, value);
}

// Stub example for your Vec class
template<typename TNum, int cnt> class Vec { };

// Stub example for your Vec deserializer specialization
template <typename TNum, int cnt> struct Deserializer<Vec<TNum, cnt>> {
    static auto deserialize(long version, const Value &value) {
        std::cout << "specialization impl: cnt=" << cnt << "\n";
        return Vec<TNum, cnt>();
    }
};

int main() {
    Value value{"abcdefg"};
    long version = 1;

    deserialize<int>(version, value);
    deserialize<Vec<int, 10>>(version, value);
}
person Chris Uzdavinis    schedule 28.08.2017
comment
У этого подхода есть заметные недостатки, хотя в этом случае, когда вы возвращаете только рассматриваемый тип и не принимаете его, это менее очевидно. Один из них заключается в том, что он гораздо хуже работает с полиморфными типами; специализация плохо сочетается с наследованием. Функции также дают вам бесплатное преобразование в const; иногда со специализацией вас даже могут укусить, потому что T и const T - это разные типы! - person Nir Friedman; 29.08.2017
comment
@NirFriedman Это хорошие моменты, спасибо. :) Но я не вижу, как помеченная отправка, использующая Tag‹Vec› и Tag‹DerivedVec›, будет работать по-разному, поскольку экземпляры класса Tag не имеют полиморфных отношений. - person Chris Uzdavinis; 29.08.2017
comment
Как я уже сказал, здесь вы этого не увидите, потому что вы возвращаете разные типы, которые в любом случае не могут быть полиморфными. Но если бы вы принимали тип по ссылке, вы бы получили это преобразование бесплатно (а также квалификацию const). Главное помнить, что дублирование специализации с перегрузкой тривиально, мой ответ показывает это. Но перегрузка дает вам доступ ко множеству полезных техник, которые требуют гораздо больше усилий, чтобы воспроизвести их со специализацией. - person Nir Friedman; 29.08.2017

В идеале в этой ситуации Vec должен отражать собственные параметры шаблона в виде элементов Vec::value_type и Vec::size(), которые должны быть constexpr.

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

template< typename >
struct vector_traits;

template< typename TNum, int cnt >
struct vector_traits< Vec< TNum, cnt > > {
    typedef TNum value_type;
    constexpr static int size = cnt;
};

template<typename TVec> TVec Deserialize(long version, Value &value) {
    typedef vector_traits< TVec > traits;
    typedef typename traits::value_type TNum;
    constexpr static int cnt = traits::size;
    …
}

Это решение вписывается в любую существующую функцию и даже делает подписи чище. Кроме того, функция более гибкая, потому что вы можете адаптировать ее, добавляя traits специализаций вместо целых новых перегрузок.

person Potatoswatter    schedule 28.08.2017
comment
Этот подход предполагает, что Deserialize принимает для десериализации только векторные типы. Что делать, если есть несвязанный класс Foo, который необходимо десериализовать? Специализировать vector_traits, чтобы он компилировался? - person Chris Uzdavinis; 29.08.2017
comment
Ясно, что тело функции может работать только с типами, которые концептуально являются векторами. Отключите сигнатуру функции для других типов (по SFINAE или Concepts) и предоставьте больше перегрузок. - person Potatoswatter; 29.08.2017