Специализация шаблона класса для нескольких типов

#c #c 11 #templates

#c #c 11 #шаблоны

Вопрос:

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

У меня есть шаблон класса, который должен принимать каждый тип. Простой пример:

 template <class T>
class State {
  public:
    void set(T newState);
    T get();
  private:
    T state;
};

template <class T>
void State<T>::set(T newState){
  state = newState;
}

template <class T>
T State<T>::get(){
  return state;
}
  

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

Например, эта специализация для типа int, но вместо того, чтобы писать это только для типа int, я также хотел бы разрешить все другие варианты int и float. Я нашел std::is_arithmetic, но понятия не имею, как его использовать для достижения этой цели.

 template <>
class State <int> {
  public:
    void set(int newState);
    int get();
    int multiplyState(int n);
  private:
    int state;
};

void State<int>::set(int newState){
  state = newState;
}

int State<int>::get(){
  return state;
}

int State<int>::multiplyState(int n){
  return state*n;
}
  

Комментарии:

1. поиск std::enable_if , sfinae и частичная специализация 😉

Ответ №1:

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

 #include <type_traits>

template <class T, typename = void>
class State
{
    T state;

public:
    void set(T newState)
    {
        state = newState;
    }

    T get()
    {
      return state;
    }
};

template <typename T>
class State<T, std::enable_if_t<std::is_arithmetic_v<T>>>
{
    T state;

public:
    void set(int newState)
    {
        state = newState;
    }

    int get()
    {
        return state;
    }

    int multiplyState(int n)
    {
        return state*n;
    }
};
  

живой пример здесь

Хитрость здесь заключается в использовании второго параметра шаблона (который может быть неназванным и ему присваивается аргумент по умолчанию). Когда вы используете специализацию вашего шаблона класса, например, State<some_type> , компилятор должен выяснить, какой из шаблонов следует использовать. Для этого необходимо каким-то образом сравнить приведенные аргументы шаблона с каждым шаблоном и решить, какой из них лучше всего подходит.

Способ, которым это сопоставление фактически выполняется, заключается в попытке вывести аргументы каждой частичной специализации из заданных аргументов шаблона. Например, в случае State<int> аргументами шаблона будут int и void (последнее присутствует из-за аргумента по умолчанию для второго параметра основного шаблона). Затем мы пытаемся вывести аргументы в пользу нашей единственной частичной специализации

 template <typename T>
class State<T, std::enable_if_t<std::is_arithmetic_v<T>>>;
  

из аргументов шаблона int, void . Наша частичная специализация имеет единственный параметр T , который может быть непосредственно выведен из первого аргумента шаблона, который должен быть int . И с этим мы уже закончили, поскольку вывели все параметры (здесь есть только один). Теперь мы подставляем выведенные параметры в частичную специализацию: State<T, std::enable_if_t<std::is_arithmetic_v<T>>> . В итоге мы получаем State<int, void> , который соответствует списку начальных аргументов int, void . Следовательно, применяется частичная специализация шаблона.

Теперь, если бы вместо этого мы написали, State<some_type> где some_type не является арифметическим типом, тогда процесс был бы таким же вплоть до того момента, когда мы успешно вывели параметр для частичной специализации, который должен быть some_type . Опять же, мы подставляем параметр обратно в частичную специализацию State<T, std::enable_if_t<std::is_arithmetic_v<T>>> . Однако std::is_arithmetic_v<some_type> теперь это будет false , что приведет к тому, что std::enable_if_t<…> определение не будет выполнено и замена завершится неудачей. Поскольку сбой замены не является ошибкой в данном контексте, это просто означает, что частичная специализация здесь не является опцией и вместо нее будет использоваться основной шаблон.

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

Комментарии:

1. Не могли бы вы объяснить, что означает второй аргумент (?) в случае <class T, typename = void> и class State <T, std::enable_if_t<std::is_arithmetic_v<T>>> мне трудно найти хорошее объяснение этого синтаксиса.

2. @PTS Я обновил свой ответ тем, что, надеюсь, является полезным объяснением.

3. Спасибо за дальнейшее объяснение. К сожалению, это не компилируется с C 11. Я попытался enable_if и is_arithmetic вместо godbolt.org/z/VocB39 но это, похоже, неверно. Требуется ли для этого C 17 или он должен быть сконструирован иначе для C 11?

4. Это godbolt.org/z/1qgSY1 кажется, что он компилируется, но затем не может использовать специализацию, а вместо этого, похоже, использует базовый шаблон.

5. Для моего примера действительно требуется C 17. Для C 11 вам пришлось бы использовать typename std::enable_if<std::is_arithmetic<T>::value>::type> .

Ответ №2:

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

 // StateBase only contains the extra multiplyState member when State tells it to
// define it, based on T being an arithmetic type
template <class D, class T, bool has_multiply>
struct StateBase {};

template <class D, class T>
struct StateBase<D, T, true> {
    T multiplyState(int n) {
        return static_cast<D*>(this)->state * n;
    }
};

template <class T>
class State : public StateBase<State<T>, T, std::is_arithmetic<T>::value> {
  public:
    // no need to duplicate these declarations and definitions
    void set(T newState);
    T get();
  private:
    // note that we write State::StateBase to force the injected-class-name to be found
    friend struct State::StateBase;
    T state;
};
  

Ссылка на Coliru

Комментарии:

1. Спасибо, это действительно был бы мой следующий вопрос.

2. Не могли бы вы, возможно, объяснить использование структур здесь? Это не знакомая мне концепция, но я часто вижу использование структур в сочетании с шаблонами классов. Работает ли это как создание частичной части класса, которая вставляется компилятором в фактическое определение класса?

3. @PTS Структура — это просто класс, который по умолчанию имеет открытый доступ для своих членов и базовых классов.

4. Ах да, я это смутно помню. Я не был осведомлен о концепции друзей в C 🙂