enable_if
та концепти C++20 — це два способи обмеження шаблонів для певних типів. Давайте порівняємо ці два методи.
Розпочнемо з простого програмного коду, який виводить різні числа. Щоб уникнути втрати точності та витрат на перетворення між числовими типами, ми використовуємо шаблони для цієї функції.
#include <iostream>
template <typename T>
void print(T a){
std::cout << "Це число: " << a << std::endl;
}
int main(){
const int a = 2;
const long b = 3;
const float c = 4;
const double d = 5;
print(a);
print(b);
print(c);
print(d);
}
Компіляція за допомогою g++ main.cpp
і виконання дає результат:
Це число: 2
Це число: 3
Це число: 4
Це число: 5
Якщо ми хочемо змінити поведінку для цілих та з плаваючою точкою чисел, ми повинні обмежити шаблони для цих типів.
До C++20 ми використовували enable_if
таким чином.
template <
typename T,
std::enable_if_t<std::is_integral<T>::value, bool> dummyparam = true // Можна опустити ім’я dummyparam
> void print(T a){
std::cout << "Це ціле число: " << a << std::endl;
}
template <
typename T,
std::enable_if_t<std::is_floating_point<T>::value, bool> dummyparam = true // Можна опустити ім’я dummyparam
> void print(T a){
std::cout << "Це число з плаваючою точкою: " << a << std::endl;
}
Компіляція та повторний запуск дають наступний результат.
Це ціле число: 2
Це ціле число: 3
Це число з плаваючою точкою: 4
Це число з плаваючою точкою: 5
Як C++ визначає, який шаблон використовувати для кожного типу? Магія криється в другому шаблонному параметрі.
Для першого шаблону перший шаблонний параметр — це наш знайомий T
, але далі ми маємо ось таку штуку.
std::enable_if_t<std::is_integral<T>::value, bool> dummyparam = true
Давайте розглянемо процес підстановки для int
або float
. Для int
шаблон std::is_integral
(див. https://en.cppreference.com/w/cpp/types/is_integral) створює структуру з членом static const bool
на ім'я value
, який дорівнює true
(для float
він буде false
). is_integral_v
дає нам std::is_integral<T>::value
, тому це спрощується до
std::enable_if_t<std::is_integral<T>::value, bool> dummyparam = true // для int
std::enable_if_t<std::is_integral<T>::value, bool> dummyparam = true // для float
Далі розглянемо, що робить enable_if
(див. https://en.cppreference.com/w/cpp/types/enable_if). Він створює структуру на основі двох шаблонних параметрів. Перший — це bool
, а другий — тип.
Якщо перший аргумент дорівнює true
, то створюється структура з обмеженим typedef
під ім'ям type
, який встановлений у другий шаблонний параметр. Отже, при підстановці для int
std::enable_if<T>::type
буде bool
. _t
дає нам те ж саме, що й ::type
. Тому вираз спрощується до
bool dummyparam = true
dummyparam
— це необов'язковий другий шаблонний параметр зі значенням за замовчуванням, яке дорівнює true
. Але зверніть увагу, що dummyparam
насправді не використовується в самій дефініції шаблону. В більшості випадків у коді, що використовує enable_if
, ім'я цього параметра взагалі опускається. Крім того, його тип навіть не має значення. Головне, щоб він відповідав значенню за замовчуванням, що також не важливо. Ми могли б так само легко оголосити
std::enable_if_t<char> = 'a'
і це працювало б так само. Шаблон має просто розв'язатися, коли підставляється int
.
Щоб зрозуміти, чому цей невикористовуваний шаблонний параметр корисний, розглянемо випадок з float
.
std::enable_if
створює порожню структуру без жодного typedef
. Тому компілятор не може розв'язати ::type
, і підстановка шаблону не вдалася, оскільки ми не знаємо тип dummyparam
. Це викликає відому проблему SFINE (substitution failure is not an error — невдача підстановки не є помилкою). Коли компілятор намагається й не може підставити тип у шаблон, це не призводить до помилки компіляції. Замість цього компілятор намагається знайти іншу дефініцію шаблону, яка підходить.
У цьому випадку float
не пройде інстанціацію шаблону для першого шаблону, але успішно підійде для версії з числовими типами з плаваючою точкою.
enable_if
може бути складним, але ми можемо досягти того ж результату за допомогою концептів.
template <typename T>
requires std::is_integral_v<T>
void print(T a){
std::cout << "Це ціле число: " << a << std::endl;
}
template <typename T>
requires std::is_floating_point_v<T>
void print(T a){
std::cout << "Це число з плаваючою точкою: " << a << std::endl;
}
Компілюючи за допомогою g++ -std=c++20 -fconcepts main.cpp
(переконайтеся, що ваша версія компілятора достатньо нова) і повторно запустивши, отримаємо той самий результат, але з більш читабельним кодом.
Умови requires
можуть використовувати більш складні багаторазові метафункції, визначені як concept
(концепти).
// концепт для цілісних типів
template <typename T>
concept integer_concept = std::is_integral_v<T>;
template <typename T>
requires integer_concept<T>
void print(T a){
std::cout << "Це ціле число: " << a << std::endl;
}
// концепт для типів з плаваючою точкою
template <typename T>
concept float_concept = std::is_floating_point_v<T>;
template <typename T>
requires float_concept<T>
void print(T a){
std::cout << "Це число з плаваючою точкою: " << a << std::endl;
}
У цьому випадку концепти є тривіальними обгортками для std::is_integral
та std::is_floating_point
, але загалом концепти дозволяють реалізовувати більш складну логіку.
Як бонус, якщо ви зробите помилку, повідомлення про помилки компілятора будуть значно кориснішими, ніж при використанні enable_if
.
- Репозиторій на Github: https://github.com/mpnunez/C-concepts
- Відео на Youtube: https://www.youtube.com/watch?v=7nqKdoxzkTk
Перекладено з: enable_if versus c++20 Concepts