До C++17 у нас было несколько довольно неэлегантных способов написать static if (if, который работает во время компиляции). Например, мы можем использовать статическую диспетчеризацию или SFINAE. К счастью, ситуация изменилась к лучшему, ведь теперь мы можем воспользоваться для этого if constexpr  и концептами C++20!

Ну что ж, давайте разберемся, как мы можем использовать это в качестве замены std::enable_if кода!

  • Обновление от апреля 2021 г.: изменения, связанные с C++20 — концепты.

  • Обновление от августа 2022 г.: дополнительные примеры if constexpr (четвертый).

Введение  

If во время компиляции в форме if constexpr — это замечательная фича, которая была добавлена в C++17. Этот функционал может помочь нам значительно улучшить читаемость кода с большими нагромождениями шаблонов.

Кроме того, C++20 принес нам концепты (сoncepts)! Это еще один шаг на пути к достижению почти “органичного” кода времени компиляции.

На эту статью меня вдохновил пост на @Meeting C++ с очень похожим названием. Я нашел еще четыре примера, которые наглядно продемонстрировать в действии этот новый функционал:

  • Сравнение чисел.

  • (Новинка!) Вычисление среднего значения в контейнере.

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

  • Примеры реального кода из продакшена.

Но для начала, чтобы у нас было больше контекста, я все-таки хотел бы пройтись по некоторым базовым сведениям о enable_if.

Для чего может понадобиться if во время компиляции?    

Начнем с примера, в котором мы пытаемся преобразовать некоторый ввод в строку:

#include <string>
#include <iostream>

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

std::string str(const std::string& s) {
    return s;
}

std::string str(const char* s) {
    return s;
}

std::string str(bool b) {
    return b ? "true" : "false";
}

int main() {
    std::cout << str("hello") << '\n';
    std::cout << str(std::string{"hi!"}) << '\n';
    std::cout << str(42) << '\n';
    std::cout << str(42.2) << '\n';
    std::cout << str(true) << '\n';
}

Вы можете посмотреть этот код в Compiler Explorer.

В коде, приведенном выше, мы видим три перегрузки функций для конкретных типов и один шаблон функции для всех остальных типов, которые должны поддерживать to_string(). Похоже, что этот код работает. Давайте попробуем преобразовать все это в одну функцию.

Сработает ли здесь “заурядный” if?

Вот тестовый код:

template <typename T>
std::string str(T t) {
    if (std::is_convertible_v<T, std::string>)
        return t;
    else if (std::is_same_v<T, bool>)
        return t ? "true" : "false";
    else
        return std::to_string(t);
}

Звучит достаточно просто… но давайте попробуем скомпилировать этот код:

// код, вызывающий нашу функцию
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_convertible  возвращает true  для используемого нами типа (std::string), так что мы можем просто вернуть t  без какого-либо преобразования… так что же не так?

Вот в чем дело:

Компилятор скомпилировал все ветки и нашел ошибку в else. Он не может отбросить “неправильный” код для этого частного случая конкретизации шаблона.

Вот зачем нам нужен статический if, который “отбросит” ненужный код и скомпилирует только блок, в который ведет ветвление. Иными словами, мы по прежнему будем делать проверку синтаксиса для всего кода, но некоторые части функции не будут созданы.

std::enable_if   

Один из способов, которым можно реализовать статический if в C++11/14 — это использовать enable_if.

enable_ifenable_if_v C++14). У него довольно странный синтаксис:

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

enable_if выводит тип T, если входное условие B истинно. В противном случае это SFINAE, и конкретная перегрузка функции удаляется из набора перегрузок. Это означает, что в случае false компилятор “отбрасывает” код — это как раз то, что нам нужно.

Мы можем переписать наш пример следующим образом:

template <typename T>
enable_if_t<is_convertible_v<T, string>, string> strOld(T t) {
    return t;
}

template <typename T>
enable_if_t<!is_convertible_v<T, string>, string> strOld(T t) {
    return to_string(t);
}
// префикс std:: был опущен

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

Вот почему нам нужен if constexpr из C++17, который может помочь в таких случаях.

Почитав эту статью, вы сами сможете быстро переписать нашу функцию str (или найти решение в конце).

Чтобы лучше разобраться с новыми фичами, давайте рассмотрим несколько базовых примеров:

Пример 1 — сравнение чисел    

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

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

Версия С++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 constexpr выражение вычисляется во время компиляции, после чего код в одной из ветвей отбрасывается.

Но важно отметить, что отброшенный код все-таки должен иметь правильный синтаксис. Компилятор выполнит базовую проверку синтаксиса, но затем пропустит эту часть функции на этапе конкретизации шаблона.

Именно поэтому следующий код генерирует ошибку компилятора:

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 aaaa == bxxxx; // ошибка компилятора - синтаксис!
}

close_enough(10.04f, 20.f);

Кстати, заметили ли вы какие-нибудь другие фичи C++17, которые здесь использовались?

Вы можете посмотреть этот код в @Compiler Explorer

Добавление концептов C++20   

Но подождите… на дворе уже 2021 год, так почему бы нам не воспользоваться концептами? :)

До C++20 мы могли рассматривать шаблонные параметры как что-то вроде void* в обычной функции. Если вы хотели ограничить такой параметр, вам приходилось использовать различные методы, описанные в этой статье. Но вместе с концептами мы получили естественный способ ограничить эти параметры.

Взгляните на следующий фрагмент кода: 

template <typename T>
requires std::is_floating_point_v<T>
constexpr bool close_enough20(T a, T b) {
   return absolute(a - b) < precision_threshold<T>;
}
constexpr bool close_enough20(auto a, auto b) {
   return a == b;
}

Как видите, версия C++20 вернулась обратно к двум функциям. Но теперь код намного читабельнее, чем с enable_if. С помощью концептов мы можем легко выразить наши требования к шаблонным параметрам:

requires std::is_floating_point_v<T>

is_floating_point_v является свойством типа (из библиотеки <type_traits>), а оператор requires, как вы можете видеть, вычисляет булевы константные выражения.

Вторая функция использует новый обобщенный синтаксис функции, в котором мы можем опустить template<> и написать:

constexpr bool close_enough20(auto a, auto b) { }

Такой синтаксис мы получили с обобщенными (generic) лямбда-выражениями. Это не прямая трансляция нашего C++11/14 кода, поскольку он соответствует следующей сигнатуре:

template <typename T, typename U>
constexpr bool close_enough20(T a, U b) { }

Вдобавок, C++20 предлагает нам краткий синтаксис для концептов, который основан на auto с ограничениями (constrained auto):

constexpr bool close_enough20(std::floating_point auto a,
                              std::floating_point auto b) {
   return absolute(a - b) < precision_threshold<std::common_type_t<decltype(a), decltype(b)>>;
}
constexpr bool close_enough20(std::integral auto a, std::integral auto b) {
   return a == b;
}

В качестве альтернативы, мы также можем использовать имя концепта вместо typename без оператора requires:

template <std::is_floating_point T>
constexpr bool close_enough20(T a, T b) {
   return absolute(a - b) < precision_threshold<T)>;
}

В этом случае мы также переключились с is_floating_point_v на концепт floating_point определенный под заголовком <concepts>.

Вы можете посмотреть этот код в @Compiler Explorer

Хорошо, а как насчет других вариантов использования?

Пример 2 — вычисление среднего значения 

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

Вот, что мы хотели бы видеть:

std::vector ints { 1, 2, 3, 4, 5};
std::cout << Average(ints) << '\n';

Наша функция должна:

  • Принимать числа с плавающей запятой или целочисленные типы.

  • Возвращать double.

В C++20 для этого мы можем использовать диапазоны (ranges), но в целях этой статьи давайте рассмотрим другие способы реализовать такую функцию.

Вот возможная версия с концептами:

