Несколько новых возможностей C++17 позволяют написать более компактный и ясный код. Это особенно важно при шаблонном мета-программировании, результат которого часто выглядит жутко…


Например если вы хотите выразить if, который вычисляется во время компиляции, вы будете вынуждены написать код используя приём SFINAE (например enable_if) или статическую диспетчеризацию (tag dispatching). Такие выражения тяжело понять, и они выглядят как магия для разработчиков, незнакомых с продвинутыми шаблонами мета-программирования.


К счастью, с появлением C++17 мы получаем if constexpr. Теперь большинство приёмов SFINAE и статической диспетчеризации отпадает, и код уменьшается, становится похожим на "обычный" if.


Эта статься демонстрирует несколько приёмов использования if constexpr.


Введение


Статический if в форме if constexpr полезная возможность, появившаяся в C++17. Недавно на сайте Meeting C++ была опубликована статься о том, как автор статьи Jens упростил код, используя if constexpr: Как if constexpr упрощает ваш код в C++17.


Я нашёл пару дополнительных примеров, которые могут продемонстрировать, как работает новая возможность.


  • Сравнение чисел
  • Фабрики с переменным числом аргументов

Я надеюсь, что эти примеры помогут вам понять статический if из C++17.
Но для начала я бы хотел освежить основы enable_if.


Для чего нужен if во время компиляции?


Услышав об этом в первый раз, возможно вы спросите, зачем нужен статический if и эти сложные шаблонные выражения… Разве нормальный if не будет работать?


Рассмотрим пример:


template <typename T>
std::string str(T t) {
  if (std::is_same_v<T, std::string>) // строка или преобразуемый в строку
    return t;
  else
    return std::to_string(t);
}

Эта функция может служить простым инструментом для вывода текстового представления объектов. Так как to_string не принимает параметр типа std::string, мы можем проверить это и просто вернуть t если t — string. Звучит просто… Но давайте попробуем скомпилировать этот код:


// код, который вызывает нашу функцию
auto t = str("10"s);

Мы получим что-то похожее на это:


In instantiation of 'std::__cxx11::string str(T) [with T = std::__cxx11::basic_string<char>; std::__cxx11::string = std::__cxx11::basic_string<char>]': required from here error: no matching function for call to 'to_string(std::__cxx11::basic_string<char>&)' return std::to_string(t);


is_same даёт true для используемого типа (string), и мы можем просто вернуть t без преобразований… но что пошло не так?


Главная причина в этом: компилятор попытался разобрать обе условные ветви и нашёл ошибку в случае else. Он не может отбросить "неправильный" кода в нашем частном случае конкретизации шаблона.


Вот для этого нам нужен статический if, который будет "исключать" код и компилировать только тот блок, который подходит условию.


std::enable_if


Один из способов написать статический if в C++11/14 — использовать enable_ifenable_if_v начиная с C++14). Он имеет достаточно странный синтаксис::


template< bool B, class T = void >  
struct enable_if;

enable_if выводит тип T, если условие B истинно. Иначе, согласно SFINAE, частичная перегрузка функции удаляется из доступных перегрузок фунции.


Мы можем переписать наш простой пример так:


template <typename T>
std::enable_if_t<std::is_same_v<T, std::string>, std::string> str(T t) {
  return t;
}

template <typename T>
std::enable_if_t<!std::is_same_v<T, std::string>, std::string> str(T t) {
  return std::to_string(t);
}

Это не просто, не так ли?


Я использовал enable_if, чтобы отделить случай, когда тип — строка… Но точно такой же эффект можно достичь простой перегрузкой функции, избежав использование enable_if.


Далее мы упростим подобный код с помощью if constexpr из C++17. После этого мы сможем быстро переписать нашу функцию str.


Использование первое — сравнение чисел


Начнём с простого примера: функция close_enough, работающая с двумя числами. Если числа не с плавающей точкой (например, когда мы имеем два целочисленных int), мы можем просто сравнить их. Для чисел с плавающей точкой лучше использовать некоторую малую величину epsilon.


