value
. Теперь вопрос заключается в том, что за тип — T
? Если мы не хотим указывать его явно, то ему следует зависеть от типов значений, переданных в конструктор. В случае передачи объектов-строк тип должен быть std::string
. При передаче целых чисел тип должен быть int
. Если мы передадим целые числа, числа с плавающей точкой и числа с удвоенной точностью, то компилятору следует определить, какой тип подходит всем значениям без потери точности. Для этого мы предоставляем явные правила выведения типов:
template
sum(Ts&& ... ts) -> sum<std::common_type_t >;
Согласно этим правилам компилятор может использовать типаж
std::common_ type_t
, который способен определить, какой тип данных подходит всем значениям. Посмотрим, как его применить:
sum s {1u, 2.0, 3, 4.0f};
sum string_sum {std::string{"abc"}, "def"};
std::cout << s.value << '\n'
<< string_sum.value << '\n';
В первой строке мы создаем объект типа sum на основе аргументов конструктора, имеющих типы
unsigned
, double
, int
и float
. Типаж std::common_type_t
возвращает тип double
, поэтому мы получаем объект типа sum
. Во второй строке мы предоставляем экземпляр типа std::string
и строку в стиле C. В соответствии с нашими правилами компилятор создает экземпляр типа sum
.При запуске этот код выведет значение
10
как результат сложения чисел и abcdef
в качестве результата объединения строк.Упрощаем принятие решений во время компиляции с помощью constexpr-if
В коде, содержащем шаблоны, зачастую необходимо по-разному выполнять определенные действия в зависимости от типа, для которого конкретный шаблон был специализирован. В С++17 появились выражения
constexpr-if
, позволяющие значительно упростить написание кода в таких ситуациях.
Как это делается
В этом примере мы реализуем небольшой вспомогательный шаблонный класс. Он может работать с разными типами, поскольку способен выбирать различные пути выполнения кода в зависимости от типа, для которого мы конкретизируем шаблон.
1. Напишем обобщенную часть кода. В нашем примере рассматривается простой класс, который добавляет значение типа
U
к элементу типа T
с помощью функции add
:
template
class addable
{
T val;
public:
addable(T v) : val{v} {}
template
T add(U x) const {
return val + x;
}
};
2. Представим, что тип
T
— это std::vector<что-то>
, а тип U
— просто int
. Каков смысл выражения «добавить целое число к вектору»? Допустим, нужно добавить данное число к каждому элементу вектора. Это делается в цикле:
template
T add(U x)
{
auto copy (val); // Получаем копию элемента вектора
for (auto &n : copy) {
n += x;
}
return copy;
}
3. Следующий и последний шаг заключается в том, чтобы объединить оба варианта. Если
T
— это вектор, состоящий из элементов типа U
, то выполняем цикл. В противном случае выполняем обычное сложение.
template
T add(U x) const {
if constexpr (std::is_same_v>) {
auto copy (val);
for (auto &n : copy) {
n += x;
}
return copy;
} else {
return val + x;
}
}
4. Теперь класс можно использовать. Посмотрим, насколько хорошо он может работать с разными типами, такими как
int
, float
, std::vector
и std::vector
:
addable{1}.add(2); // результат - 3
addable{1.0}.add(2); // результат - 3.0
addable{"aa"}.add("bb"); // результат - "aabb"
std::vector v {1, 2, 3};
addable>{v}.add(10);
// is std::vector{11, 12, 13}
std::vector sv {"a", "b", "c"};
addable>{sv}.add(std::string{"z"});
// is {"az", "bz", "cz"}
Как это работает
Новая конструкция
constexpr-if
работает точно так же, как и обычные конструкции if-else
. Разница между ними заключается в том, что значение условного выражения определяется во время компиляции. Весь код завершения, который компилятор сгенерирует из нашей программы, не будет содержать дополнительных ветвлений, относящихся к условиям constexpr-if
. Кто-то может сказать, что эти механизмы работают так же, как и макросы препроцессора #if
и #else
, предназначенные для подстановки текста, но в данном случае всему коду даже не нужно быть синтаксически правильным. Ветвления конструкции constexpr-if
должны быть синтаксически правильными, но неиспользованные ветви не обязаны быть семантически корректными.Чтобы определить, должен ли код добавлять значение
х
к вектору, задействуем типаж std::is_same
. Выражение std::is_same::value
вычисляется в логическое значение true, если A и B имеют один и тот же тип. В нашем примере применяется условие std::is_same>::value
, которое имеет значение true
, если пользователь конкретизировал шаблон для класса T = std::vector
и пробует вызвать функцию add
с параметром типа U = X
.В одном блоке
constexpr-if-else
может оказаться несколько условий (обратите внимание, что a
и b
должны зависеть от параметров шаблона, а не только от констант времени компиляции):
if constexpr (a) {
// что-нибудь делаем
} else if constexpr (b) {
// делаем что-нибудь еще
} else {
// делаем нечто совсем другое
}
С помощью C++17 гораздо легче как выразить, так и прочитать код, получающийся при метапрограммировании.
Дополнительная информация
Для того чтобы убедиться, каким прекрасным новшеством являются конструкции
constexpr-if
для C++, взглянем, как решалась та же самая задача до С++17:
template
class addable
{
T val;
public:
addable(T v) : val{v} {} template
std::enable_if_t>::value, T>
add(U x) const { return val + x; }
template
std::enable_if_t