#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;
};
Комментарии:
1. Спасибо, это действительно был бы мой следующий вопрос.
2. Не могли бы вы, возможно, объяснить использование структур здесь? Это не знакомая мне концепция, но я часто вижу использование структур в сочетании с шаблонами классов. Работает ли это как создание частичной части класса, которая вставляется компилятором в фактическое определение класса?
3. @PTS Структура — это просто класс, который по умолчанию имеет открытый доступ для своих членов и базовых классов.
4. Ах да, я это смутно помню. Я не был осведомлен о концепции друзей в C 🙂