Я нашёл этот пример в Практическая головоломка современного C++ (Practical Modern C++ Teaser) — фантастическое введение в возможности современного C++ от Patrice Roy. Он любезно разрешил мне включить его пример.


Версия для C++11/14:


template <class T>
constexpr T absolute(T arg) {
  return arg < 0 ? -arg : arg;
}

template <class T>
constexpr enable_if_t<is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
  return absolute(a - b) < static_cast<T>(0.000001);
}

template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
  return a == b;
}

Как вы видите, здесь используется enable_if. Это очень похоже на нашу функцию str. Код проверяет, удовлетворяет ли тип входящих чисел условию is_floating_point. Затем компилятор может удалить одну их перегрузок функций.


А сейчас посмотрим, как это делается в C++17:


template <class T>
constexpr T absolute(T arg) {
  return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T>
constexpr bool close_enough(T a, T b) {
  if constexpr (is_floating_point_v<T>) // << !!
    return absolute(a - b) < precision_threshold<T>;
  else
    return a == b;
}

Это всего одна функция, которая в основном выглядит как нормальная функция. С почти "нормальным" if .


if constexpr вычисляется во время компиляции и затем пропускается код одной из ветвей выражения.


Здесь используются чуть больше возможностей C++17. Вы видите, какие?


Использование второе — фабрика с переменным количеством параметров


В главе 18 книги "Эффективное использование С++" Скотта Майрса описывается метод, названный makeInvestment:


template<typename... Ts> 
std::unique_ptr<Investment> 
makeInvestment(Ts&&... params);

Это — фабричный метод, который создаёт наследников класса Investment, и главное преимуществ в нём — поддержка различного количества параметров!


Для примера, ниже предлагаются типы наследников:


class Investment {
public:
    virtual ~Investment() { }
    virtual void calcRisk() = 0;
};

class Stock : public Investment {
public:
    explicit Stock(const std::string&) { }
    void calcRisk() override { }
};

class Bond : public Investment {
public:
    explicit Bond(const std::string&, const std::string&, int) { }
    void calcRisk() override { }
};

class RealEstate : public Investment {
public:
    explicit RealEstate(const std::string&, double, int) { }
    void calcRisk() override { }
};

Пример из книги слишком идеализированный и не рабочий — он работает, пока конструкторы ваших классов принимают одинаковое число и одинаковые типы входных аргументов.
Скотт Майрес комментирует в исправлениях и дополнениях к его книге "Эффективное использование С++" так:


Интерфейс makeInvestment не практичный, потому что предполагается, что наследники могут быть созданы из одних и тех же наборов аргументов. Это особенно заметно в реализации выбора конструируемого объекта, где аргументы передаются в конструкторы всех классов с помощью механизма perfect-forwarding (идеальная передача).

Для примера, если у вас есть два класса, конструктор одного принимает два аргумента, а другого — три, то такой код не будет компилироваться:


// псевдокод:
Bond(int, int, int) { }
Stock(double, double) { }
make(args...) {
  if (bond)
     new Bond(args...);
  else if (stock)
     new Stock(args...)
}

Если вы напишите make(bond, 1, 2, 3), то тогда выражение под else не будет скомпилировано, так нет подходящего конструктора для Stock(1, 2, 3)! Чтобы это заработало, нам нужно что-то похожее на static if — компилировать это только тогда, когда это удовлетворяет условию, иначе отбросить.


Вот код, который мог бы работать:


template <typename... Ts> 
unique_ptr<Investment> 
makeInvestment(const string &name, Ts&&... params) {
    unique_ptr<Investment> pInv;

    if (name == "Stock")
        pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...);
    else if (name == "Bond")
        pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...);
    else if (name == "RealEstate")
        pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...);

    // вызов дополнительных методов для инициализации pInv...

    return pInv;
}

Как мы видим, "магия" происходит внутри функции constructArgs.


Основания идея заключается в возврате unique_ptr<Type>, когда тип Type конструируется из данного набора атрибутов, или nullptr в противном случае.


