coding

Шаблоны С++

В данной статье я хочу простыми словами и примерами объяснить работу шаблонов в Си++, зачем они вообще нужны и о работе с подаваемыми в шаблон типами: специализацией шаблонов, tag dispatch и SFINAE.

Итак, если вы все еще не очень разбираетесь в шаблонах, то вот вам его базовый синтаксис:

template <typename T> 

bool Equals(T lhs, T rhs)
{
   return lhs == rhs;
}

Что означает этот код?

Словом template мы обозначаем, что функция (или в целом участок кода далее) — является шаблоном (от англ. «template» — шаблон).

<typename T> — означает, что внутри этого шаблона будет какой-то тип данных, но мне в целом неважно, какой это будет конкретно тип. И имя, которое я назначаю этому типу (параметру) будет T (можно в целом назвать как угодно, просто принято программистами писать T — по первой букве слова template).

Кстати, вместо template <typename T> можно использовать template <class T>.

По большому счету, обе записи почти ничем не отличаются. typename более общее, обширное понятие, так как подаваемый тип данных в этом случае может быть не только классом, но и базовым, примитивным типом (int / char / float и т.п.)

Но если смотреть по стандартной библиотеке, то там они чаше всего пишут class, — это связано с тем, что typename ввели уже позже.

Кстати, в шаблонах можно также использовать несколько параметров, записывается это так: template <typename T, typename U>

Шаблоны еще называют одним из типом полиморфизма. Параметрический полиморфизм (parametric polymorphism). Это означает, что имплементация кода не изменяется, изменяется только параметр(ы).

Здесь для примера сразу вспомнились контейнеры STL, которые как раз написаны шаблонным классом, что позволяет передавать в контейнер любой тип данных:

std::vector<int> vector_of_ints;                    
std::vector<std::string> vector_of_string;
std::vector<char> vector_of_chars;

А теперь вернемся к нашей шаблонной функции Equals, с которой я начала эту статью.

По сути, она возвращает true, если оба параметра равны, false, если не равны. Все просто. Как 2 x 2. Давайте попробуем протестить такой кейс:

bool b = Equals(0.42f - 0.4f, 0.02f);

По идее арифметика проста: 0.42 — 0.4 = 0.02, значит функция должна вернуть true.

Но если вы протестите нашу шаблонную функцию с этим случаем, то убедитесь, что функция вернет false. Почему так произошло? Все дело в точности. В точности наши числа не оказались одинаковыми.

В связи с этим у нас возникает потребность в обработки таких случаев.

И можно это сделать несколькими способами. В данной статье мы рассмотрим 3 способа.

Template specialisation (или специализация шаблона)

// общий шаблон
template <typename T>
bool Equals(T lhs, T rhs)
{
   return lhs == rhs;
}

// специализация шаблона
template <>
bool Equals<float>(float lhs, float rhs)
{
   // реализация неточности float
   return true;
}

int main()
{
    bool b = Equals(0.42f - 0.4f, 0.02f); // здесь вызовется template specialization
    bool c = Equals(6, 6); // а здесь наш основной шаблон
    return 0;
}

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

Таким образом, мы создали два шаблона — один — общий, основной шаблон, другой — на случай работы с типом float.

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

Например, специализация шаблона реализовано в библиотеке type_traits, где, если взять например шаблонный класс «is_floating_point» возвращает true_type в шаблоне, где в качестве T передается float, double или long double (это как раз 3 специальных случая), а основной шаблон возвращает false_type.

так это прописано в std

Мы также можем написать шаблон, ограничив тип данных, который будет с ним работать. И сделать это можно с помощью tag dispatch или SFINAE.

Tag dispatch

(честно, не нашла, как это будет по-русски)

Дословно можно перевести как «отправка тэга». Приведу в пример сначала базовый код, который мы далее дополним.

// это основной шаблон, который будет вызываться первым
template <typename T>
bool Equals(T lhs, T rhs)
{
   return Equals(lhs, rhs); // здесь мы будем как бы "отправлять" в нужный шаблон
}

// шаблонная функция для float
template <typename T>
bool Equals(T lhs, T rhs)
{
    // реализация неточности float
    return true;
}

// шаблонная функция для остальных типов
template <typename T>
bool Equals(T lhs, T rhs)
{
    return (lhs == rhs);
}

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

Конкретно в нашем случае работы с floats, в библиотеке <type_traits> есть шаблонный класс is_floating_point, о котором я уже ранее писала, мы будем его использовать для определения приходящего типа данных.

Также мы будем использовать шаблон conditional (conditional в переводе с англ. «условие») присваивает типу true или false, в зависимости от условия. Можно представить этот шаблон как некое условие: если оно верно, то делаешь одно, если неверно — то другое.

template <typename T>
bool Equals(T lhs, T rhs)
{
  return Equals(lhs, rhs, typename conditional<is_floating_point< T >::value, true_type, false_type>::type{});
}

// шаблонная функция для float
template <typename T>
bool Equals(T lhs, T rhs, true_type)
{
	// реализация неточности float
	return true;
}

template <typename T>
bool Equals(T lhs, T rhs, false_type)
{
	return lhs == rhs;
}

// шаблонная функция для остальных типов
int main()
{
	Equals(1, 1);
	return 0;
}
return Equals(lhs, rhs, typename conditional::value, true_type, false_type>::type{});

Выглядит крипово и непонятно, да?

Сейчас все разберем 🙂

