if constexpr. Мотивация.



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



На самом деле это тривиально только для интегральных типов. a == b на этом все. Но вот для чисел с плавающей точкой все не так тривиально. Чисто исходя из того, что из себя представляют такие числа и как с ними оперировать, нельзя их сравнивать оператором ==. Корректное сравнение представляет собой сравнение модуля разности двух чисел с некоторой очень маленькой величиной epsilon. Если расстояние между двумя числами находится в пределах допускаемой нами погрешности, тогда эти числа равны.



Но даже в таком случае проблему можно решить однообразно. Просто определить одну перегрузку для даблов и все. Тогда все целые числа будут приводиться к вещественным и сравниваться однообразно.



Только вот это не очень эффективно с точки зрения производительности. Для целых чисел мы могли бы использовать один оператор, а тут будем использовать 3 действия - вычитание, взятие модуля и сравнение. Поэтому хотелось бы эти ветки разделить, чтобы они не пересекались. А сделать это можно с помощью шаблонов и sfinae. Для С++14 код будет выглядеть примерно так:



templete <class T>

constexpr T absolute(T arg) {

return arg < 0 ? -arg : arg;

}



template <class T>

constexpr std::enable_if_t<std::is_floating_point<T>::value, bool>

is_equal(T a, T b) {

return absolute(a - b) < static_cast<T>(0.000001);

}



template <class T>

constexpr std::enable_if_t<!std::is_floating_point<T>::value, bool>

is_equal(T a, T b) {

return a == b;

}




Как видите, здесь используется std::enable_if. Код проверяет тип аргументов функции и направляет нужные типы в нужную перегрузку. Но как будто бы это очень сложно. И много кода просто повторяется. Хочется две эти перегрузки как-то объединить. Тем более тут вообще явно проглядываются две ветки условия, при ложном и правдивом исходе. Разве просто if тут не подойдет? Тут может и подойдет. Но не всегда обычный if в принципе может являться опцией. Давайте рассмотрим такой пример.



Мы хотим универсальную функцию to_str, которая возвращает строку, сделанную из переданного параметра. Используя стандартный if мы пишем:



template <class T>

std::string to_str(T t) {

if (std::is_same_v<T, std::string>)

return t;

else

return std::to_string(t);

}



auto str = to_str("10"s);




И при попытке это дело скомпилировать мы получим неожиданную ошибку: компилятор отказывается находить перегрузку функции std::to_string для std::basic_string. И правильно делает, ведь такой перегрузки нет. Но как же так? is_same дает true и мы можем просто вернуть строку без преобразований. Но не так все просто.



Компилятору нужно проверить весь код функции на корректность. Чтобы все символы резолвились, синтаксис соблюдался и вызывались корректные перегрузки. Во всех ветках кода. И в этом примере при попадании строки в функцию для нее не найдется перегрузка для std::to_string,



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



И с этим очень хорошо справляется if constexpr. То есть статический if. Условие времени компиляции. Как хотите. В следующем посте подробнее разберем эту конструкцию и с чем ее едят.



Choose the right path and don't overanalyze others. Stay cool.



#cpp14 #template #compiler