Грокаем C++
7.53K subscribers
25 photos
3 files
336 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Fold expressions. Мотивация.

Стандарт C++11 привнес в нашу жизнь замечательную фичу - variadic templates, которая является очень мощным инструментом в метапрограммировании. Она используется, когда нам необходимо написать функцию, которая принимает неопределенное количество аргументов. Ранее такой возможности в С++ не было (имею ввиду типобезопасные шаблонные функции) и приходилось отдельно специфицировать функцию в начале с одним аргументом, потом с двумя, потом с тремя и так далее, пока не надоест, не настанет обед или больше не нужно будет. Не очень удобненько.

Однако и для вариадиков нам нужно писать некоторый "дополнительный" код. Например, когда мы хотим написать функцию sum, которая складывает все аргументы, которые ей передали, рекурсивно. Мы должны определить базу для рекурсии. Выглядит это так:

auto SumCpp11() {
return 0;
}

template<typename T1, typename... T>
auto SumCpp11(T1 s, T... ts) {
return s + SumCpp11(ts...);
}

Если бы у нас не было первого определения, то рекурсия дошла бы до нуля аргументов и не смогла бы инстанцировать функцию без аргументов и компиляция бы провалилась.

Но важно еще кое-что заметить. Что мы так или иначе предполагаем, что все наши аргументы могут успешно быть сложены с интом. Это довольно сильное ограничение, потому что может я хочу и матрицы складывать тоже этой функцией. А тут такого сделать не получится.

Но решение этих проблем есть!

Называется fold expression. Появилось это спасение в С++17 и позволяет писать намного более простой код. Посмотрим, как будет выглядеть прошлый пример при его использовании.

template<typename ...Args> 
auto SumCpp17(Args ...args) {
return (args + ...);
}

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

Однако есть все-таки одно ограничение. Функцию Sum не получится инстанцировать без аргументов. Это свойство оператора сложения. И об этом в том числе мы поговорим завтра, когда будем подробнее разбирать внутрянку fold expression.

Make things simplier. Stay cool.

#cpp11 #cpp17 #template
Fold expression. Подробности.

В сущности, fold expression - сворачивание всего пака шаблонных параметров с помощью комбинации синтаксиса variadic templates и бинарных операторов. Есть всего 4 формата, в которых можно использовать эту фичу.

1️⃣ ( pack op ...) - унарный правый фолд
2️⃣ ( ...  op pack) - унарный левый фолд
3️⃣ (pack op ... op init ) - бинарный правый фолд
4️⃣ (init op ... op pack) - бинарный левый фолд

где pack - выражение, содержащее нераспакованный набор шаблонных параметров. op - бинарный оператор. В последних двух случаях он должен быть одинаковым справа и слева от точек. В число бинарных операторов входит почти все, что вы могли бы себе представить: +  -    /   %  ^   &   |   =   <   >   <<   >>   +=  -=   =   /= %=   ^=   &=   |=   <<=   >>=   ==   !=   <=   >=   &&   ||   ,   .   ->. init - выражение, которое никак не относится к шаблонным параметрам и является базой вычислений. Это как в std::accumulate вы можете выставить начальное значение для аггрегации. Вот это тоже самое.

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

Очень важное уточнение по поводу левых и правых фолдов.

👉🏿 Унарный  правый фолд (E op ...) раскрывается в  (E1 op (... op (En-1 op En)))
👉🏿 Унарный левый фолд (... op E) раскрывается в  (((E1 op E2) op ...) op En)
👉🏿 Бинарный правый фолд (E op ... op init) раскрывается в  (E1 op (... op (En−1 op (EN op init))))
👉🏿 Бинарный левый фолд(init op ... op E) раскрывается в  ((((init op E1) op E2) op ...) op En)

И тут очень сильно решает коммутативность операции. То есть неависимость от порядка аргументов. Если оператор обладает этим свойством, как например сложение или умножение, то можете не париться о порядке. Пишите, как удобно. А вот если от порядка операндов зависит итоговый результат операции(тот же бинарный сдвиг), то подумайте, какой именно фолд подойдет для решения вашей задачи.

А помните, я вчера упоминал функцию Sum, которую нельзя расшаблонивать с нулевым количеством аргументов(там в комментах @PyXiion придумал как, но я сейчас имею ввиду нативный формат без оберток)? Вот сейчас и коснемся этого вопроса.

Функция без аргументов - всегда был особым случаем при использовании вариадик шаблонов. И для fold expression она также является таковым. Тут следующие правила:

💥 У оператора Логическое И (&&) значение для пустого набора параметров - true.
💥 У оператора Логическое ИЛИ (||) значение для пустого набора параметров - false.
💥 У оператора "запятая" (,) значение для пустого набора параметров - void().
☠️ Для всех остальных операторов конкретизация шаблона с пустым набором параметров запрещена.

Почему так? А непонятно, какой результат у сложения ничего с ничем. Или непонятно, что будет если у ничего сдвинуть несуществующие биты вправо на никакое количество позиций. И так далее. И соответственно, тут надо либо использовать инициализатор, либо обвязки писать. Но вот для трех операторов нашелся логичный результат, поэтому они вот такие особенные.

В следующий раз в подробностях разберем, как нормально принтоваться с помощью fold expression. Это довольно популярное и нужное применение. Может и не в продакшен коде. Но при экспериментах или при отладке поможет сильно сократить время и ошибки.

Постарался кое-где использовать ваши синонимы, отпишите, как звучит.

Explore internals of things. Stay cool.

#cpp17 #template
Принтуем с fold expression

Для начала разберем, как бы все выглядело до С++17.

Задача - вывести на экран все аргументы функции подряд. Не так уж и сложно. Код будет выглядеть примерно вот так:

void print() {
}

template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print(ts...);
}

print(1.7, -2, "qwe");

// Output: 1.7-2qwe


Это будет работать, но не очень прикольно выводить аргументы прям подряд символами. Нужно какое-то форматирование. Например, между аргументами выводить пробел, а в конце перенести строку. Тут уже все несколько усложняется...

void print_impl() {
}

template<typename T1, typename... T>
void print_impl(T1 s, T... ts) {
std::cout << ' ' << s;
print_impl(ts...);
}

template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print_impl(ts...);
std::cout << std::endl;
}

print(1.7, -2, "qwe");
print("You", ", our subscribers,", "are", "the", "best!!");

// Output:
// 1.7 -2 qwe
// You , our subscribers, are the best!!


Нам мало того, что пришлось использовать базу рекурсии, так еще и прокси-функцию, которая допиливает форматирование. Слишко МНОГА БУКАВ. Ща исправим.

Вот так будет выглядеть базовый принт без форматирования на fold expression:

template<typename... Args>
void print(Args&&... args) {
(std::cout << ... << args);
}

print(1.7, -2, "qwe");

// Output: 1.7-2qwe


Уже лучше. Точнее не так. Проще не бывает уже)
Как видите, здесь я использую бинарный левый фолд. В качестве инициализатора выступает стандартный поток вывода и он слева не только потому, что так обычно принято, а потому что оператор << также применяется например для бинарного сдвига. И чтобы мы всегда именно в поток писали, нужно, чтобы слева всегда был нужный поток. Тогда будет вызываться соответствующая перегрузка для ostream'ов и каждый раз будет возвращаться ссылка на этот поток. Таким образом мы и будем продолжать писать именно в него.

Но как тут быть с форматингом? args тут просто раскроются в последовательность "arg1 << arg2 << arg3" и тд
И непонятно, как в таких условиях добавить вывод пробела, не придумывая нагромождения в виде проксей и прочего. Для решения этой проблемы надо воспользоваться двумя хаками:

1️⃣ Не обязательно использовать сырой пакет параметров. Можно использовать функцию, принимающую этот пак.
2️⃣ Применяя оператор запятую, мы можем в операндах выполнять любое выражение, даже возвращающее void.

Получается такая штука:

template<typename ...Args>
void print(Args&&... args) {
auto print_with_space = [](const auto& v) { std::cout << v << ' '; };
(print_with_space(args), ... , (std::cout << std::endl));
}

print(1.7, -2, "qwe");
print("You, "are", "the", "best!!");

// Output:
// 1.7 -2 qwe
// You are the best!!


Здесь мы за счет лямбды и запятой выполняем каждый раз отдельную операцию вывода в поток с пробелом. А затем вместо init выражения подставляем вывод конца строки.

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

Hack this life. Stay cool.

#cpp17 #template
Что выведется на экран?

Попробуем новую рубрику на канале - #quiz. Мы задаем вопрос - а вы выбираете один из предоставленных ответов. Все обсуждения в комментах. А вечером выходит пост с подробными объяснениями. Погнали!

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

int number = -12;
int result = number >> 31;
std::cout << result << std::endl;


Для определенности предположим, что number = -12.

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

Считайте, что -12 представляется в памяти, как 1111 1111 1111 1111 1111 1111 1111 0100. Почти наверняка так и будет.
(Опрос следующим постом выйдет)
Что выведется в консоль в этом случае?
Anonymous Poll
39%
1
9%
0
21%
-1
11%
2^32 - 1
15%
Мне-то откуда знать???
5%
Hello, World!!!
Ответ на вопрос выше не так уж и прост на самом деле. Начнем с того, что когда вы запустите этот код на своей машине, то получите ответ: -1.

Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".

Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)

Заинтригованы?

Тогда мы идем к вам!

Что было до С++20.

Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.

Почему я тогда утверждаю, что вы на своих машинах получите -1?

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

Что это за акула такая.

Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.

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

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

Арифметический сдвиг - хоть теперь и стандартное поведение, но

Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение

Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт

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

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

Stay surprised. Stay cool.

#cpp20 #compiler
У нас большой праздник!!!

Вчера на канале случился юбилей - к нам подписался наш тысячный подписчик и соратник!!! Ура!!! Ура!!! Ураааааа!!!

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

Поэтому хочу сказать Вам всем: Спасибо огромное!!!!

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

Спасибо всем ребятам, кто перешел с других каналов. Спасибо всем ребятам, кто пришел с LinkedIn. Спасибо всем, кто каким-то образом нашел наш канал и стал частью сообщества. Вы все крутые люди и профессионалы. Вы настоящие герои этого дня!!!

Мысли по созданию канала были уже давненько, но решение о его создании было принято нами в знаменитом самом маленьком стейкхаусе в мире «Steak Me Truck» в Нижнем Новгороде. Это был конец сентября того года.

Сегодня, чтобы отметить наш юбилей, мы пришли в то же самое место и съели по крутому мягкому стейку! Теперь будем считать это тотемным местом нашего канала. Так что тем, кто живет в Нижнем и туристам из других городов, рекомендуем посетить это, не побоюсь этой фразы, на весь мир известное место. А какой там томатный сок с халапеньо…. Ммммм…. Только ради него и стоит идти, не говоря уже про сами стейки.

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

Enjoy your achievements. Stay cool.
Способы узнать знак целого числа

Иногда появляется необходимость узнать, является ли данное число отрицательным или нет. За примером далеко ходить не будем. В одной из наших задачек про переворачивание десятичных цифр числа один из подходов к решению может быть таким: узнать, является ли число отрицательным, если да, то домножить его на -1 и оперировать им как положительным числом. После всех действий и проверок домножить число обратно на -1.

И вот мне стало интересно. Сколько способов есть, чтобы узнать является ли число отрицательным? Давайте же узнаем! Я вот что надумал:

💥 Как в прошлом посте сдвинуть число вправо на 31/63 бита и привести все к инту. Если получился 0 - число положительное. Если 1 - отрицательное.

💥 bool is_signed = number < 0. Один из самых очевидных и прямолинейных подходов. Просто проверяем число оператором меньше и все на этом. Скучно, попсово, но зато понятно и эффективно.

💥 Использовать битовую маску. bool is_signed = number & 0x80000000. Здесь мы оставляем только знаковый бит на его месте. Затем приводим число к булевому значению. Положительное число превратится в нолик, а значит в true, а отрицательное - в false. Размер маски естественно меняется в зависимости от типа знакового числа.

💥 std::signbit(number). Эта шаблонная функция вернет вам true, если number - отрицательное, и false в обратном случае. На мой взгляд, это больше по плюсовому и функция имеет человеческое название, поэтому читаться такой код будет намного проще, чем в предыдущих случаях.

На этом я застопорился. Никак больше не могу придумать других способов, которые бы не включали в себя предыдущие. Тут скорее мне интересно, какие вы варианты придумаете. Обязательно отпишитесь к комментах!

Generate a dozens of different solutions. Stay cool.

#fun #cppcore
std::signbit

В прошлом посте мы уже упоминали std::signbit. Сегодня мы посмотрим на эту сущность по-подробнее.

По сути, это самый говорящий и плюсовый чтоли способ узнать знаковый бит числа, который появился в нашем арсенале с приходом С++11. Причем не только целого, но и числа с плавающей точкой. Хотя на самом деле даже наоборот.

bool signbit( float num );  
bool signbit( double num );
bool signbit( long double num );


вот такие перегрузки мы имеем для floating-point чисел. А вот такую:

template< class Integer >  
bool signbit( Integer num );


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

В чем особенность целочисленной перегрузки. В том, что число, которое туда попадает трактуется, как double. Поэтому выражение std::signbit(num) эквивалентно std::signbit(static_cast<double>(num)).

Также эта функция детектирует наличие знакового бита у нулей, бесконечностей и NaN'ов. Да, да. У нуля есть знак. Так что 0.0 и -0.0 - не одно и то же. И если вы внимательные, то заметили даже у NaN есть знак. И std::signbit - один из двух возможных кроссфплатформенных способов узнать знак NaN. Этот факт еще больше мотивирует использовать эту функцию(в ситуациях, где это свойство решает).

Начиная с 23 стандарта функция становится constexpr, что не может не радовать любителей compile-time вычислений.

Для языка С тоже кстати есть похожая сущность. Только там это макрос

#define signbit( arg ) /* implementation defined */


И для него гарантируется такое поведение: для положительных чисел возвращаем ноль, а для отрицательных - ненулевое целое число.

Мне кажется, что в повседневной разработке(там где не нужно выжимать все возможные такты и кода) плюсовое решение будет более предпочтительным, по сравнению с аналогами. Говорящее название и поддержка стандрата - наши главные друзья.

Look for signs in life. Stay cool.

#cpp23 #cpp11 #goodoldc
Бинарные логические операторы. Short circuit.

Хотел прояснить один момент. В этом посте я сказал, что при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы, в том числе в виде fold expression. Я имел ввиду, что вам придется конкретизировать все метаклассы(ну не прям вам своими ручками, но тем не менее) для того, чтобы начать вычислять выражение. Это правда, без сомнений.

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

Опять же, без сомнений, && и || что в обычном виде, что в форме fold expression выполняют вычисления по короткой схеме. Они просто обязаны инстанциировать все шаблонные параметры/параметры из пака шаблонных аргументов, чтобы начать выполняться и начать проявлять свои short circuit свойства. То есть

template <class T>
struct type_without_value
{
};

template <class T1, class T2>
constexpr auto numbers = (std::is_integral_v<T1> && type_without_value<T2>::value);

constexpr auto result = numbers<float, int>;


вот этот пример крашнется на компиляции, потому что компилятору нужно инстанцировать type_without_value<T2> и достать из него value, но он не сможет этого сделать, потому что type_without_value не содержит члена value. Но в случае удачной инстанциации этот пример:

template <class T1, class T2>
constexpr auto numbers = (std::is_integral_v<T1> && std::is_integral_v<T2>);

constexpr auto result = numbers<float, int>;


успешно соберется и result будет равен false, как и ожидается. В этом случае значение правого операнда учитываться не будет.

Это можно продемонстрировать на следующем примере. Вот такой код успешно соберется и выполнится:

constexpr bool is_even(int value) noexcept {
return (value % 2 == 0) ? true : throw std::logic_error("Don't throw me around, you bastard!");
}

template <class T>
constexpr auto numbers = (std::is_integral_v<T> && is_even(1));

constexpr auto result = numbers<float>;
// constexpr bool fail = is_even(1);


Как видите, при выполнении функции is_even, если в нее попадет нечетное число, то бросится исключение. Исключение, брошенное в compile-time, прерывает компиляцию.

Но в крайнем примере все пройдет хорошо, потому что is_even ни разу не выполнится, как раз из-за короткосхемности оператора &&!

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

Вот такие дела. Надеюсь, я не зря волновался и для кого-то получше прояснил ситуацию.

Explain things clearly. Stay cool.

#template #compiler
Задачка

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

Дан код на языке С++. Нужно проверить, что все скобки расположены правильно. Это значит, что каждая открытая скобка должна быть закрыта соответствующей закрывающей скобкой в правильном порядке. И каждая закрывающая скобка должна иметь соответствующую открывающую скобку.

Предположим, что наша гипотетическая функция будет принимать строку со всем кодом, а возвращать true, если все корректно, и false в обратном случае.

Cпециально не описываю условие подробно, потому что мы все здесь плюсовики и понимаем, что в себя включает С++ код. Хотя на самом деле, это чтобы вы немножко помучались и на камни все-таки наступили😈.

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

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

Вечером выйдет пост с объяснениями и решением.

Погнали решать!

P.S. Благодаря Евгению понял, что сам наткнулся на подводный камень и не учел кое-что. Давайте введем гарантию, что в коде отсутствуют директивы препроцессора и макросы, так как это значительно усложняет задачу. А также обойдемся без вложенных комментариев.

Challenge your life. Stay cool.

#задачки
Тред для ревью решений
Решение задачи через пару секунд появится в комментах
Почему не работает?

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

Что произойдет при попытке компиляции и запуска следующего кода?

#include <type_traits>

template<typename... Args>
constexpr bool AndL(Args&&... args)
{
return (... && std::forward<decltype(args)>(args));
}

template<typename... Args>
constexpr bool AndR(Args&&... args)
{
return (std::forward<decltype(args)>(args) && ...);
}

int main()
{
bool* pb = nullptr;
false && (*pb = true);
AndR(false, (*pb = true));
AndL(false, (*pb = true));
}


Ответ не то, чтобы сильно интригующий, но все же оставлю его под спойлерами, чтобы была возможно подумать, а не сразу ответ читать.

Компиляция завершится успешно, но при выполнении будет сегфолт. На вот этой строчке "AndR(false, (*pb = true));".

Коротко разберем происходящее. Допустим я хочу проверить короткосхемность логического И во всех ипостасях. То есть в обычном виде и в виде fold expression. Для fold выражения нужны функции шаблонные. Вот я и определяю 2 штуки для правой и левой свертки. Ну и есть использование оператора в обычном виде.

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

Так вот. Свойство short circuit дает операции право не вычислять следующие аргументы после того, как ответ уже будет известен. В нашем случае, первый аргумент - false и по идее дальше вычислений быть не должно.

Так и делается во второй строчке функции main. Но вот все падает на 3-й.

Хотя казалось бы должно упасть на 4-й, потому что там так фолд раскрывается, что первым будет учтен последний аргумент. Поэтому первый false нас не защитит от разыменования нулевого указателя.


На 4-й бы тоже упало, если бы не было третьей строчки. Но не по той причине, которую я указал.

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

Теперь, чтобы старшим ребятам не совсем скучно было, задам вопрос. Как сделать так, чтобы все-таки не упасть в функции, но в какой-то форме передать такие же параметры в качестве аргументов? Отписывайте в комментах свои варианты.


Train your brain. Stay cool.

#template #cpp17
Вычисления по короткой схеме vs ленивые вычисления Ч1

Вчера Евгений поднял тему различий short circuited evaluation и lazy evaluation. Действительно важная тема для обсуждений. Потому что многие путают. Это конечно почти ни на что не влияет, но прояснить все же нужно.

Разберемся с ленивыми вычислениями. Если описать этот термин лозунгом, то это будет что-то типа "вычисли меня, когда я буду нужен". Для плюсов это ненативный термин. Потому что это strict language. В таких языках, например, все параметры функции должны быть вычислены до входа в функцию. Однако, есть языки(Scala, Haskell), где такого условия нет и аргумент вычисляется только тогда, когда он нужен в теле функции. Это значит, что функция может выдать результат, даже не вычислив всех аргументов! То есть вычисление аргумента откладывается до того момента, когда его значение будет нужно.

Но такие вычисления применимы не только к вычислению аргументов, естественно. Чтобы было понятно, давайте приведу несколько примеров, но в плюсовом контексте.

Логгер-синглтон. Не хотим мы запариваться над сложным и безопасным логгером, поэтому напишем его очень просто и безобразно:

class Logger{
Logger() {}
public:
static Logger * instance() {
static Logger myinstance;
return &myinstance;
}
void Log(const std::string str) {
std::cout << str << std::endl;
}
};


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

Можно чуть сложнее придумать:

template<typename O, typename T1, typename T2>
struct Lazy
{
Lazy(T1 const& l,T2 const& r)
:lhs(l),rhs(r) {}

typedef typename O::Result Result;
operator Result() const
{
O op;
return op(lhs,rhs);
}
private:
T1 const& lhs;
T2 const& rhs;
};

struct ZeroDimentionalTensor
{
ZeroDimentionalTensor(int n) : a{n} {}
int a;
};

using ZDTensor = ZeroDimentionalTensor;

struct ZDTensorAdd
{
using Result = ZDTensor;
Result operator()(ZDTensor const& lhs,ZDTensor const& rhs) const
{
Result r = lhs.a + rhs.a;
std::cout << "Actual calculation" << std::endl;
return r;
}
};

Lazy<ZDTensorAdd,ZDTensor,ZDTensor> operator+(ZDTensor const& lhs,ZDTensor const& rhs)
{
return Lazy<ZDTensorAdd,ZDTensor,ZDTensor>(lhs,rhs);
}

int main() {
ZDTensor a{1};
ZDTensor b{2};
std::cout << "Check" << std::endl;
auto sum = a + b;
std::cout << "Check" << std::endl;
bool external_parameter = true;
ZDTensor c{0};
if (std::cout << "Condition" << std::endl; external_parameter) {
c = sum;
} else {
c = 5;
}
std::cout << c.a << std::endl;
}


Здесь мы определяем ленивую структурку, которая может выполнять какую-то бинарную операцию над какими-то объектами. Причем делает она это в операторе преобразования к типу результата бинарной операции. Это довольно важный момент. Пример и так довольно сложный, поэтому не буду мудрить с compile-time проверками правильности типов.

Дальше с помощью этой структурки я хочу лениво посчитать сумму тензоров нулевой размерности(просто посчитать сумму чиселок - не комильфо, а так я как будто бы математик в 10-м поколении).

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

И вишенка на торте - перегружаю оператор+ для того, чтобы он на сложение двух тензоров возвращал не тензор, а нашу ленивую структурку.

Ну и теперь магия в действии. ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ.

Differentiate the meanings of things. Stay cool.

#template #cppcore #cpp17
Вычисления по короткой схеме vs ленивые вычисления Ч2

Когда мы поняли, что такое ленивые вычисления, давайте определим вычисления по короткой схеме и посмотрим, чем отличаются эти термины.

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

То есть это про конкретные операторы с конкретными свойствами в конкретных языках программирования.

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

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

Однако это мышление в рамках strict languages. Посмотрим на такой псевдокод:

variable = bigAndSlowFunc() or evenSlowerFnc()
if (need_heavy_computations)
print "Here it is: ", variable
else
print "I don't need you heavy computations!"


Если вычисления - ленивые, то переменная variable вычислится только при попадании в позитивную ветку условия. В другом случае ни bigAndSlowFunc, ни evenSlowerFnc никогда не будут вычислены.

И вот когда мы уже попали в позитивную ветку, то там идет в ход короткая схема.

В С/С++ если мы вставим в рандомном месте кода

auto variable = bigAndSlowFunc() || evenSlowerFnc();

и потом изъявим возможность использования переменной внутри веток условия(чтобы компилятор не удалил из бинаря ненужные вычисления), то variable вычислится до захода в условие. Тогда как при использовании ленивых вычислений это не так.

Вопрос конечно, отчасти, философский. Только исходя из определений, можно прийти к выводу, что short circuit - это реализация ленивых вычислений. Но даже в таком случае это разные термины и нужно употреблять правильный термин в заданном контексте.

Но по мне, это разные вещи и говорят они о разном. Интересно узнать ваше мнение в комментах по этому поводу.

Differentiate the meanings of things. Stay cool.

#cppcore
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
if constexpr

Вчера мы обсуждали, что до 17-го стандарта было довольно сложно писать шаблонный код, сценарии которого зависели от типа шаблонных параметров. Появляются проблемы с дублированием кода и с фактической невозможностью писать зависящий от конкретного типа код, потому что в каких-то ветках он может просто не скомпилироваться. Но с этими проблемами справляется фича С++17 - if constexpr.

if constexpr - расширение обычной условной конструкции с двумя дополнениями/ограничениями.

1️⃣ Условие проверяется в compile-time. И нужная ветка выбирается тоже на этом этапе. Это значит, что выражение в условии должно быть тоже вычислимым на этапе компиляции. То есть должно быть constexpr. Это несколько ограничивает его функционал(например constexpr функции могут вычисляться и во время компиляции, и во время выполнения). Хотя учитывая, что конструкция используется в шаблонном коде, для разделения поведения в зависимости от шаблонного параметра, и других кейсов я особо не вижу, то это не совсем и ограничения.

2️⃣Если одна из веток условий выбрана, то другая ветка будет отброшена(discarded) и она не будет компилироваться!

Благодаря этим особенностям, наш пример с функцией to_str будет выглядеть вот так:

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


А пример со сравнением чисел так:

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 is_equal(T a, T b) {
if constexpr (is_floating_point_v<T>)
return absolute(a - b) < precision_threshold<T>;
else
return a == b;}


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

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_same_v<T, std::string>)
Life_without_coding();
else
return std::to_string(t);
}

auto str = to_str(5);


Получим ошибку: error: use of undeclared identifier 'Life_without_coding'. Эх. Не прожить нам без кода...

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

После того, как компилятор проверит отсутствие ошибок, он просто не будет дальше посылать отброшенную ветку на генерацию кода. Примерно так это и работает.

Choose the right path. Stay cool.

#cpp17 #template #compiler
Возвращаемый тип при if constexpr

Ещё одним интересным местом функционала if constexpr, является возможность возвращения совершенно разных типов из одной функции (естественно, не одновременно). То есть с использованием if constexpr мы можем иметь несколько return выражений, каждое из которых возвращает объект типа, который не конвертируется в другой. Разумеется, все такие return должны быть спрятаны в блоки кода, которые будут выбрасываться на этапе компиляции.

Представим, что вы захотели совершить пакость и посмеяться над своими тиммейтами. Вы написали функцию GetStringNumber, которая возвращает строковое представление числа, которое вы в него передали. Но вы затеяли подлянку, и если в функцию передать строку с числом, то она попытается вернуть его в виде интегрального типа. Типа у коллег код похерится, если они случайно передадут туда строку. Эта функция будет выглядеть примерно так:

