Выбрать главу

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) {

      n += x;

    }

    return copy;

  }

};

Без конструкций constexpr-if этот класс работает для всех необходимых нам типов, но кажется очень сложным. Как же он работает?

Сами реализации двух разных функций add выглядят просто. Все усложняет объявление возвращаемого типа — выражение наподобие std::enable_if_t<условие, тип> обращается в тип, если выполняется условие. В противном случае выражение std::enable_if_t ни во что не обращается. Обычно такое положение дел считается ошибкой. Далее мы рассмотрим, почему в нашем случае это не так.

Для второй функции add то же условие используется противоположным образом. Следовательно, условие может иметь значение true только для одной из двух реализаций в любой момент времени.

Когда компилятор видит разные шаблонные функции с одинаковым именем и должен выбрать одну из них, в ход вступает важный принцип: он обозначается аббревиатурой SFINAE, которая расшифровывается как Substitution Failure is not an Error («Сбой при подстановке — не ошибка»). В данном случае это значит, что компилятор не генерирует ошибку, если возвращаемое значение одной из функций нельзя вывести на основе неверного шаблонного выражения (т.е. std::enable_if, когда условие имеет значение false). Он просто продолжит работу и попробует обработать другие реализации функции. Вот и весь секрет.