Единица трансляции
В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" aka translation unit aka юнит. На канале много не нюхавшего прода народа, поэтому пришла в голову идея закрыть этот гештальт.
Вот вы изучаете плюсы. Долго и упорно. И сделали свой первый нетривиальный проект. И бежите радостный показывать его маме со словами: "Мама! Смотри, какую я программу написал.". И показываете ей, как ваши условные крестики-нолики работают. Это вы запустили исполняемый файл, который является результатом превращения вашего кода на с++ в машинный код. Но каким образом все многообразие файлов собирается в одну сущность? Как они между собой взаимодействуют для этого?
В рамках культуры разработки на С++ есть 2 принципиальных вида файлов: headers(заголовочные файлы)(.h|.hpp) и sources(файлы реализации)(.cxx|.cpp). Первые обычно предназначены для помещения в них сущностей, которые будут широко(или потенциально широко) использоваться в проекте. Вторые уже конкретно реализуют поведение этих сущностей. Ответ на вопросы: "Зачем такое разделение в принципе есть?" и "Зачем мы пишем множество файлов реализации?"- заслуживают отдельных постов. Просто примем за данность всем известный факт существования двух видов файлов.
Но на вход компилятора попадает только один тип файлов - sources(не будем касаться precompiled headers и сильно усложнять разговор). На самом деле компилятора может сожрать любой подходящий файл, просто так принято, что это .cpp файлы.
Стандартный source файл состоит из кучи заинклюженых заголовочников, возможно еще несколько видов директив препроцессора, типа #ifdef или #define и прочего, ну и, собственно, нашего кода. Когда такой файл пожирает компилятор, на самом деле первым в работу вступает препроцессор. Это такой предварительный обработчик файлов с кодом. В С и С++ есть несколько директив препроцессора, которые предназначены именно для этого обработчика. Он оперирует в основном текстом программы и фактически делает текстовые манипуляции. Например, директива #include"something.hpp" заменяется на полный текст файла something.hpp. Директива #define MAXARRAYSIZE 10 обозначает, что во всем файле строку MAXARRAYSIZE нужно заменить на 10. Причем 10 это будет прям строка в файле, это надо учитывать. Никакой проверки типов на этом этапе нет.
И вот файл, который получается после обработки исходника препроцессором называется единицей трансляции. То есть это базовый элемент компиляции С++ программы. Компилятор обрабатывает все единицы трансляции, превращая их в объектные файлы. И уже линкер собирает все эти объектные файлы в одну программу.
То есть и вправду не очень корректно говорить, что компилируются цппшники. Это делают все-таки единицы трансляции. Но я не оч понимаю людей, которые хейтят других за это, потому что просто культурой принято файлы реализации называть .cpp. А то, что там есть препроцессор - это и так понятно. Это понимает почти любой, кто писал #include <iostream>, а то есть почти все. Я уж преувеличиваю, но вы поняли мою мысль.
Divide et impera. Stay cool.
#compiler #cppcore
В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" aka translation unit aka юнит. На канале много не нюхавшего прода народа, поэтому пришла в голову идея закрыть этот гештальт.
Вот вы изучаете плюсы. Долго и упорно. И сделали свой первый нетривиальный проект. И бежите радостный показывать его маме со словами: "Мама! Смотри, какую я программу написал.". И показываете ей, как ваши условные крестики-нолики работают. Это вы запустили исполняемый файл, который является результатом превращения вашего кода на с++ в машинный код. Но каким образом все многообразие файлов собирается в одну сущность? Как они между собой взаимодействуют для этого?
В рамках культуры разработки на С++ есть 2 принципиальных вида файлов: headers(заголовочные файлы)(.h|.hpp) и sources(файлы реализации)(.cxx|.cpp). Первые обычно предназначены для помещения в них сущностей, которые будут широко(или потенциально широко) использоваться в проекте. Вторые уже конкретно реализуют поведение этих сущностей. Ответ на вопросы: "Зачем такое разделение в принципе есть?" и "Зачем мы пишем множество файлов реализации?"- заслуживают отдельных постов. Просто примем за данность всем известный факт существования двух видов файлов.
Но на вход компилятора попадает только один тип файлов - sources(не будем касаться precompiled headers и сильно усложнять разговор). На самом деле компилятора может сожрать любой подходящий файл, просто так принято, что это .cpp файлы.
Стандартный source файл состоит из кучи заинклюженых заголовочников, возможно еще несколько видов директив препроцессора, типа #ifdef или #define и прочего, ну и, собственно, нашего кода. Когда такой файл пожирает компилятор, на самом деле первым в работу вступает препроцессор. Это такой предварительный обработчик файлов с кодом. В С и С++ есть несколько директив препроцессора, которые предназначены именно для этого обработчика. Он оперирует в основном текстом программы и фактически делает текстовые манипуляции. Например, директива #include"something.hpp" заменяется на полный текст файла something.hpp. Директива #define MAXARRAYSIZE 10 обозначает, что во всем файле строку MAXARRAYSIZE нужно заменить на 10. Причем 10 это будет прям строка в файле, это надо учитывать. Никакой проверки типов на этом этапе нет.
И вот файл, который получается после обработки исходника препроцессором называется единицей трансляции. То есть это базовый элемент компиляции С++ программы. Компилятор обрабатывает все единицы трансляции, превращая их в объектные файлы. И уже линкер собирает все эти объектные файлы в одну программу.
То есть и вправду не очень корректно говорить, что компилируются цппшники. Это делают все-таки единицы трансляции. Но я не оч понимаю людей, которые хейтят других за это, потому что просто культурой принято файлы реализации называть .cpp. А то, что там есть препроцессор - это и так понятно. Это понимает почти любой, кто писал #include <iostream>, а то есть почти все. Я уж преувеличиваю, но вы поняли мою мысль.
Divide et impera. Stay cool.
#compiler #cppcore
🔥15👍5❤3
static функции
В этом посте были краткие выжимки из того, как ключевое слово static влияет на сущности. Сегодня будем разбирать функции.
Для начала надо понимать базовые настройки функции, чтобы отталкиваться от этого в контексте static.
Функция - блок кода в .text section, то есть просто в области, где находится код. Этому куску кода соответствует определенная метка - замангленное имя функции(видимо уже пора делать пост про манглинг, а то много упоминаний без объяснений). Когда функцию хотят вызвать, то это делается через инструкцию call, которая принимает метку функции. Этой метке после линковки будет соответствовать конкретный адрес, которому и будет передано исполнение кода во время выполнения программы.
Mangled name функции формируется только на основе ее сигнатуры. Поэтому любой код, который знает только лишь(!) сигнатуру функции, то есть ее объявление, знает трушное название функции(ту самую метку). Вот теперь интересности.
По дефолту функции имеют внешнее связывание.
Для текущей единицы трансляции все тривиально. Есть метка, мы можем просто перейти на нее.
Но внешнее связывание значит, что и другие единицы трансляции могут видеть эту функцию, не зная ее определение! Не только видеть, но и вызвать! Как? Имея правильное объявление функции, текущая единица трансляции получает доступ к замангленному имени функции. А в коде появится такая строчка: call label. Прикол в том, что до этапа линковки мы можем пытаться в коде вызывать вообще любые функции и нам это будет сходить с рук. А вот уже работа линкера заключается в том, чтобы сопоставить метку из вызова с адресом реальной функции. И если линкер найдет код для этой метки в другой единице трансляции, то он просто подставит адрес, соответствующий метке, в call и все будет чики-пуки.
Ну и для того, чтобы линкер в принципе смог определить, что текущую метку могут видеть все остальные единицы трансляции, ее надо пометить как .globl label. Логично предположить, что так обозначаются глобальные для всей программы сущности, коей и является базовая функция.
Я описываю все сильно верхнеуровнево(насколько это возможно, обсуждая ассемблер ахха). Но вроде должно быть понятно.
Теперь вернемся к нашим static баранам. Что тут на самом деле меняется. Сильно верхнеуровнего - меняется тип связывания с внешнего на внутреннее. Это значит, что другие единицы трансляции просто перестают видеть эту функцию. Звучит прикольно, но как конкретно это изменение достигается?
На самом деле всего двумя деталями.
1) Пометка .globl label больше не генерируется.
2) Появляется заглавная L перед именем функции(которое в с++ коде было) в ее замангленном варианте.
Что это дает. Даже если мы знаем сигнатуру функции и объявили ее в другой единице трансляции, то на этапе линковки компоновщик посмотрит на реальное определение функции, не увидит пометку о глобальности символа, распарсит замангленное имя и увидит эту букву L и поймет, что это локальная функция для этой единицы трансляции. И не будет резолвить этот символ. Если линкер не найдет подходящего глобального определения в остальных юнитах трансляции, то произойдет ошибка линковки - undefined reference.
И на самом деле, локальная видимость функции открывает дорогу к некоторым оптимизациям. Например, компилятор может решить, что функция подходит для inline expantion и встроить все ее вызовы. Но раз в текущем юните код функции не нужен(его полностью встроили везде, где требуется), а в других его никто не должен видеть, то компилятор просто удалит метку этой функции и ее сгенерированный код. Это позволяет уменьшить размер бинаря. Мы конечно его увеличиваем за счет встраивания кода функции. Но лучше так, чем оставлять бесполезный код в бинарнике.
Hide your secrets. Stay cool.
#compiler #cppcore #optimization
В этом посте были краткие выжимки из того, как ключевое слово static влияет на сущности. Сегодня будем разбирать функции.
Для начала надо понимать базовые настройки функции, чтобы отталкиваться от этого в контексте static.
Функция - блок кода в .text section, то есть просто в области, где находится код. Этому куску кода соответствует определенная метка - замангленное имя функции(видимо уже пора делать пост про манглинг, а то много упоминаний без объяснений). Когда функцию хотят вызвать, то это делается через инструкцию call, которая принимает метку функции. Этой метке после линковки будет соответствовать конкретный адрес, которому и будет передано исполнение кода во время выполнения программы.
Mangled name функции формируется только на основе ее сигнатуры. Поэтому любой код, который знает только лишь(!) сигнатуру функции, то есть ее объявление, знает трушное название функции(ту самую метку). Вот теперь интересности.
По дефолту функции имеют внешнее связывание.
Для текущей единицы трансляции все тривиально. Есть метка, мы можем просто перейти на нее.
Но внешнее связывание значит, что и другие единицы трансляции могут видеть эту функцию, не зная ее определение! Не только видеть, но и вызвать! Как? Имея правильное объявление функции, текущая единица трансляции получает доступ к замангленному имени функции. А в коде появится такая строчка: call label. Прикол в том, что до этапа линковки мы можем пытаться в коде вызывать вообще любые функции и нам это будет сходить с рук. А вот уже работа линкера заключается в том, чтобы сопоставить метку из вызова с адресом реальной функции. И если линкер найдет код для этой метки в другой единице трансляции, то он просто подставит адрес, соответствующий метке, в call и все будет чики-пуки.
Ну и для того, чтобы линкер в принципе смог определить, что текущую метку могут видеть все остальные единицы трансляции, ее надо пометить как .globl label. Логично предположить, что так обозначаются глобальные для всей программы сущности, коей и является базовая функция.
Я описываю все сильно верхнеуровнево(насколько это возможно, обсуждая ассемблер ахха). Но вроде должно быть понятно.
Теперь вернемся к нашим static баранам. Что тут на самом деле меняется. Сильно верхнеуровнего - меняется тип связывания с внешнего на внутреннее. Это значит, что другие единицы трансляции просто перестают видеть эту функцию. Звучит прикольно, но как конкретно это изменение достигается?
На самом деле всего двумя деталями.
1) Пометка .globl label больше не генерируется.
2) Появляется заглавная L перед именем функции(которое в с++ коде было) в ее замангленном варианте.
Что это дает. Даже если мы знаем сигнатуру функции и объявили ее в другой единице трансляции, то на этапе линковки компоновщик посмотрит на реальное определение функции, не увидит пометку о глобальности символа, распарсит замангленное имя и увидит эту букву L и поймет, что это локальная функция для этой единицы трансляции. И не будет резолвить этот символ. Если линкер не найдет подходящего глобального определения в остальных юнитах трансляции, то произойдет ошибка линковки - undefined reference.
И на самом деле, локальная видимость функции открывает дорогу к некоторым оптимизациям. Например, компилятор может решить, что функция подходит для inline expantion и встроить все ее вызовы. Но раз в текущем юните код функции не нужен(его полностью встроили везде, где требуется), а в других его никто не должен видеть, то компилятор просто удалит метку этой функции и ее сгенерированный код. Это позволяет уменьшить размер бинаря. Мы конечно его увеличиваем за счет встраивания кода функции. Но лучше так, чем оставлять бесполезный код в бинарнике.
Hide your secrets. Stay cool.
#compiler #cppcore #optimization
👍17🔥8❤4
Объединения условий в enable_if
Иногда мы хотим сильно ограничить свойства типов, с которыми мы хотим инстанцировать шаблон. Например, тип должен быть default-constructed и иметь оператор сравнения. У нас есть для этого метафункция std::enable_if, которая позволяет нам проверять наличие свойств у типов. Но вот незадача, как проверить два условия одновременно? Я хочу и то, и то.
Ранее для этого использовались обычные операторы && и || между тайптрейтами.
Однако в С++17 появились специальные метаклассы, которые позволяют комбинировать условия. Это
• template<class... B> struct conjunction; - логическое И
• template<class... B> struct disjunction; - логическое ИЛИ
• template<class B> struct negation; - логичесткое НЕ
Эти трейты имеют подходящие осмысленные имена, поэтому их использование повышает читаемость кода.
Например, вы можете сочетать эти метафункции с variadic шаблонами
Тогда может появиться что-то такое:
В функцию PrintIntegers мы можем передать сколько угодно(почти) аргументов и все они будут проверяться на соответствие целочисленному типу
Или например вы хотите принтовать только числа? Тогда мы идем к вам:
Если тип - не целочисленный и не с плавающей точкой, то такая перегрузка будет отбрасываться.
Ну и например вы хотите, чтобы функция для вывода в консоль не принимала аргумент-указатель. Тоже можно сделать.
Такие вот удобные метафункции.
Create complex conditions. Stay cool.
#cpp17 #template
Иногда мы хотим сильно ограничить свойства типов, с которыми мы хотим инстанцировать шаблон. Например, тип должен быть default-constructed и иметь оператор сравнения. У нас есть для этого метафункция std::enable_if, которая позволяет нам проверять наличие свойств у типов. Но вот незадача, как проверить два условия одновременно? Я хочу и то, и то.
Ранее для этого использовались обычные операторы && и || между тайптрейтами.
Однако в С++17 появились специальные метаклассы, которые позволяют комбинировать условия. Это
• template<class... B> struct conjunction; - логическое И
• template<class... B> struct disjunction; - логическое ИЛИ
• template<class B> struct negation; - логичесткое НЕ
Эти трейты имеют подходящие осмысленные имена, поэтому их использование повышает читаемость кода.
Например, вы можете сочетать эти метафункции с variadic шаблонами
Тогда может появиться что-то такое:
template<typename... Ts>
std::enable_if_t<std::conjunction_v<std::is_same<int, Ts>...>> PrintIntegers(Ts ... args)
{
(std::cout << ... << args) << 'n';
}
В функцию PrintIntegers мы можем передать сколько угодно(почти) аргументов и все они будут проверяться на соответствие целочисленному типу
Или например вы хотите принтовать только числа? Тогда мы идем к вам:
template<typename T, typename = std::enable_if_t<std::disjunction_v<std::is_integral<T>, std::is_floating_point<T>>>>
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}
Если тип - не целочисленный и не с плавающей точкой, то такая перегрузка будет отбрасываться.
Ну и например вы хотите, чтобы функция для вывода в консоль не принимала аргумент-указатель. Тоже можно сделать.
template<typename T, typename = std::enable_if_t<std::negation_v<std::is_pointer<T>>>>
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}
Такие вот удобные метафункции.
Create complex conditions. Stay cool.
#cpp17 #template
👍22🔥8❤4😁2🌭1
Вычисления по короткой схеме
Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин 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
Базовое и очень важное понятие для программирования в принципе и на плюсах в частности. Встретил просто английский термин 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👍10❤4
Подробности про std::conjunction vs &&
В этом посте я рассказывал про замечательные тайптрейты std::conjunction, std::disjunction. Они позволяют компоновать несколько трейтов в одну логическую последовательность. Там же я рассказывал про то, что до них для этих целей использовались операторы &&, ||. Безусловно, человеческим языком обозванные сущности проще воспринимаются, чем какие-то символы. Но неужели это все различия? Какая-то вялая причина, чтобы вводить в стандарт эти трейты.
И правда, различия есть. Еще какие!
Прежде, чем начать разбирать их, нужно поподробнее рассмотреть эти метаклассы, потому что оттуда все различия. Рассматривать будем на примере std::conjunction, ибо у них все очень похоже.
Примерно так этот класс может быть реализован
Специализация 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 - это вычисление по короткой схеме! То есть как только мы нашли такой Bi, что для него bool(Bi::value) == false, компилятор прекращает дальше инстанцировать вглубь рекурсии и однозначно определяет тип базового класса, а значит и поля value.
И как раз таки в этом аспекте метаклассы conjunction и disjunction отличаются от обычных && и ||. Но об этом мы поговорим завтра.
Understand true essence of things. Stay cool.
#cpp17 #template #hardcore
В этом посте я рассказывал про замечательные тайптрейты 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
👍15❤6🔥6❤🔥1
std::conjunction vs &&
Вчера мы поговорили о том, что инстанцирование в std::conjunction и std::disjunction происходит по короткой схеме. Как только мы нашли шаблонный аргумент, для которого будет валидно выражение bool(Bi::value) == false, инстанциация остальных аргументов прекращается и выводится тип std::conjunction::value.
Однако при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы. Что в форме fold expression, что в явной последовательной форме. То есть
компиляция вот этого кода закончится с ошибкой error: value is not a member of type_without_value<int>. Хотя float - совсем не интергральный тип и при работающей короткой схеме вычислений, ошибки компиляции не было бы. Потому что второй тип даже не инстанцировался бы. Как в случае с std::conjunction.
Такой код успешно скомпилируется и result будет равен false. Потому что инстанциация первого шаблона valid_except_void<T1> завершится успешно и для него bool(value) == false. Поэтому validexceptvoid с войдом не испортит нам игру.
То есть преимущество std::conjunction в том, что как только мы нашли его аргумент, для которого bool(value) == false, то все последующие аргументы не инстанцируются. Это может быть полезно, когда последующие типы очень затратные при инстанцировании или могут вызвать фатальные ошибки, если их инстанцировать с неправильным типом.
Прошу прощение за частое употребление слова "инстанцировать". Я просто не знаю нормального аналога/синонима. Кто знает, поделитесь в комментах.
Для ценителей метапрограммирования оставляю ссылку на годболт, чтобы вы могли поиграться с кодом.
Always compare your tools. Stay cool.
#template #cpp17
Вчера мы поговорили о том, что инстанцирование в 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👍5❤4
Сумма трех
Надеюсь, что вы и ваши близкие живы и здоровы. Чтобы начать новую неделю со свежей головой и отвлеченными мыслями, предлагаю порешать задачку.
Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.
Дан массив интов arr. Как найти в этом массиве триплет {arr[i], arr[j], arr[k]}, где i != j, i != k, j != k, такой что arr[i] + arr[j] + arr[k] = 0? Напишите функцию, которая возвращает все такие триплеты. Если их нет, то верните пустой массив.
Очень важно заметить, что в ответе не принимаются дубликаты триплетов. То есть не только индексы не должны повторяться, но и сами наборы чисел в тройках. Это значит, что даже банальным брут форсом простыми тремя циклами без дополнительной памяти вы не сможете решить.
Напомню про формат: под этим постом пишут совсем зеленые, кто не знает, правильно ли они решают или нет. Под следующим постом мы разбираем конкретные решения людей, которые желают получить фитбэк. Если вы точно знаете ответ и хотите его сказать, то прошу воздержаться от комментариев. Если они все же останутся, то оставьте их под постом с решением, который выйдет вечером.
Погнали решать!
Challenge yourself. Stay cool.
#задачки
Надеюсь, что вы и ваши близкие живы и здоровы. Чтобы начать новую неделю со свежей головой и отвлеченными мыслями, предлагаю порешать задачку.
Есть знаменитая в кругах решателей алгосов задача - сумма двух. Решается просто, понимается просто. Идеальная задачка для начинающих. Но сегодня я задам вам похожий по формулировке, но уже не такой простой вопрос.
Дан массив интов 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, которая складывает все аргументы, которые ей передали, рекурсивно. Мы должны определить базу для рекурсии. Выглядит это так:
Если бы у нас не было первого определения, то рекурсия дошла бы до нуля аргументов и не смогла бы инстанцировать функцию без аргументов и компиляция бы провалилась.
Но важно еще кое-что заметить. Что мы так или иначе предполагаем, что все наши аргументы могут успешно быть сложены с интом. Это довольно сильное ограничение, потому что может я хочу и матрицы складывать тоже этой функцией. А тут такого сделать не получится.
Но решение этих проблем есть!
Называется fold expression. Появилось это спасение в С++17 и позволяет писать намного более простой код. Посмотрим, как будет выглядеть прошлый пример при его использовании.
Никаких дополнительных определений и мы можем хоть обезьянок складывать, хоть их испражнения(и все в одной функции).
Однако есть все-таки одно ограничение. Функцию Sum не получится инстанцировать без аргументов. Это свойство оператора сложения. И об этом в том числе мы поговорим завтра, когда будем подробнее разбирать внутрянку fold expression.
Make things simplier. Stay cool.
#cpp11 #cpp17 #template
Стандарт 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🔥9❤3
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 - сворачивание всего пака шаблонных параметров с помощью комбинации синтаксиса 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👍4❤3😁3🥴1
Принтуем с fold expression
Для начала разберем, как бы все выглядело до С++17.
Задача - вывести на экран все аргументы функции подряд. Не так уж и сложно. Код будет выглядеть примерно вот так:
Это будет работать, но не очень прикольно выводить аргументы прям подряд символами. Нужно какое-то форматирование. Например, между аргументами выводить пробел, а в конце перенести строку. Тут уже все несколько усложняется...
Нам мало того, что пришлось использовать базу рекурсии, так еще и прокси-функцию, которая допиливает форматирование. Слишко МНОГА БУКАВ. Ща исправим.
Вот так будет выглядеть базовый принт без форматирования на fold expression:
Уже лучше. Точнее не так. Проще не бывает уже)
Как видите, здесь я использую бинарный левый фолд. В качестве инициализатора выступает стандартный поток вывода и он слева не только потому, что так обычно принято, а потому что оператор << также применяется например для бинарного сдвига. И чтобы мы всегда именно в поток писали, нужно, чтобы слева всегда был нужный поток. Тогда будет вызываться соответствующая перегрузка для ostream'ов и каждый раз будет возвращаться ссылка на этот поток. Таким образом мы и будем продолжать писать именно в него.
Но как тут быть с форматингом? args тут просто раскроются в последовательность "arg1 << arg2 << arg3" и тд
И непонятно, как в таких условиях добавить вывод пробела, не придумывая нагромождения в виде проксей и прочего. Для решения этой проблемы надо воспользоваться двумя хаками:
1️⃣ Не обязательно использовать сырой пакет параметров. Можно использовать функцию, принимающую этот пак.
2️⃣ Применяя оператор запятую, мы можем в операндах выполнять любое выражение, даже возвращающее void.
Получается такая штука:
Здесь мы за счет лямбды и запятой выполняем каждый раз отдельную операцию вывода в поток с пробелом. А затем вместо init выражения подставляем вывод конца строки.
Прикольный хак, я считаю! В нем много нюансов, но его явно можно взять себе на вооружение и использовать в специфичных кейсах.
Hack this life. Stay cool.
#cpp17 #template
Для начала разберем, как бы все выглядело до С++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👍7❤5
Что выведется на экран?
Попробуем новую рубрику на канале - #quiz. Мы задаем вопрос - а вы выбираете один из предоставленных ответов. Все обсуждения в комментах. А вечером выходит пост с подробными объяснениями. Погнали!
Допустим, я хочу сдвинуть 4-х байтное знаковое число на 31 бит вправо и вывести значение получившегося числа.
Для определенности предположим, что number = -12.
Знаю, знаю. Я не совсем больной ублюдок, чтобы заставлять вас отрицательные числа в бинарный формат хранения переводить.
Считайте, что -12 представляется в памяти, как 1111 1111 1111 1111 1111 1111 1111 0100. Почти наверняка так и будет.
(Опрос следующим постом выйдет)
Попробуем новую рубрику на канале - #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👍4❤2
Что выведется в консоль в этом случае?
Anonymous Poll
39%
1
10%
0
22%
-1
10%
2^32 - 1
14%
Мне-то откуда знать???
4%
Hello, World!!!
🔥6❤2👍2
Ответ на вопрос выше не так уж и прост на самом деле. Начнем с того, что когда вы запустите этот код на своей машине, то получите ответ: -1.
Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".
Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)
Заинтригованы?
Тогда мы идем к вам!
Что было до С++20.
Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.
Почему я тогда утверждаю, что вы на своих машинах получите -1?
Потому что в большинстве реализаций правый битовый сдвиг для знаковых чисел работает с помощью так называемого "арифметического сдвига".
Что это за акула такая.
Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.
С++20 начинает нам гарантировать, что правый сдвиг для знаковых чисел выполняется с помощью арифметического сдвига. Спасибо Дмитрию за это уточнение)
Вот такой прикол. Надеюсь, что я многих удивил тем, как это работает)
Арифметический сдвиг - хоть теперь и стандартное поведение, но
Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение
Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт
В-третьих, слишком мало времени прошло с момента стандартизации. Если раньше это было грязным нестандартным приемом, то в умах людей он таковым еще долго останется. И это не изменится взмахом палочки комитета.
Поэтому не стоит использовать такие приколы без острой необходимости и правильного, подробного комментирования в случае этой необходимости.
Stay surprised. Stay cool.
#cpp20 #compiler
Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".
Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)
Заинтригованы?
Тогда мы идем к вам!
Что было до С++20.
Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.
Почему я тогда утверждаю, что вы на своих машинах получите -1?
Потому что в большинстве реализаций правый битовый сдвиг для знаковых чисел работает с помощью так называемого "арифметического сдвига".
Что это за акула такая.
Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.
С++20 начинает нам гарантировать, что правый сдвиг для знаковых чисел выполняется с помощью арифметического сдвига. Спасибо Дмитрию за это уточнение)
Вот такой прикол. Надеюсь, что я многих удивил тем, как это работает)
Арифметический сдвиг - хоть теперь и стандартное поведение, но
Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение
Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт
В-третьих, слишком мало времени прошло с момента стандартизации. Если раньше это было грязным нестандартным приемом, то в умах людей он таковым еще долго останется. И это не изменится взмахом палочки комитета.
Поэтому не стоит использовать такие приколы без острой необходимости и правильного, подробного комментирования в случае этой необходимости.
Stay surprised. Stay cool.
#cpp20 #compiler
🔥21❤6👍6
У нас большой праздник!!!
Вчера на канале случился юбилей - к нам подписался наш тысячный подписчик и соратник!!! Ура!!! Ура!!! Ураааааа!!!
Это большое достижение для нас. Не только админы виноваты в этом успехе. Все участники коммьюнити поспособствовали достижению этой цифры. Благодаря вашим реакциям, вашим репостам, вашим комментариям в канале создается атмосфера уюта, профессионализма и позитива. Людям это нравится, они подписываются и остаются с нами надолго.
Поэтому хочу сказать Вам всем: Спасибо огромное!!!!
Вы мотивируете нас делать крутой и глубокий контент. Каждая ваша благодарность и приятные слова как мед на сердца админов. Это придает какой-то смысл чтоли нашему труду. Это очень теплое чувство, когда ты понимаешь, что приносишь реальную пользу людям.
Спасибо всем ребятам, кто перешел с других каналов. Спасибо всем ребятам, кто пришел с LinkedIn. Спасибо всем, кто каким-то образом нашел наш канал и стал частью сообщества. Вы все крутые люди и профессионалы. Вы настоящие герои этого дня!!!
Мысли по созданию канала были уже давненько, но решение о его создании было принято нами в знаменитом самом маленьком стейкхаусе в мире «Steak Me Truck» в Нижнем Новгороде. Это был конец сентября того года.
Сегодня, чтобы отметить наш юбилей, мы пришли в то же самое место и съели по крутому мягкому стейку! Теперь будем считать это тотемным местом нашего канала. Так что тем, кто живет в Нижнем и туристам из других городов, рекомендуем посетить это, не побоюсь этой фразы, на весь мир известное место. А какой там томатный сок с халапеньо…. Ммммм…. Только ради него и стоит идти, не говоря уже про сами стейки.
В этот прекрасный день мы разрешили себе не публиковать ничего, а просто затусить и насладится победой. Но завтра возвращаемся с постами в том же рабочем ритме.
Enjoy your achievements. Stay cool.
Вчера на канале случился юбилей - к нам подписался наш тысячный подписчик и соратник!!! Ура!!! Ура!!! Ураааааа!!!
Это большое достижение для нас. Не только админы виноваты в этом успехе. Все участники коммьюнити поспособствовали достижению этой цифры. Благодаря вашим реакциям, вашим репостам, вашим комментариям в канале создается атмосфера уюта, профессионализма и позитива. Людям это нравится, они подписываются и остаются с нами надолго.
Поэтому хочу сказать Вам всем: Спасибо огромное!!!!
Вы мотивируете нас делать крутой и глубокий контент. Каждая ваша благодарность и приятные слова как мед на сердца админов. Это придает какой-то смысл чтоли нашему труду. Это очень теплое чувство, когда ты понимаешь, что приносишь реальную пользу людям.
Спасибо всем ребятам, кто перешел с других каналов. Спасибо всем ребятам, кто пришел с 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
Иногда появляется необходимость узнать, является ли данное число отрицательным или нет. За примером далеко ходить не будем. В одной из наших задачек про переворачивание десятичных цифр числа один из подходов к решению может быть таким: узнать, является ли число отрицательным, если да, то домножить его на -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
🔥8❤5👍5
std::signbit
В прошлом посте мы уже упоминали std::signbit. Сегодня мы посмотрим на эту сущность по-подробнее.
По сути, это самый говорящий и плюсовый чтоли способ узнать знаковый бит числа, который появился в нашем арсенале с приходом С++11. Причем не только целого, но и числа с плавающей точкой. Хотя на самом деле даже наоборот.
вот такие перегрузки мы имеем для floating-point чисел. А вот такую:
для целых. Последняя перегрузка является дополнительной. Это значит, что в имплементации стандартной библиотеки она не обязана выглядеть прям в точности так. Единственное требование - перегрузки должны быть достаточными, чтобы определить попадание интегрального типа в функцию.
В чем особенность целочисленной перегрузки. В том, что число, которое туда попадает трактуется, как double. Поэтому выражение std::signbit(num) эквивалентно std::signbit(static_cast<double>(num)).
Также эта функция детектирует наличие знакового бита у нулей, бесконечностей и NaN'ов. Да, да. У нуля есть знак. Так что 0.0 и -0.0 - не одно и то же. И если вы внимательные, то заметили даже у NaN есть знак. И std::signbit - один из двух возможных кроссфплатформенных способов узнать знак NaN. Этот факт еще больше мотивирует использовать эту функцию(в ситуациях, где это свойство решает).
Начиная с 23 стандарта функция становится constexpr, что не может не радовать любителей compile-time вычислений.
Для языка С тоже кстати есть похожая сущность. Только там это макрос
И для него гарантируется такое поведение: для положительных чисел возвращаем ноль, а для отрицательных - ненулевое целое число.
Мне кажется, что в повседневной разработке(там где не нужно выжимать все возможные такты и кода) плюсовое решение будет более предпочтительным, по сравнению с аналогами. Говорящее название и поддержка стандрата - наши главные друзья.
Look for signs in life. Stay cool.
#cpp23 #cpp11 #goodoldc
В прошлом посте мы уже упоминали 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
👍13❤7🔥7
Бинарные логические операторы. Short circuit.
Хотел прояснить один момент. В этом посте я сказал, что при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы, в том числе в виде fold expression. Я имел ввиду, что вам придется конкретизировать все метаклассы(ну не прям вам своими ручками, но тем не менее) для того, чтобы начать вычислять выражение. Это правда, без сомнений.
Однако я понял, что некоторые из наших дорогих подписчиков возможно могли сконфузится конфузом, что тут я говорю, что короткой схемы у них нет, а в посте про вычисления по короткой схеме я ставлю в пример эти два оператора. Вроде бы противоречие. Но нет. Тут довольно тонкая грань.
Опять же, без сомнений, && и || что в обычном виде, что в форме fold expression выполняют вычисления по короткой схеме. Они просто обязаны инстанциировать все шаблонные параметры/параметры из пака шаблонных аргументов, чтобы начать выполняться и начать проявлять свои short circuit свойства. То есть
вот этот пример крашнется на компиляции, потому что компилятору нужно инстанцировать type_without_value<T2> и достать из него value, но он не сможет этого сделать, потому что type_without_value не содержит члена value. Но в случае удачной инстанциации этот пример:
успешно соберется и result будет равен false, как и ожидается. В этом случае значение правого операнда учитываться не будет.
Это можно продемонстрировать на следующем примере. Вот такой код успешно соберется и выполнится:
Как видите, при выполнении функции is_even, если в нее попадет нечетное число, то бросится исключение. Исключение, брошенное в compile-time, прерывает компиляцию.
Но в крайнем примере все пройдет хорошо, потому что is_even ни разу не выполнится, как раз из-за короткосхемности оператора &&!
При этом, если раскомментить последнюю строчку, то компиляция зафейлится. Потому что функция все-таки начнет выполняться во время компиляции, но из нее вылетит исключение, которое и прервет эту самую компиляцию.
Вот такие дела. Надеюсь, я не зря волновался и для кого-то получше прояснил ситуацию.
Explain things clearly. Stay cool.
#template #compiler
Хотел прояснить один момент. В этом посте я сказал, что при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы, в том числе в виде 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👍6❤4🤯3
Задачка
Есть классическая задачка про валидность скобок. Сегодня мы попробуем порешать чуть более замысловатую задачку, в которой несколько подводных камней.
Дан код на языке С++. Нужно проверить, что все скобки расположены правильно. Это значит, что каждая открытая скобка должна быть закрыта соответствующей закрывающей скобкой в правильном порядке. И каждая закрывающая скобка должна иметь соответствующую открывающую скобку.
Предположим, что наша гипотетическая функция будет принимать строку со всем кодом, а возвращать true, если все корректно, и false в обратном случае.
Cпециально не описываю условие подробно, потому что мы все здесь плюсовики и понимаем, что в себя включает С++ код. Хотя на самом деле, это чтобы вы немножко помучались и на камни все-таки наступили😈.
Напомню наш регламен по задачкам. Под этим постом обсуждаем все непонятки и пытаемся разобраться в концепции решения с людьми, которые не очень понимают, как решать такую задачу.
Следом выходит пост, куда уже можно выкладывать готовые решения и обсуждать их.
Вечером выйдет пост с объяснениями и решением.
Погнали решать!
P.S. Благодаря Евгению понял, что сам наткнулся на подводный камень и не учел кое-что. Давайте введем гарантию, что в коде отсутствуют директивы препроцессора и макросы, так как это значительно усложняет задачу. А также обойдемся без вложенных комментариев.
Challenge your life. Stay cool.
#задачки
Есть классическая задачка про валидность скобок. Сегодня мы попробуем порешать чуть более замысловатую задачку, в которой несколько подводных камней.
Дан код на языке С++. Нужно проверить, что все скобки расположены правильно. Это значит, что каждая открытая скобка должна быть закрыта соответствующей закрывающей скобкой в правильном порядке. И каждая закрывающая скобка должна иметь соответствующую открывающую скобку.
Предположим, что наша гипотетическая функция будет принимать строку со всем кодом, а возвращать true, если все корректно, и false в обратном случае.
Cпециально не описываю условие подробно, потому что мы все здесь плюсовики и понимаем, что в себя включает С++ код. Хотя на самом деле, это чтобы вы немножко помучались и на камни все-таки наступили😈.
Напомню наш регламен по задачкам. Под этим постом обсуждаем все непонятки и пытаемся разобраться в концепции решения с людьми, которые не очень понимают, как решать такую задачу.
Следом выходит пост, куда уже можно выкладывать готовые решения и обсуждать их.
Вечером выйдет пост с объяснениями и решением.
Погнали решать!
P.S. Благодаря Евгению понял, что сам наткнулся на подводный камень и не учел кое-что. Давайте введем гарантию, что в коде отсутствуют директивы препроцессора и макросы, так как это значительно усложняет задачу. А также обойдемся без вложенных комментариев.
Challenge your life. Stay cool.
#задачки
👍8😁3❤2🔥2