Наша первая шаблонная функция вызовет одну из шаблонных функций для float или общую для остальных типов, в зависимости от того, получим ли мы true_type или false_type.

По сути, conditional создаст объект, который будет либо true_type, либо false_type.

Кстати, в 14м стандарте можно записать наше выражение вот так:

return Equals(lhs, rhs, conditional_t::value, true_type, false_type>{});

А конструкция внутри conditional, is_floating_point<T> определит, является ли T типом float или нет, в итоге в value запишется либо true, либо false, а уже conditional, соответственно, определит, какой объект на основе этого создать.

Еще раз:

is_floating point — это шаблонный класс, внутри которого есть typedef под названием value (который может быть true или false).

conditional — это также шаблонный класс, и внутри этого шаблона есть typedef, который в зависимости от подаваемого первого параметра типа bool станет true_type или false_type.

SFINAE — «substitution failure is not an error»

В этом способе мы будем использовать шаблонную структуру enable_if

Что делает enable_if ? Он говорит, что если условие, которое было подано, верно, то компилятору можно рассматривать этот шаблон. Если нет, то компилятор будет искать тот шаблон, который подойдет под тип подаваемых аргументов, и либо найдет подходящий, либо вернет ошибку компиляции.

Есть несколько мест, куда мы можем вставить enable_if:

  1. Можно подавать его в качестве типа в шаблон. Так как мы не собираемся его использовать в шаблоне, поэтому имя давать не нужно. Мы просто пишем enable_if, чтобы компилятор понимал, нужно ли ему использовать этот шаблон или нет.
template <typename T, typename = enable_if_t<is_floating_point<T>::value>>

bool Equals(T lhs, T rhs)
{
	// реализация неточности float
	return true;
}

template <typename T, typename = enable_if_t<!is_floating_point<T>::value>>
bool Equals(T lhs, T rhs)
{
	return lhs == rhs;
}

Однако, в нашем случае такой код работать не будет.

Почему?

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

В целом, это тоже самое, что иметь вот такие две функции:

void Foo(int i = 1);

void Foo(int i = 2);

В общем, если бы мы просто использовали одну шаблонную функцию в таком формате, то все бы было ок.

2. Мы также можем поставить enable_if в лист аргументов и присвоить ему дефолтное значение (почему-то принято присваивать указатель на NULL)

template <typename T>
bool Equals(T lhs, T rhs, enable_if_t<is_floating_point<T>::value>* = nullptr)
{
	// реализация неточности float
	return true;
}

template <typename T>
bool Equals(T lhs, T rhs, enable_if_t<!is_floating_point<T>::value>* = nullptr)
{
	return lhs == rhs;
}

int main()
{
	Equals(1, 1);
	Equals(1.1f, 1.2f);
	return 0;
}

Такой код будет работать для обеих функций. Потому что теперь сигнатуры отличаются. enable_if_t<!is_floating_point<T>::value>* = nullptr и enable_if_t<is_floating_point<T>::value>* = nullptr это две разные сигнатуры, не смотря на то, что обе в конечном итоге по дефолтному значению дают нам указатель на null.

3. И последнее место, куда можно поместить enable_if — это на место возвращаемого значения. Только теперь мы не можем приравнивать выражение в указатель на null, мы должны указать тот тип данных, который нам хочется по итогу вернуть. То есть в нашем случае это bool.

template <typename T>
enable_if_t<is_floating_point<T>::value, bool> Equals(T lhs, T rhs)
{
	// реализация неточности float
	return true;
}

template <typename T>
enable_if_t<!is_floating_point<T>::value, bool> Equals(T lhs, T rhs)
{
	return lhs == rhs;
}

int main()
{
	Equals(1, 1);
	Equals(1.1f, 1.2f);
	return 0;
}

Кстати, во всех вариантах с enable_if_t я использовала 14й стандарт.

Как перезаписать эту строчку кода, чтобы работало даже в 98м?*

Ответ:

typename enable_if::value>::type* = nullptr

В enable_if изначально не прописано, что тип, который нам вернет выражение, действительно будет являться типом данных. Поэтому мы прописываем typename. Также мы должны записать результат выражения в переменную type, которая содержится внутри enable_if.

И напоследок, с шаблонами можно также делать вот такие кастомные штуки:

struct Foo
{
};

template<>
struct is_floating_point<Foo> : true_type{}; // it inherits from true_type

То есть мы создали структуру Foo, которую мы определили как true_type в качестве float.

В целом, можно также самому создать шаблонные структуры/классы с таким же функционалом, как enable_if или is_floating_point и использовать их в своем коде, не подключая никаких библиотек 🙂

Ведь по сути, вот весь исходный код enable_if:

template<bool Cond, class T = void> struct enable_if {};
template<class T> struct enable_if<true, T> { typedef T type; };

Если в enable_if придет true, то он определит тип и запишет его в T. В противном случае, T станет void.

На этом всё 🙂 Надеюсь, статья была для вас полезной.

p.s.: Если вы обнаружили какие-то неточности или ошибки в формулировках, то обязательно дайте мне об этом знать (в комментарии/по email или напишите мне в инстаграм @codinggirl_)

Автор

business.codinggirl@gmail.com

Комментарии

Олег
19 мая, 2021 в 2:35 пп

Класс! Спасибо за статью, проясняется enable_if !



10 сентября, 2021 в 7:01 пп

Ух ты, ты такая умная, подскажи с чего ты начинала?



Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *