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