template <typename T> 
requires std::is_integral_v<T> || std::is_floating_point_v<T>
constexpr double Average(const std::vector<T>& vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

Для реализации нам нужно ограничить шаблонный параметр до целых чисел или чисел с плавающей запятой.

У нас нет предопределенного концепта, объединяющего типы с плавающей запятой и целочисленные, поэтому мы можем попробовать написать свой собственный:

template <typename T> 
concept numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;

И использовать его следующим образом:

template <typename T> 
requires numeric<T>
constexpr double Average2(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

Мы также можем сделать этот код достаточно лаконичным:

constexpr double Average3(std::vector<numeric auto> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

Так этот код будет выглядеть с enable_if C++14:

template <typename T> 
std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, double>
Average4(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / static_cast<double>(vec.size());
}

Вы можете посмотреть этот код в @Compiler Explorer

Пример 3 — фабрика с переменным количеством аргументов

В параграфе 18 книги “Эффективное использование C++” Скотт Майерс описал функцию 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 { }
};

Код из книги был слишком идеалистичным и практически нерабочим. Он работал только в тех случаях, когда все ваши классы имели одинаковое количество и типы входных параметров:

Скотт Майерс: История изменений и список исправлений для книги “Эффективное использование C++”:

Интерфейс 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)! Чтобы этот код работал, нам нужно что-то вроде статического if, который будет отбрасывать во время компиляции части кода, которые не соответствуют условию.

Несколько статей назад вместе с одним из моих читателей мы придумали работающее решение (подробнее вы можете прочитать в Nice C++ Factory Implementation 2).

Вот код, который будет работать:

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 в противном случае.

До С++17  

В моем предыдущем решении (до 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 (смотрите c++ reference.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;
}

Очень лаконично!

Мы можем даже добавить сюда логирование, используя свертку (fold expression):

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 канула в лету; нам даже не нужна перегрузка функции для else. Теперь мы можем заключить весь этот выразительный код в одной функции.

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

C++20   

Мы можем легко заменить enable_if концептами:

// C++20:
template <typename Concrete, typename... Ts>
requires std::is_constructible_v<Concrete, Ts...>
std::unique_ptr<Concrete> constructArgs20(Ts&&... params) {
    return std::make_unique<Concrete>(std::forward<Ts>(params)...);
}

template <typename Concrete, typename... Ts>
std::unique_ptr<Concrete> constructArgs20(...) {
    return nullptr;
}

Но я не уверен, что этот вариант лучше. Я думаю, что в этом случае, if constexpr выглядит намного лаконичнее и проще для понимания.

Вы можете посмотреть этот код в @Compiler Explorer

Пример 4 — реальные проекты     

if constexpr годится не только для экспериментальных демок — он уже нашел применение в продакшене.

Если мы посмотрим на опенсорсную реализацию STL от команды MSVC, мы можем найти несколько случаев, где if constexpr пришелся очень кстати.

Журнал изменений можно посмотреть здесь: https://github.com/microsoft/STL/wiki/Changelog

Вот некоторые из улучшений:

  • Теперь вместо статической диспетчеризации используется if constexpr в: get<I>() и get<T>() для pair (#2756)

  • Вместо статической диспетчеризации, перегрузок или специализаций используется if constexpr в таких алгоритмах, как is_permutation(), sample(), rethrow_if_nested() и default_searcher (#2219), общих механизмах <map> и <set> (#2287) и паре других мест.

  • Используется if constexpr вместо статической диспетчеризации в оптимизации в find() (#2380), basic_string(first, last) (#2480)

  • Улучшена реализация вектора, также для упрощения кода был задействован if constexpr (#1771)

Давайте посмотрим на улучшения для std::pair:

Untag dispatch get for pair by frederick-vs-ja · Pull Request #2756 · microsoft/STL

До C++17 код выглядел следующим образом:

template <class _Ret, class _Pair>
constexpr _Ret _Pair_get(_Pair& _Pr, integral_constant<size_t, 0>) noexcept {
    // получаем ссылку на элемент 0 в паре _Pr
    return _Pr.first;
}

template <class _Ret, class _Pair>
constexpr _Ret _Pair_get(_Pair& _Pr, integral_constant<size_t, 1>) noexcept {
    // получаем ссылку на элемент 1 в паре _Pr
    return _Pr.second;
}

template <size_t _Idx, class _Ty1, class _Ty2>
_NODISCARD constexpr tuple_element_t<_Idx, pair<_Ty1, _Ty2>>& 
    get(pair<_Ty1, _Ty2>& _Pr) noexcept {
    // получаем ссылку на элемент по адресу _Idx в паре _Pr
    using _Rtype = tuple_element_t<_Idx, pair<_Ty1, _Ty2>>&;
    return _Pair_get<_Rtype>(_Pr, integral_constant<size_t, _Idx>{});
}

И после изменения:

template <size_t _Idx, class _Ty1, class _Ty2>
_NODISCARD constexpr tuple_element_t<_Idx, pair<_Ty1, _Ty2>>& get(pair<_Ty1, _Ty2>& _Pr) noexcept {
    // получить ссылку на элемент по адресу _Idx в паре _Pr
    if constexpr (_Idx == 0) {
        return _Pr.first;
    } else {
        return _Pr.second;
    }
}

Теперь это одна функция, и ее намного легче читать! Больше нет никакой необходимости использовать статическую диспетчеризацию и хелпер integer_constant.

В другой библиотеке, на этот раз связанной с SIMD-типами и вычислениями (популярная реализация от Agner Fog), вы можете найти множество примеров использования if constexpr:

https://github.com/vectorclass/version2/blob/master/instrset.h

Одним из ярких примеров является функция маски:

// zero_mask: возвращает компактную битовую маску для обнуления с использованием маски AVX512.
// Параметр a является ссылкой на массив constexpr int индексов перестановок
template <int N>
constexpr auto zero_mask(int const (&a)[N]) {
    uint64_t mask = 0;
    int i = 0;

    for (i = 0; i < N; i++) {
        if (a[i] >= 0) mask |= uint64_t(1) << i;
    }
    if constexpr      (N <= 8 ) return uint8_t(mask);
    else if constexpr (N <= 16) return uint16_t(mask);
    else if constexpr (N <= 32) return uint32_t(mask);
    else return mask;
}

Без if constexpr код был бы намного длиннее и потенциально дублировался бы.

Заключение

if во время компиляции — замечательная фича, значительно упрощающая шаблонный код. Более того, она намного выразительнее и элегантнее, чем предыдущие решения: статическая диспетчеризация и enable_if (SFINAE). Теперь мы можем легко выразить свои намерения наподобии с рантайм-кодом.

Мы также переработали код примеров с нововведениями C++20! Как видите, благодаря концептам код стал еще более читабельным, ведь теперь мы можем выразить требования к своим типам “естественным” образом. Мы также получили несколько сокращений синтаксиса и несколько способов сообщить о наших ограничениях.

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

Возвращаясь назад…

И возвращаясь к нашему примеру со str:

Можете ли вы теперь переписать str (из начала этой статьи), используя if constexpr? :) Попробуйте, а затем взгляните на мое простое решение в @CE.

Еще больше

Вы можете найти больше примеров и вариантов использования if constexpr в моей книге о C++17: C++17 в деталях на @Leanpub или печатную версию на @Amazon.


Статья подготовлена в преддверии старта курса "C++ Developer. Professional". Всех желающих приглашаем посмотреть запись открытого урока «Умные указатели», на котором разобрали, что такое умные указатели и зачем они нужны; а также провели обзор умных указателей, входящих в stl: unique_ptr, shared_ptr, weak_ptr. Посмотреть можно по ссылке.

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


  1. vadimr
    16.09.2022 17:17
    +2

    Динамическая типизация своими руками при помощи жевачки, синей изоленты и какой-то матери.


    1. allcreater
      16.09.2022 23:12
      +2

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

      Код некоторых страшных примеров из статьи во многих может упроститься до пары инструкций времени исполнения, либо и вовсе отработать во время компиляции(в зависимости от контекста вызова).


      1. vadimr
        17.09.2022 00:12
        +2

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


        1. allcreater
          17.09.2022 13:31
          +2

          Не совсем понял, что Вы имеете в виду.


          Не расскажете чуть конкретнее, что именно не нравится и как, по-Вашему, эта проблема должна решаться без "сумерек рассудка"?


          1. vadimr
            17.09.2022 13:40

            Мне не нравится конкретно вот это вот:

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

            Но важно отметить, что отброшенный код все-таки должен иметь правильный синтаксис. Компилятор выполнит базовую проверку синтаксиса, но затем пропустит эту часть функции на этапе конкретизации шаблона.

            Именно поэтому следующий код генерирует ошибку компилятора

            Без "сумерек рассудка" проблема синтаксического анализа и тем более проверки типизации в никогда не выполняющемся и тем более не компилируемом коде не должна вообще возникать.

            Или, может быть, вы объясните, какую цель преследует проверка правил типизации в коде, не предназначенном для исполнения? Конечно, содержательно объясните, а не в ключе "мы сделали лопату с черенком из суковатого дерева, и теперь так само естественно получается, что сучья цепляются за разные предметы".


            1. sergio_nsk
              18.09.2022 05:55
              +3

              Код не предназначен для выполнения - это совсем не так. Он предназначен для выполнения. В каждом конкретном вызове во время компиляции какая-то ветвь не нужна, это не значит, что она не нужна в других случаях.


              1. vadimr
                18.09.2022 09:39

                Ну так в каждом конкретном случае, когда она используется, она корректна и без того.


              1. GigaCore
                18.09.2022 22:37

                Так почему она проверяется даже когда никогда не нужна ?


                1. sergio_nsk
                  19.09.2022 03:02
                  +1

                  Что проверяется? Синтаксически код должен быть верным.


            1. sergegers
              18.09.2022 15:36
              -1

              Без "сумерек рассудка" проблема синтаксического анализа и тем более проверки типизации в никогда не выполняющемся и тем более не компилируемом коде не должна вообще возникать.

              В раних версиях MSVC такое было, за что его все хейтили.


            1. iCpu
              19.09.2022 06:45
              +2

              Без "сумерек рассудка" проблема синтаксического анализа и тем более проверки типизации в никогда не выполняющемся и тем более не компилируемом коде не должна вообще возникать.

              Ну, во-первых, "не компилируемый код" - немножко не верно. Не компилируются комментарии. А это - отбрасываемый на этапе компиляции код, а, значит, он должен быть синтаксически корректным.

              Во-вторых, никто не отменял несовершенство компиляторов - и самого стандарта, пестрящего разными UB и UB.

              И, позвольте, вы бы точно так же кричали, "Почему там выполняются базовые проверки синтаксиса?!", если бы вместо не существующих имён переменных там была бы закрывающая фигурная скобочка или объявление класса?


              1. vadimr
                19.09.2022 08:39

                Директивы условной компиляции в том же самом C++ устроены именно так.

                (Хотя вообще-то уже лет 50 существуют примеры, как и скобки посчитать, и макросы нормально обработать).


  1. sci_nov
    16.09.2022 18:32

    Подскажите пожалуйста где можно почитать про zero_mask подробнее?


  1. Racheengel
    16.09.2022 19:49
    +4

    Почему бы не сделать всю эту красоту непосредственно частью языка, вместо нагромождения куч темплейтов?

    Да, стало лучше, чем было, но всё равно какой-то дух палеозоя витает сверху...


    1. NN1
      16.09.2022 23:18
      +2

      Уточните, что именно предлагается сделать частью языка.

      Как раз вынося больше в библиотеку позволяет языку быть более простым.


      1. Racheengel
        17.09.2022 14:12

        Например, проверку типов аргументов. Что то наподобие

        template<T = int|float|double>...

        как то короче и понятнее..


        1. eao197
          17.09.2022 14:43
          +3

          Почти так и сделали (цынк):

          #include <type_traits>
          
          template<typename T>
          requires (std::is_same_v<T, int> 
              || std::is_same_v<T, float>
              || std::is_same_v<T, double>)
          auto my_abs(T v) {
              return v < 0 ? -v : v;
          }
          
          int main() {
              my_abs(1);
              my_abs(0.1f);
              my_abs(-0.2);
              my_abs(42ul);
          }
          

          Или принципиально важно иметь именно int|float|double вместо std::is_same_v<T,int>?


          1. allcreater
            17.09.2022 15:20
            +3

            Можно даже ввести дополнительный концепт, и сделать код еще ближе к тому, что просят :)

            template <typename T, typename ... Variants>
            concept one_of_types = (std::is_same_v<T, Variants> || ...);
            
            auto my_abs(one_of_types<int, float, double> auto v) {
                return v < 0 ? -v : v;
            }
            


            1. Racheengel
              17.09.2022 16:08
              +1

              А вот это обязательно прописывать в коде?

              template <typename T, typename ... Variants>
              concept one_of_types = (std::is_same_v<T, Variants> || ...);

              Компилятор не проще научить уметь в типы? Обязательно бойлерплейт тащить?


              1. allcreater
                17.09.2022 16:36

                В современных реалиях - обязательно.

                На месте typename могло бы быть указано значение (Non-Type Template Argument), или имя другого ограничения(концепта).
                На месте concept - using, inline переменная, а то и вовсе объявление функции или типа.

                C++ - очень гибкий язык, и за это приходится расплачиваться более сложным и не очень лаконичным синтаксисом.

                С другой стороны, шаблонная магия как правило живет где-то в библиотеках, а прикладной код может быть вполне лаконичен.


                1. NN1
                  17.09.2022 18:26

                  using T = int;

                  template<T> void f();

                  Здесь T это не тип, а число: f<1>();

                  Как компилятор должен знать, что вам нужен был тип ?


                  1. allcreater
                    17.09.2022 23:16

                    ой, это, кажется, не мне, а @Racheengel ? :)


                    1. NN1
                      18.09.2022 00:27

                      Да. Слегка промахнулся :)


          1. NN1
            17.09.2022 18:30

            Концепт будет правильней так как позволяет не вычислять типы, в случае истины, дальнейшие сравнения, т.е. работает как логическое «ИЛИ».

            Либо использовать std::disjunction.

            https://akrzemi1.wordpress.com/2020/03/26/requires-clause/


  1. segment
    16.09.2022 22:50
    +3

    Громоздко.


  1. markhor
    17.09.2022 09:51
    +8

    Надо отдать должное C++ - каждые несколько лет комитет стабильно умудряется добавить в язык новую кучу сложных абстракций которые "упрощают" код, на самом деле просто заметая детали реализации под ковёр, оставляя старые. Во времена ++11/++14 уже большинство программистов не понимало как работают шаблонные приблуды вместе с &&-фетишем. ++17/++20 добили понимание внутренностей языка и окончательно превратили его в причудливого монстра. А ещё накопилось огромное легаси, так что нужно знать всё - и как писали в 2003, и в 2011, и в 2017, и как фигачит концепты с constexpr if-ами без разбору новое поколение. Зато можно постоянно переписывать код, интересно же!
    Иногда меньше - значит больше. И - нет, у Go куча своих проблем, там перегиб в другую сторону.


    1. NN1
      17.09.2022 14:36

      Вы про модули забыли :)


  1. x2v0
    17.09.2022 23:41
    -1

    сылка на "Practical Modern C++ Teaser" - дохлая!!!!
    Вообеще-то C++ ждут большие перемены на пути упрощения языка
    Все что здесь описано делается гораздо проще с помощью Reflection
    Надеюсь, что уже в следующем году мы увидим выражения "in, out, ref, inout"
    https://github.com/hsutter/708/blob/main/708 talk slides.pdf


    1. vadimr
      18.09.2022 12:55

      Идея сама по себе хорошая, только очень общая, как всякая абстракция. Все приведённые в презентации примеры путей подразумевают последовательное исполнение программы. Как будут маркироваться и диагностироваться переменные, используемые в нескольких процессах одновременно? И не возникнет ли проблем, если честно попытаться построить полную иерархию использования данных от прикладной программы к ядру операционной системы, где в конечном итоге зависимости придут к самопроизвольно изменяющимся аппаратным регистрам? Это всё хорошо звучит на описании класса “Лошадка”, а с таким понятием, как файл, уже могут быть проблемы.


  1. svr_91
    19.09.2022 10:14

    Насколько я понимаю, минус использования if constexpr в 90% представленных здесь примеров - это то, что функции, написанные таким образом, нельзя впоследствии расширить. Тоесть, написать перегрузку для to_string для своего типа, не затрагивая изначальную функцию, больше нельзя