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