До 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_if
(и enable_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
. Если нет, то вернем nullptr
(и make_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)
Racheengel
16.09.2022 19:49+4Почему бы не сделать всю эту красоту непосредственно частью языка, вместо нагромождения куч темплейтов?
Да, стало лучше, чем было, но всё равно какой-то дух палеозоя витает сверху...
NN1
16.09.2022 23:18+2Уточните, что именно предлагается сделать частью языка.
Как раз вынося больше в библиотеку позволяет языку быть более простым.
Racheengel
17.09.2022 14:12Например, проверку типов аргументов. Что то наподобие
template<T = int|float|double>...
как то короче и понятнее..
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>
?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; }
Racheengel
17.09.2022 16:08+1А вот это обязательно прописывать в коде?
template <typename T, typename ... Variants>
concept one_of_types = (std::is_same_v<T, Variants> || ...);Компилятор не проще научить уметь в типы? Обязательно бойлерплейт тащить?
allcreater
17.09.2022 16:36В современных реалиях - обязательно.
На месте typename могло бы быть указано значение (Non-Type Template Argument), или имя другого ограничения(концепта).
На месте concept - using, inline переменная, а то и вовсе объявление функции или типа.C++ - очень гибкий язык, и за это приходится расплачиваться более сложным и не очень лаконичным синтаксисом.
С другой стороны, шаблонная магия как правило живет где-то в библиотеках, а прикладной код может быть вполне лаконичен.
NN1
17.09.2022 18:26using T = int;
template<T> void f();
Здесь T это не тип, а число: f<1>();
Как компилятор должен знать, что вам нужен был тип ?
NN1
17.09.2022 18:30Концепт будет правильней так как позволяет не вычислять типы, в случае истины, дальнейшие сравнения, т.е. работает как логическое «ИЛИ».
Либо использовать std::disjunction.
markhor
17.09.2022 09:51+8Надо отдать должное C++ - каждые несколько лет комитет стабильно умудряется добавить в язык новую кучу сложных абстракций которые "упрощают" код, на самом деле просто заметая детали реализации под ковёр, оставляя старые. Во времена ++11/++14 уже большинство программистов не понимало как работают шаблонные приблуды вместе с &&-фетишем. ++17/++20 добили понимание внутренностей языка и окончательно превратили его в причудливого монстра. А ещё накопилось огромное легаси, так что нужно знать всё - и как писали в 2003, и в 2011, и в 2017, и как фигачит концепты с constexpr if-ами без разбору новое поколение. Зато можно постоянно переписывать код, интересно же!
Иногда меньше - значит больше. И - нет, у Go куча своих проблем, там перегиб в другую сторону.
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.pdfvadimr
18.09.2022 12:55Идея сама по себе хорошая, только очень общая, как всякая абстракция. Все приведённые в презентации примеры путей подразумевают последовательное исполнение программы. Как будут маркироваться и диагностироваться переменные, используемые в нескольких процессах одновременно? И не возникнет ли проблем, если честно попытаться построить полную иерархию использования данных от прикладной программы к ядру операционной системы, где в конечном итоге зависимости придут к самопроизвольно изменяющимся аппаратным регистрам? Это всё хорошо звучит на описании класса “Лошадка”, а с таким понятием, как файл, уже могут быть проблемы.
svr_91
19.09.2022 10:14Насколько я понимаю, минус использования if constexpr в 90% представленных здесь примеров - это то, что функции, написанные таким образом, нельзя впоследствии расширить. Тоесть, написать перегрузку для to_string для своего типа, не затрагивая изначальную функцию, больше нельзя
vadimr
Динамическая типизация своими руками при помощи жевачки, синей изоленты и какой-то матери.
allcreater
В том-то и дело, что в C++ вместо динамической типизации при любой возможности стараются использовать статическую. И на этом поле язык действительно очень хорош.
Код некоторых страшных примеров из статьи во многих может упроститься до пары инструкций времени исполнения, либо и вовсе отработать во время компиляции(в зависимости от контекста вызова).
vadimr
Я бы немало прифигел, изучая программирование, если бы узнал, что в прекрасном будущем 21 веке в популярном языке будут предусматривать специальные конструкции, чтобы соблюсти строгую типизацию в коде, который не только никогда не выполняется, но и не должен компилироваться в несущие содержание инструкции. С практической точки зрения, это (типизация в отбрасываемом коде) выглядит как сумерки рассудка. Механизм затмил собой своё назначение.
allcreater
Не совсем понял, что Вы имеете в виду.
Не расскажете чуть конкретнее, что именно не нравится и как, по-Вашему, эта проблема должна решаться без "сумерек рассудка"?
vadimr
Мне не нравится конкретно вот это вот:
Без "сумерек рассудка" проблема синтаксического анализа и тем более проверки типизации в никогда не выполняющемся и тем более не компилируемом коде не должна вообще возникать.
Или, может быть, вы объясните, какую цель преследует проверка правил типизации в коде, не предназначенном для исполнения? Конечно, содержательно объясните, а не в ключе "мы сделали лопату с черенком из суковатого дерева, и теперь так само естественно получается, что сучья цепляются за разные предметы".
sergio_nsk
Код не предназначен для выполнения - это совсем не так. Он предназначен для выполнения. В каждом конкретном вызове во время компиляции какая-то ветвь не нужна, это не значит, что она не нужна в других случаях.
vadimr
Ну так в каждом конкретном случае, когда она используется, она корректна и без того.
GigaCore
Так почему она проверяется даже когда никогда не нужна ?
sergio_nsk
Что проверяется? Синтаксически код должен быть верным.
sergegers
В раних версиях MSVC такое было, за что его все хейтили.
iCpu
Ну, во-первых, "не компилируемый код" - немножко не верно. Не компилируются комментарии. А это - отбрасываемый на этапе компиляции код, а, значит, он должен быть синтаксически корректным.
Во-вторых, никто не отменял несовершенство компиляторов - и самого стандарта, пестрящего разными UB и UB.
И, позвольте, вы бы точно так же кричали, "Почему там выполняются базовые проверки синтаксиса?!", если бы вместо не существующих имён переменных там была бы закрывающая фигурная скобочка или объявление класса?
vadimr
Директивы условной компиляции в том же самом C++ устроены именно так.
(Хотя вообще-то уже лет 50 существуют примеры, как и скобки посчитать, и макросы нормально обработать).