template <typename T>
auto GetStringNumber(T val)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(val);
else if constexpr(std::is_same_v<T, std::string>)
return std::stoi(val);
}

Здесь мы используем фичу С++14, благодаря которой компилятор сам выводит тип возвращаемого значения.

Очевидно, что для различных веток, функция GetStringNumber будет иметь разный тип возвращаемого значения: в первом случае std::string, а во втором int. И это работает! Правда, мы легко можем всё сломать, для этого достаточно добавить ещё один return, находящийся вне условий, который будет несовместим с двумя прочими. Если мы перестараемся со своими шаловливыми мыслями, то можем написать и что-то такое:

template <typename T>
auto GetStringNumber(T val)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(val);
else if constexpr(std::is_same_v<T, std::string>)
return std::stoi(val);
return std::vector<std::string>{"Joke has gone too far"};
}


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

Такая вот интересная особенность нашлась. Поделитесь им в комментариях практическими примерами использования if constexpr. Думаю, многим пригодится опыт наших замечательных комментаторов)

Apply things in an unusual ways. Stay cool.

#cpp17 #cpp14 #template
static_assert и if constexpr

Вернемся к прошлому примеру с функцией to\_str, но сделаем ее немножко безопасней.

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(false, "cannot convert type to std::string");
}


Все просто. Функция std::to_string имеет перегрузки только для тривиальных арифметических типов, поэтому чтобы ее использовать и получить ошибку, нужно провести проверку на эту арифметичность. Если переданный в функцию to\_str тип не конструируется в строку или не является арифметическим типом, то не совсем понятно, как такой тип переводить в строку. Точнее так: в каждом конкретном случае мы можем сказать как, но общего поведения нет. Поэтому, чтобы детектировать такие нештатные ситуации, я поставлю ассерт времени компиляции.

И вроде всё хорошо, но да не всё...

Дело в том, что при попытке скомпилировать этот пример, процесс прервется на этом ассерте даже в том случае, когда мы попадем в первые две ветки.

Тут в чем прикол. Согласно стандарту, если хотя бы в одной из веток if constexpr не может быть сгенерировано ни одной валидной специализации, то программа - ill-formed и это ошибка. И вправду, false всегда будет false, как бы мы там не тужились родить что-то валидное.

Но даже сам cppreference предлагает нам способ обойти эту неприятную бяку. Мы можем подставить вместо false шаблонное constexpr типазависимое выражение, которое будет возвращать false. И тогда все магическим образом заработает.

template<typename>
inline constexpr bool dependent_false_v = false;

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(dependent_false_v<T>, "cannot convert type to std::string");
}


Видимо из-за того, что шаблон можно вручную специализировать, то теоритически возможна специализация, которая будет возвращать true. И даже если она еще не инстанцирована, то возможность-то остается. Поэтому компилятор пропускает такую конструкцию.

Однако ситуация изменилась с приходом С++23, но об этом мы поговорим завтра.

Find loopholes in the system. Stay cool.

#cpp17 #template
Обновления в отношениях static_assert и if constexpr

Наш подписчик Вадим упомянул о важном изменении в отношениях static_assert и if constexpr. Эти изменения вступили в силу с приходом С++23 и говорят о том, что теперь программа не считается ill-formed, даже если static_assert фейлится для всех специализаций.

Посмотрим на уже заезженном примере:

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(false, "cannot convert type to std::string");
}

class A{};

int main()
{
std::cout << to_str("qwe") << std::endl; // OK
std::cout << to_str(5.0) << std::endl; // OK
std::cout << to_str(A{}) << std::endl; // static_assert failed
}


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

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

До прихода 23-х плюсов была очевидная проблема с этим static_assert'ом. Вроде бы это очень логично писать условия как в примере выше и ожидать, что это сработает. Да и наличие смешного донельзя воркэраунда с шаблонным типозависимым выражением, которое все равно возвращает false, как бы намекает, что что-то не так. Радостно видеть развитие языка и закрытие таких болючих пробелов.

Fix your flaws. Stay cool.

#cpp23 #template