C++17 STL Стандартная библиотека шаблонов — страница 7 из 119

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