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

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Вычисления по короткой схеме

Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин short-circuit evaluation и понял, что, в целом, тема достойна поста.

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

Посмотрим, что это значит.

В плюсах есть два логических оператора, которые работают по этому признаку - && и ||. Логические И и ИЛИ.

Когда мы пишем if (expression1 && expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к false, то результат всего составного условия - false. А expression2 даже не вычисляется. Если expression1 приводится к true, то вычисляем expression2 и уже его значение определит результат. Такое поведение вполне понятно. Выражение с логическим И истинно тогда и только тогда, когда истинны оба операнда. А если один из них ложный - тогда и все выражение ложно. Тогда нет смысла тратить время на вычисление expression2, если оно никак не повлияет на результат операции.

По аналогии работает оператор ИЛИ. Когда мы пишем if (expression1 || expression2) это значит, что сначала вычисляется expression1 и смотрится его значение. Если оно приводится к true, то результат всего составного условия - true. И, естественно, expression2 даже не вычисляется. Если expression1 приводится к false, то вычисляем expression2 и уже его значение определит результат. Все опять же исходит от определения. Выражение с логическим ИЛИ ложно тогда и только тогда, когда ложны оба операнда. А если один из них истинный - тогда и все выражение истинно. (Немного копипасты, но, надеюсь, вы выдержали).

Если немного обобщить, то в выражениях вида p1 && p2 && p3... либо p1 || p2 || p3 … вычисление продолжается слева направо, пока очередной операнд не даст false или true соответственно.

Почему это вообще важно?

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

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

Use preconditions for making important choice. Stay cool.

#cppcore
🔥24👍104
Подробности про std::conjunction vs &&

В этом посте я рассказывал про замечательные тайптрейты std::conjunction, std::disjunction. Они позволяют компоновать несколько трейтов в одну логическую последовательность. Там же я рассказывал про то, что до них для этих целей использовались операторы &&, ||. Безусловно, человеческим языком обозванные сущности проще воспринимаются, чем какие-то символы. Но неужели это все различия? Какая-то вялая причина, чтобы вводить в стандарт эти трейты.

И правда, различия есть. Еще какие!

Прежде, чем начать разбирать их, нужно поподробнее рассмотреть эти метаклассы, потому что оттуда все различия. Рассматривать будем на примере std::conjunction, ибо у них все очень похоже.

Примерно так этот класс может быть реализован

template<class...> struct conjunction : std::true_type
template<class B1> struct conjunction<B1> : B1 {};
template<class B1, class... Bn>
struct conjunction<B1, Bn...>
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};


Специализация std::conjunction<B1, ..., Bn> имеет публичную базу, которая варьируется в зависимости от аргументов

Если их нет, то базовым классом для std::conjunction будет std::true_type.

Если они есть, то базой будет первый тип Bi из B1, ..., Bn, для которого bool(Bi::value) == false.

Если для всех Bi bool(Bi::value) == true, тогда базой будет Bn.

Кстати std::conjunction не обязательно по итогу наследуется либо от std::true_type, либо от std::false_type: она просто наследует от первого B, для которого его ::value, явно преобразованное в bool, является ложным, или от самого последнего B, когда все они преобразуются в true. То есть это самое value может быть даже не булевым значением, а например числом. Вот так:

std::conjunction<std::integral_constant<int, 2>,std::integral_constant<int, 4>>::value == 4 - верно!


И вот в этом весь прикол. std::conjunction - это вычисление по короткой схеме! То есть как только мы нашли такой Bi, что для него bool(Bi::value) == false, компилятор прекращает дальше инстанцировать вглубь рекурсии и однозначно определяет тип базового класса, а значит и поля value.

И как раз таки в этом аспекте метаклассы conjunction и disjunction отличаются от обычных && и ||. Но об этом мы поговорим завтра.

Understand true essence of things. Stay cool.

#cpp17 #template #hardcore
👍156🔥6❤‍🔥1
std::conjunction vs &&

Вчера мы поговорили о том, что инстанцирование в std::conjunction и std::disjunction происходит по короткой схеме. Как только мы нашли шаблонный аргумент, для которого будет валидно выражение bool(Bi::value) == false, инстанциация остальных аргументов прекращается и выводится тип std::conjunction::value.

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

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>;


компиляция вот этого кода закончится с ошибкой error: value is not a member of type_without_value<int>. Хотя float - совсем не интергральный тип и при работающей короткой схеме вычислений, ошибки компиляции не было бы. Потому что второй тип даже не инстанцировался бы. Как в случае с std::conjunction.

template <typename T>
struct valid_except_void : std::false_type
{
};

template <>
struct valid_except_void<void>
{
};

template <class T1, class T2>
constexpr auto test = std::conjunction_v<valid_except_void<T1>, valid_except_void<T2>>;

constexpr auto result = test<float, void>;


Такой код успешно скомпилируется и result будет равен false. Потому что инстанциация первого шаблона valid_except_void<T1> завершится успешно и для него bool(value) == false. Поэтому validexceptvoid с войдом не испортит нам игру.

То есть преимущество std::conjunction в том, что как только мы нашли его аргумент, для которого bool(value) == false, то все последующие аргументы не инстанцируются. Это может быть полезно, когда последующие типы очень затратные при инстанцировании или могут вызвать фатальные ошибки, если их инстанцировать с неправильным типом.

Прошу прощение за частое употребление слова "инстанцировать". Я просто не знаю нормального аналога/синонима. Кто знает, поделитесь в комментах.

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

Always compare your tools. Stay cool.

#template #cpp17
🔥13👍54
Сумма трех

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

Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.

Дан массив интов arr. Как найти в этом массиве триплет {arr[i], arr[j], arr[k]}, где i != j, i != k, j != k, такой что arr[i] + arr[j] + arr[k] = 0? Напишите функцию, которая возвращает все такие триплеты. Если их нет, то верните пустой массив.

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

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

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

Challenge yourself. Stay cool.

#задачки
10🔥6👍4😱1
Тред для ваших решений
Решение суммы трех в комментах под этим постом
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
👍18🔥93
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
🔥11👍43😁3🥴1
Принтуем с 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
🔥10👍75
Что выведется на экран?

Попробуем новую рубрику на канале - #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. Почти наверняка так и будет.
(Опрос следующим постом выйдет)
🔥8👍42
Что выведется в консоль в этом случае?
Anonymous Poll
39%
1
10%
0
22%
-1
10%
2^32 - 1
14%
Мне-то откуда знать???
4%
Hello, World!!!
🔥62👍2
Ответ на вопрос выше не так уж и прост на самом деле. Начнем с того, что когда вы запустите этот код на своей машине, то получите ответ: -1.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Stay surprised. Stay cool.

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

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

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

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

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

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

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

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

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

Enjoy your achievements. Stay cool.
39🔥15🍾15👍4🏆2
Способы узнать знак целого числа

Иногда появляется необходимость узнать, является ли данное число отрицательным или нет. За примером далеко ходить не будем. В одной из наших задачек про переворачивание десятичных цифр числа один из подходов к решению может быть таким: узнать, является ли число отрицательным, если да, то домножить его на -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
🔥85👍5
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
👍137🔥7
Бинарные логические операторы. 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
🔥8👍64🤯3
Задачка

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

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

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

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

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

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

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

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

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

Challenge your life. Stay cool.

#задачки
👍8😁32🔥2
Тред для ревью решений
Решение задачи через пару секунд появится в комментах
👍53🔥3
Почему не работает?

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

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

#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
8👍6🔥3