До C++17


В этом случае мы использовали бы std::enable_if так:


// до C++17
template <typename Concrete, typename... Ts>
enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>>
constructArgsOld(Ts&&... params) {
    return std::make_unique<Concrete>(forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> >
constructArgsOld(...) {
    return nullptr;
}

std::is_constructible позволяет быстро проверить, будет ли данный тип конструироваться из заданного списка аргументов. // @cppreference.com

В C++17 немного проще, появился новый помощник:


is_constructible_v = is_constructible<T, Args...>::value;

Так что мы можем сделать код немного короче… Однако, использование enable_if всё ещё ужасно и сложно. Как насчёт C++17?


С if constexpr


Обновлённая версия:


template <typename Concrete, typename... Ts>
unique_ptr<Concrete> constructArgs(Ts&&... params) {  
  if constexpr (is_constructible_v<Concrete, Ts...>)
      return make_unique<Concrete>(forward<Ts>(params)...);
   else
       return nullptr;
}

Мы можем даже расширить функциональность логироваием действий, используя свёртку выражения:


template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs(Ts&&... params) { 
    cout << __func__ << ": ";
    // свёртка:
    ((cout << params << ", "), ...);
    cout << "\n";

    if constexpr (std::is_constructible_v<Concrete, Ts...>)
        return make_unique<Concrete>(forward<Ts>(params)...);
    else
       return nullptr;
}

Клёво… не так ли?


Весь сложный синтаксис выражений с enable_if ушёл прочь; нам даже не нужна перегрузка функции. Мы можем написать выразительный код всего лишь в одной функции.


В зависимости от результата вычисления условия выражения if constexpr только один блок кода будет компилироваться. В нашем случае, если объект может быть сконструирован из заданного набора атрибутов, тогда мы компилируем вызов make_unique. Если нет, то возвращаем nullptrmake_unique даже не компилируется).


Заключение


Условные выражения времени компиляции — замечательная возможность, которая сильно упрощает использование шаблонов. Кроме того, код становится яснее, чем при использовании существовавших ранее решений: статической диспетчеризации (tag dispatching) или enable_if (SFINAE). Сейчас вы можете выразить свои намерения "похоже" на код в рантайме.


В этой статье рассматривались только простые выражения, и я призываю вас исследовать более широко применимость новых возможностей.


Возвращаясь назад к нашему примеру функции str: можете ли вы сейчас переписать её используя if constexpr?

Комментарии (28)


  1. Antervis
    25.03.2018 18:46

    Обновлённая версия:

    Клёво… не так ли? :)

    Да не особо. Ошибся в порядке/кол-ве аргументов в constructArgs(...) и втихую получил nullptr, чтобы потом что? В рантайме на него проверять? Уж лучше static_assert втыкать


    1. sergio_nsk Автор
      25.03.2018 20:56
      -1

      Да кто же ограничивает и мешает вставить static_assert в else.


      1. Antervis
        26.03.2018 11:58

        так это же тавтология:

        Пример:
        template <typename Concrete, typename... Ts>
        std::unique_ptr<Concrete> constructArgs(Ts&&... params) {
            static_assert(std::is_constructible_v<Concrete, Ts...>,
                "constructArgs(...) failed: Type is not constructible from args");
            return make_unique<Concrete>(forward<Ts>(params)...);
        }
        


  1. kesn
    25.03.2018 20:56
    -1

    Здорово, целая статья про compile-time if! Ёжики, хватит жрать кактус (c++), переходите на тёмную сторону: в nim для этого есть просто ключевое слово when. Без всяких статей :)


    1. VioletGiraffe
      26.03.2018 09:55
      +2

      Лучше переходите на brainfuck, у него одни плюсы (ну, и угловые скобочки с запятыми).


    1. UberSchlag
      26.03.2018 10:09

      Спасибо за ультимативный аргумент! Теперь уж точно перейдем и все легаси перетащим! (нет)


      1. kesn
        26.03.2018 11:19
        -1

        Воу воу воу, полегче! Надеюсь, вы ещё не начали всё переписывать :) Смысл комментария был в другом: имхо невозможно усидеть на двух стульях сразу, что c++ и пытается делать — тут и совместимость сохранить надо, и фичи новые вводить. Получается монстр. Возможно, в один прекрасный момент вы захотите для вашего нового проекта попробовать что-то более продуктивное, с пакетным менеджером, с макросами, которые оперируют над AST (а не тупо подстановка), ну и с возможностью compile-time evaluation из коробки.


    1. ToshiruWang
      26.03.2018 12:07

      Тёмная — это потому что из-за тормозов приходится до поздна сидеть?


  1. neurocore
    26.03.2018 10:05

    Я правильно понял, что if constexpr гарантирует что условие развернётся на этапе компиляции? С другой стороны как контролируется «разворачиваемость условия» во время компиляции.


    1. sergegers
      26.03.2018 11:05

      Так и развернётся. Ложная ветка не будет компилироваться, примерно как #ifdef… #else… #endif.


    1. knstqq
      26.03.2018 11:31

      Если не развернётся, будет выброшена ошибка компиляции, что нельзя на этапе компиляции это вычислить.
      Если нельзя гарантировать — просто не скомпилируется. А про constexpr выражения в стандарте есть описание того, что им может быть — это и есть гарантия


  1. ktod
    26.03.2018 16:13

    Читать чужой плюсовый код станет еще более увлекательно.


  1. pavlushk0
    26.03.2018 16:14
    -2

    Вопрос от дремучего непрофессионала. А зачем вообще нужно "шаблонное метопрограммтрование", и как часто всплывают кейсы, где без него никак? Вот именно никак, т.е. не решить задачу по другому. И пример, если можно) для общего образования.


    1. Chaos_Optima
      26.03.2018 19:12

      Ну конечно можно решить, всё можно решить и тупо на асме или брэйнфаке, вообще на любом тьюринг полном языке. Вопрос в удобстве такие фичи как compile time if даёт богатый простор для оптимизации при этом упрощая синтаксис.


      1. pavlushk0
        26.03.2018 20:07
        -2

        Я не умоляю устремлений людей придумывающих такие фичи, мне правда интересен кейс) Я смотрел лекции техносферы по С++, там упоминались такие вещи. Личной мой инструментарий С++ заканчивается на, скажем так, совсем базовой инкапсуляции.

        мне правда интересен кейс


        1. Chaos_Optima
          26.03.2018 21:42

          Самое простое это патерн матчинг для типов, различные конвертации и серилизации, работа с биндингами по типу буст питона, упрощение работы с контейнерами и ещё куча кейсов но более специфичных тип внедрения зависимостей, автоматов и тд.


          1. pavlushk0
            27.03.2018 09:50

            Спасибо. Теперь есть от чего оттолкнуться для подробного гугления.


  1. OrionGames
    26.03.2018 16:14

    Очень много абстракций на таком уровне программирования, к сожалению нужно вникать, конечно все понятно, но на это уходит время и это печально)


  1. Miron11
    26.03.2018 16:15
    -1

    Не знаю, но мое мнение человека с гуманитарным образованием, что если для скрипки варианта 17 нужно для одной ноты смычок обычной формы ( обычный if ) а для другой ноты смычок с бетонным подвесом ( if constexpr ), то возникает вопрос, а что на ней собственно такое можно сыграть, чтобы идти на такие жуткие мучения. Я конечно понимаю, что если бы это сделало путешествие на Луну из невозможного простым и обыденным. Тут как говорится будем грызть метал и constexpr. Но вот так, чтобы элементарно проверять тип данных из того же Reflection только под иным синтаксисом…

    Я не того уровня специалист, чтобы указывать дорогу мастерам творящим С++. Так что извините, если мои сомнения выглядят как упражнения доморощенного Фомы не верующего, но мне кажется что что — то тут не совсем так.


    1. mayorovp
      26.03.2018 16:49

      Это не две разные ноты. Если в рамках вашей аналогии if — это смычок для ноты, то if constexpr — это ручка для рисования нот.


      1. Miron11
        27.03.2018 12:55
        -2

        Вы хотите сравнить наше умение играть на скрипке или писать музыку?

        Если я говорю, что это нота, то это нота. Пишите о том, что Вы знаете.

        Вообще, три ответа на мой в общем — то очень скромный вопрос дилетанта наводят на мылси о некоем групповом предприятии. Больно много эмоций и полное отсутствие технических деталей. Я бы даже сказал необычно много эмоций и они, эмоции, какие — то все острые. Не пойму… что так уж защищают, вроде очень уважительно написал все.


        1. terrier
          27.03.2018 13:00
          +1

          Пишите о том, что Вы знаете.

          А вот если бы вы следовали тому, что проповедуете, то вы бы в треде про constexpr не отписывались.


          1. Miron11
            28.03.2018 10:22
            -2

            Я прошу прощения, но фальшивые ноты в музыке программисту не грех заметить.

            А повышенные тона программистов по поводу constexpr выдают то, на сколько это ошибочное нововведение. Нормальное «if» с таким огнём защищать не потребовалось бы.

            То есть на лицо не только оголтелая лажа, но и не способность людей принадлежащих к роду деятельности адекватно воспринимать доброжелательную критику.


            1. mayorovp
              28.03.2018 10:38
              +1

              Проблема всех аналогий — в том, что местами они подобны котенку с дверцей.

              Замеченное вами «повышение тона программистов» вызвано исключительно тем, что вы пытаетесь использовать неуместную аналогию. Оператор if constexpr не является смычком с бетонным подвесом, только и всего. И нет ничего удивительного в том, что на эмоциональный аргумент вы получаете эмоциональные ответы.


            1. Antervis
              28.03.2018 11:47
              +1

              if constexpr и if в общем случае не являются взаимозаменяемыми. Это разные инструменты для разных задач. Не надо пожалуйста говорить что пианино — громоздкая гитара


    1. Antervis
      26.03.2018 17:00
      +1

      эта и подобные фичи появляются в стандарте потому, что они нужны. И нужны они не вам. Возможно даже, что и не авторам библиотек, которыми вы пользуетесь. Скорее авторам какого-нибудь boost::python. Подумайте об этом в следующий раз когда будете писать что-нибудь типа import pyopencl.


    1. Chaos_Optima
      26.03.2018 19:17

      Тут не только тип данных, метапрограмирование это довольно обширный класс задач, с помощью if constexp вы можете примерно в одном и том же контексте работать с совершенно другим типом при этом имея 0 оверхед (что никак не получится при runtime reflection) т.к все проверки происходят ещё на этапе компиляции.


      1. Miron11
        28.03.2018 10:35
        -2

        0 оверхед очень дорогая вещь. Это имеет смысл для изменений уровня «полетим / не полетим на Луну». Подобные внедрения в код есть, например, в ядре обработки криптографических примитивов. Обычно их выполняют пользуясь расширением ассемблера asm {}.

        Что касается templates и constraints, и далее метаданных и их проверки reflection, то Вы оцениваете оверхед через призму приземленного «а мне так кажется» пользователя, с целью оправдания сырого нововведения. Если это для «можно поспорить» точки зрения, то это конечно приемлемо. Но с точки зрения детального понимания runtime, то reflection так же неизбежен как смерть и налоги, без него просто нельзя. И в этом случае тривиальный запрос по верификации типа, это одна инструкция сравнения метаданных, которая в любом случае должны быть сделана. То есть для достаточно продвинутого компилятора это всего лишь вывод в стэк индикатора, считывающего результат целостности данных. То есть это 0 оверхэд в любом случае. Понимаете. Выигрыша нет. Зато язык стал сложнее. Вот в чем дело.

        Может в этом нововведении есть какое — то зерно, которое мне не понятно. Вот об этом и был мой вопрос. Что — то о «полете на Луну». Именно это я и спросил. Если это есть, то пожалуйста, покажите.