static inline constexpr
Когда-то давно @Igorlamerger попросил нас рассказать про inline, static inline и static inline constexpr. Отчасти эта большая серия постов и была предназначена как ответ на просьбу подписчика. И хотя мы уже столько всего обсудили, что, в целом, вы и так можете сказать, когда и как можно писать inline static constexpr. Но на всякий случай сегодняшний пост будет про это.
Был у нас уже вчера пост про inline constexpr, поэтому нам осталось только добавить к этому всему немного статичности)
Как мы знаем, constexpr для статических методов и полей класса подразумевает inline. Следовательно в этих случаях static обозначает принадлежность свободной функции или глобальной переменной к классу, а не объекту, и внешнюю линковку. constexpr здесь отвечает за возможность использования сущности в вычислениях времени компиляции, а inline обеспечивает эту возможность. Чтобы все единицы трансляции получили определение сущности во время компиляции.
Далее свободные функции и методы класса. Для них constexpr тоже подразумевает inline. Если к методу класса приписать static, то он будет уже не методом, а статической функцией, кейс которой мы обсуждали выше. Если для свободной, по факту уже, inline constexpr функции дописать static, то static кинет на прогиб ваш inline и навяжет свои правила. Эту функцию также можно будет продолжать использовать для compile-time вычислений, но в каждой единице трансляции будет своя копия этой функции. То есть тип линковки изменится с внешней на внутренюю.
Ну и теперь глобальные переменные. С ними ситуация почти такая же, как и со свободными функциями. Только здесь constexpr раскрывается в просто const и дает внутреннее связывание. inline говорит, что нихера подобного, пусть все тебя видят. И дает внешнее связывание. Но приходит static и в честном, бесконтактном бою закидывает невидимыми энергоударами инлайн и побеждает его. Связывание будет внутренним.
Такой вот небольшой пост. Но думаю, что теперь вы мастера спорта по линковке и связанными с ней ключевыми словами. И сможете сами спокойно пояснить за любую их комбинацию и как это будет влиять на сущность. Осталось еще несколько моментов, которые мы не разобрали, поэтому сериал "Линковка в большом городе" продолжается.
Be a master of your specialty. Stay cool.
#cppcore #cpp11 #cpp17 #compiler
Когда-то давно @Igorlamerger попросил нас рассказать про inline, static inline и static inline constexpr. Отчасти эта большая серия постов и была предназначена как ответ на просьбу подписчика. И хотя мы уже столько всего обсудили, что, в целом, вы и так можете сказать, когда и как можно писать inline static constexpr. Но на всякий случай сегодняшний пост будет про это.
Был у нас уже вчера пост про inline constexpr, поэтому нам осталось только добавить к этому всему немного статичности)
Как мы знаем, constexpr для статических методов и полей класса подразумевает inline. Следовательно в этих случаях static обозначает принадлежность свободной функции или глобальной переменной к классу, а не объекту, и внешнюю линковку. constexpr здесь отвечает за возможность использования сущности в вычислениях времени компиляции, а inline обеспечивает эту возможность. Чтобы все единицы трансляции получили определение сущности во время компиляции.
Далее свободные функции и методы класса. Для них constexpr тоже подразумевает inline. Если к методу класса приписать static, то он будет уже не методом, а статической функцией, кейс которой мы обсуждали выше. Если для свободной, по факту уже, inline constexpr функции дописать static, то static кинет на прогиб ваш inline и навяжет свои правила. Эту функцию также можно будет продолжать использовать для compile-time вычислений, но в каждой единице трансляции будет своя копия этой функции. То есть тип линковки изменится с внешней на внутренюю.
Ну и теперь глобальные переменные. С ними ситуация почти такая же, как и со свободными функциями. Только здесь constexpr раскрывается в просто const и дает внутреннее связывание. inline говорит, что нихера подобного, пусть все тебя видят. И дает внешнее связывание. Но приходит static и в честном, бесконтактном бою закидывает невидимыми энергоударами инлайн и побеждает его. Связывание будет внутренним.
Такой вот небольшой пост. Но думаю, что теперь вы мастера спорта по линковке и связанными с ней ключевыми словами. И сможете сами спокойно пояснить за любую их комбинацию и как это будет влиять на сущность. Осталось еще несколько моментов, которые мы не разобрали, поэтому сериал "Линковка в большом городе" продолжается.
Be a master of your specialty. Stay cool.
#cppcore #cpp11 #cpp17 #compiler
❤15👍8🔥7
Ретроспектива с подписчиками #1
Буквально вчера был опубликован пост про ретроспективу. Естественно, это был прогрев 😃 Мы бы хотели провести своё собственное открытое ретро с подписчиками, большая надежда на ваш отклик! 😌
Напомню цель: необходимо закрепить хорошее, выявить и исправить плохое. Желательно подкрепить замечания какими-то фактами или наблюдениями, чтобы нам была понятна мотивация. Это может быть готовое предложение, наблюдение или вопрос.
Это может касаться абсолютно всего, что связано с каналом. Например:
- Управление: время публикаций, частота публикаций;
- Наполненность: план публикаций, сложность материала, глубина погружения в тему, душность, объём публикаций;
- Общение: время отклика, качество ответов, живость, вежливость;
- Поиск материала: сложность поиска, группировка постов, подготовка гайдов (т.е. методичек, сборников статей по теме);
Голосовать за важность чьей либо идеи предлагаем просто пальцами: 👍или👎. Так мы поймем, что действительно важно для нас. Данный тред будет актуален до следующей ретроспективы. Погнали! 👨💻
#retro
Буквально вчера был опубликован пост про ретроспективу. Естественно, это был прогрев 😃 Мы бы хотели провести своё собственное открытое ретро с подписчиками, большая надежда на ваш отклик! 😌
Напомню цель: необходимо закрепить хорошее, выявить и исправить плохое. Желательно подкрепить замечания какими-то фактами или наблюдениями, чтобы нам была понятна мотивация. Это может быть готовое предложение, наблюдение или вопрос.
Это может касаться абсолютно всего, что связано с каналом. Например:
- Управление: время публикаций, частота публикаций;
- Наполненность: план публикаций, сложность материала, глубина погружения в тему, душность, объём публикаций;
- Общение: время отклика, качество ответов, живость, вежливость;
- Поиск материала: сложность поиска, группировка постов, подготовка гайдов (т.е. методичек, сборников статей по теме);
Голосовать за важность чьей либо идеи предлагаем просто пальцами: 👍или👎. Так мы поймем, что действительно важно для нас. Данный тред будет актуален до следующей ретроспективы. Погнали! 👨💻
#retro
👍10❤7🔥6
Почему нельзя объявлять нестатические поля класса constexpr?
В прошлом посте мы задели особенности линковки constexpr сущностей. Однако я не упомянул про нестатические поля класса. И не зря. Потому что нельзя нестатические поля объявлять constexpr. Но почему?
Давайте немного подумаем, что значит constexpr. В глобальном смысле подразумевается, что мы что-то можем вычислить во время компиляции. Для это придуманы константы времени компиляции(constexpr variables) и функции, которые способны делать вычисления в compile-time.
Причем на константы времени компиляции накладываются жесткие ограничения. Их инициализатор должен быть известен во время компилятору на этом самом этапе компиляции(или конструктор класса помечен constexpr). Только тогда можно создать такой объект.
И теперь взглянем на поле класса. Когда оно инициализируется? Правильно, в конструкторе. То есть мы должны начать создавать объект, чтобы инициализировать поле. А создаются объекты во время исполнения. Получается, что наше constexpr поле принадлежит сущности, которой вообще не существует до момента создания экземпляра класса. И как оно тогда может быть константой времени компиляции? Ведь если constexpr variable и создается, то создается только в compile time. А это просто невозможно в таком случае.
На самом деле поле класса может быть constexpr. Но неявно. Когда объект, в котором содержится это поле, сам является constexpr.
Посмотрите на пример на картинке. Там определяется класс с constexpr конструкторами и затем создается constexpr экземпляр этого класса. Чтобы проверить, действительно ли поле a является константой времени компиляции, можно попробовать вызвать шаблонную функцию с интовым шаблонным параметром. Так как шаблонный параметр - часть типа, то его значение должно быть известно на этапе компиляции. Поэтому, если все получится, то это докажет, что a - константа времени компиляции. И действительно, все работает. constexpr объект делает его поля constexpr.
Если мы попробуем разрешить помечать поля класса constexpr, даже одно единственное поле, значит нам нужно гарантировать, чтобы все поля становились constexpr(если это будет делаться неявно, то это явно кринж), и все объекты данного класса могли создаваться только в compile-time. Зачем это нужно? Да вроде как не за чем. В этом очень мало смысла. Можно выделить constexpr интерфейс и жить себе прекрасно в compile-time. И пользоваться полноценным интерфейсом во время выполнения. Слишком много неуверенности в полезности этой фичи. Поэтому и не разрешают так делать.
Вот такие интересности скрываются в таких, казалось бы, привычных темах. Вряд ли эта информация вам когда-нибудь понадобится, но понимание таких вещей выводит ваше осознание происходящих процессов на иной уровень.
Reach new levels. Stay cool.
#cpp11 #compiler
В прошлом посте мы задели особенности линковки constexpr сущностей. Однако я не упомянул про нестатические поля класса. И не зря. Потому что нельзя нестатические поля объявлять constexpr. Но почему?
Давайте немного подумаем, что значит constexpr. В глобальном смысле подразумевается, что мы что-то можем вычислить во время компиляции. Для это придуманы константы времени компиляции(constexpr variables) и функции, которые способны делать вычисления в compile-time.
Причем на константы времени компиляции накладываются жесткие ограничения. Их инициализатор должен быть известен во время компилятору на этом самом этапе компиляции(или конструктор класса помечен constexpr). Только тогда можно создать такой объект.
И теперь взглянем на поле класса. Когда оно инициализируется? Правильно, в конструкторе. То есть мы должны начать создавать объект, чтобы инициализировать поле. А создаются объекты во время исполнения. Получается, что наше constexpr поле принадлежит сущности, которой вообще не существует до момента создания экземпляра класса. И как оно тогда может быть константой времени компиляции? Ведь если constexpr variable и создается, то создается только в compile time. А это просто невозможно в таком случае.
На самом деле поле класса может быть constexpr. Но неявно. Когда объект, в котором содержится это поле, сам является constexpr.
Посмотрите на пример на картинке. Там определяется класс с constexpr конструкторами и затем создается constexpr экземпляр этого класса. Чтобы проверить, действительно ли поле a является константой времени компиляции, можно попробовать вызвать шаблонную функцию с интовым шаблонным параметром. Так как шаблонный параметр - часть типа, то его значение должно быть известно на этапе компиляции. Поэтому, если все получится, то это докажет, что a - константа времени компиляции. И действительно, все работает. constexpr объект делает его поля constexpr.
Если мы попробуем разрешить помечать поля класса constexpr, даже одно единственное поле, значит нам нужно гарантировать, чтобы все поля становились constexpr(если это будет делаться неявно, то это явно кринж), и все объекты данного класса могли создаваться только в compile-time. Зачем это нужно? Да вроде как не за чем. В этом очень мало смысла. Можно выделить constexpr интерфейс и жить себе прекрасно в compile-time. И пользоваться полноценным интерфейсом во время выполнения. Слишком много неуверенности в полезности этой фичи. Поэтому и не разрешают так делать.
Вот такие интересности скрываются в таких, казалось бы, привычных темах. Вряд ли эта информация вам когда-нибудь понадобится, но понимание таких вещей выводит ваше осознание происходящих процессов на иной уровень.
Reach new levels. Stay cool.
#cpp11 #compiler
🔥12👍8❤3
Inline под капотом
Мы уже знаем, что inline позволяет находится определению одних и тех же сущностей в разных единицах трансляции. А потом на этапе линковки, компоновщик объединяет все эти определения в одно. Но как конкретно он это делает? Как устроен этот механизм в деталях? Сегодня будем в этом разбираться.
Вернемся к примерам из вот этого поста(продублирую его в прикрепленной картинке к этому посту), но только уберем constexpr, чтобы компилятор не просто вставлял значение переменной в место ее использования, а прям создал эту переменную в секции .data, чтобы ее можно было видеть. Ну и пометим их static, чтобы на нас линкер не ругался. Да, это глобальная переменная, так нельзя делать, и ля ля ля. Но пример учебный, просто для понимания.
Как будет выглядеть переменная light_speed в единице трансляции, соответствующей файлу first.cpp?
Рассмотрим по порядку, что здесь происходит. Начинается сегмент данных, которые выровнены на 4 байта. Говорим, что наш символ _ZN9constantsL11light_speedE - это объект с размером 4 байта. И определяем этом символ, говорим, что он типа long со значением 299792458.
И в результирующем бинарнике у нас будет 2 экземпляра семантически одной переменной в разных единицах трансляции.
0000000000004010 d _ZN9constantsL11light_speedE
0000000000004020 d _ZN9constantsL11light_speedE
Что конечно может хорошенько подпортить жизнь трудноотловимыми багами. Не нужно объявлять в хэдерах изменяющиеся переменные как static. Но повторю, что это только учебный пример, чтобы показать интересности линковки inline. Много циферок - виртуальный адрес символа, а d значит, что символ инициализирован.
Теперь, что будет, если мы static заменим на inline.
Здесь определяется слабый символ _ZN9constants11light_speedE. Слабый символ может быть переписан другим определением. Дальше идет секция данных и очень много страшных букв, но нам важно только последнее слово "comdat". Оно значит: "Здарова братишка, компоновщик! Не в службу, а в дружбу, не конкатенируй определения для символа _ZN9constants11light_speedE, а просто выбери из них всех одно и вставь в финальный бинарь. Мое увожение!". Это и есть тот маркер, по которому линкер определяет inline сущности. Ну и это все идет с компании с типом @gnu_unique_object, который должен быть уникальным во всех программе и это предотвращает дупликацию кода.
Тогда в бинарнике будет только одна запись на эту переменную.
0000000000004018 u _ZN9constants11light_speedE
u здесь значит, что символ глобальный и уникальный для всей программы.
Кстати, можно заметить пару деталей. В первом случае символ имел имя _ZN9constantsL11light_speedE, а во втором случае _ZN9constants11light_speedE.
Их объединяет то, что в оба имя включено название их нэймспейса. В С++ каждый символ имеет свое замангленное имя, которое может включать много всяких интересностей. Такое имя сильно облегчает компилятору и линкеру работу по разрешению вызовов и сопоставлению символов. Так что имя неймспейса включено в это расширенное имя объекта.
Но можно заметить и разницу. После имени неймспейса в случае статических переменных мы имеем заглавную L. Так компилятор помечает символ с внутренним связыванием.
Всё равно все рано или поздно начинают смотреть в ассемблер, поэтому, если вы еще не мастак в этом ремесле, то важно постепенно и безболезненно впитывать особенности того, как там все работает, и потихоньку приобщатся к коду.
А для остальных этот пост, надеюсь, подарил пару интересных и новых моментов.
Stay hardcore. Stay cool.
#cppcore #hardcore #cpp17 #compiler
Мы уже знаем, что inline позволяет находится определению одних и тех же сущностей в разных единицах трансляции. А потом на этапе линковки, компоновщик объединяет все эти определения в одно. Но как конкретно он это делает? Как устроен этот механизм в деталях? Сегодня будем в этом разбираться.
Вернемся к примерам из вот этого поста(продублирую его в прикрепленной картинке к этому посту), но только уберем constexpr, чтобы компилятор не просто вставлял значение переменной в место ее использования, а прям создал эту переменную в секции .data, чтобы ее можно было видеть. Ну и пометим их static, чтобы на нас линкер не ругался. Да, это глобальная переменная, так нельзя делать, и ля ля ля. Но пример учебный, просто для понимания.
Как будет выглядеть переменная light_speed в единице трансляции, соответствующей файлу first.cpp?
.data
.align 4
.type _ZN9constantsL11light_speedE, @object
.size _ZN9constantsL11light_speedE, 4
_ZN9constantsL11light_speedE:
.long 299792458
Рассмотрим по порядку, что здесь происходит. Начинается сегмент данных, которые выровнены на 4 байта. Говорим, что наш символ _ZN9constantsL11light_speedE - это объект с размером 4 байта. И определяем этом символ, говорим, что он типа long со значением 299792458.
И в результирующем бинарнике у нас будет 2 экземпляра семантически одной переменной в разных единицах трансляции.
0000000000004010 d _ZN9constantsL11light_speedE
0000000000004020 d _ZN9constantsL11light_speedE
Что конечно может хорошенько подпортить жизнь трудноотловимыми багами. Не нужно объявлять в хэдерах изменяющиеся переменные как static. Но повторю, что это только учебный пример, чтобы показать интересности линковки inline. Много циферок - виртуальный адрес символа, а d значит, что символ инициализирован.
Теперь, что будет, если мы static заменим на inline.
.weak _ZN9constants11light_speedE
.section .data._ZN9constants11light_speedE,"awG",@progbits,_ZN9constants11light_speedE,comdat
.align 4
.type _ZN9constants11light_speedE, @gnu_unique_object
.size _ZN9constants11light_speedE, 4
_ZN9constants11light_speedE:
.long 299792458
Здесь определяется слабый символ _ZN9constants11light_speedE. Слабый символ может быть переписан другим определением. Дальше идет секция данных и очень много страшных букв, но нам важно только последнее слово "comdat". Оно значит: "Здарова братишка, компоновщик! Не в службу, а в дружбу, не конкатенируй определения для символа _ZN9constants11light_speedE, а просто выбери из них всех одно и вставь в финальный бинарь. Мое увожение!". Это и есть тот маркер, по которому линкер определяет inline сущности. Ну и это все идет с компании с типом @gnu_unique_object, который должен быть уникальным во всех программе и это предотвращает дупликацию кода.
Тогда в бинарнике будет только одна запись на эту переменную.
0000000000004018 u _ZN9constants11light_speedE
u здесь значит, что символ глобальный и уникальный для всей программы.
Кстати, можно заметить пару деталей. В первом случае символ имел имя _ZN9constantsL11light_speedE, а во втором случае _ZN9constants11light_speedE.
Их объединяет то, что в оба имя включено название их нэймспейса. В С++ каждый символ имеет свое замангленное имя, которое может включать много всяких интересностей. Такое имя сильно облегчает компилятору и линкеру работу по разрешению вызовов и сопоставлению символов. Так что имя неймспейса включено в это расширенное имя объекта.
Но можно заметить и разницу. После имени неймспейса в случае статических переменных мы имеем заглавную L. Так компилятор помечает символ с внутренним связыванием.
Всё равно все рано или поздно начинают смотреть в ассемблер, поэтому, если вы еще не мастак в этом ремесле, то важно постепенно и безболезненно впитывать особенности того, как там все работает, и потихоньку приобщатся к коду.
А для остальных этот пост, надеюсь, подарил пару интересных и новых моментов.
Stay hardcore. Stay cool.
#cppcore #hardcore #cpp17 #compiler
🔥14👍11❤4😱1
Vector vs List
Знание алгоритмов и структур данных - критично для нашей профессии. Ведь в принципе все, что мы делаем - это берем какие-то явления реального мира, создаем их представления в программе и управляем ими. От нашего понимания эффективного представления данных в памяти компьютера и управления ими зависит количество ресурсов, которое требуется на предоставление какой-то услуги, а значит и доход заказчика. Чем лучше мы знаем эти вещи, тем дешевле предоставлять услугу и тем больше мы ценны как специалисты. И наши доходы растут. Но computer science намного обширнее, чем абстрактные вещи типа теории алгоритмов или архитектуры ПО. Это еще и знание и понимание работы конкретного железа, на котором наш софт выполняется. Что важнее - непонятно. Но только в синергии можно получить топовые результаты.
Согласно теории, проход по массиву и двунаправленному списку происходит за одинаковое алгоритмическое время. O(n). То есть время прохода линейно зависит от количества элементов в структуре. И без применения знаний о принципах работы железа, можно подумать, что они взаимозаменяемы. Просто в одном случае элементы лежат рядом, а в другом - связаны ссылками. А на практике получается, что не совсем линейно и совсем не взаимозаменяемы. Расскажу подробнее о последнем.
Дело в существовании кэша у процессора. Когда мы запрашиваем доступ к одной ячейке памяти, процессор верно предполагает, что нам скорее всего будут нужны и соседние ячейки тоже. Поэтому он копирует целый интервал в памяти, называемый кэш-строкой, в свой промежуточный буфер. А доступ к этому буферу происходит намного быстрее, чем к оперативной памяти. И процессору не нужно ходить за этими рядом лежащими данными в ОЗУ, он их просто берет из кэша.
Элементы же списка хранятся в разных участках памяти и связаны между собой ссылками. В общем виде нет никакой гарантии, что они будут лежать рядом. Если конечно вы не используете кастомный аллокатор. Поэтому за каждым элементом листа процессору приходится ходит в ОЗУ.
Вот и получается, что обработка элементов списка будет происходит медленнее, чем элементов массива.
Написал простенькую программку, которая проверит разницу во времени прохода.
Получилось, что для 100кк чисел в массиве и в листе, время отличается от 1.5 до 2 раз на разных машинах. Нихрена себе такая разница. И это без оптимизаций!
С ними разница вообще 6-8 раз. Это кстати еще один повод для того, чтобы понимать, как именно мы работаем на уровне железа и какие структуры данных хорошо ложатся на разные оптимизации.
Конечно, в реальных приложениях обработка - обычно более дорогая операция, чем просто инкремент и в процентном соотношении затраты на доступ к элементам будут другие. Но для того и пишутся такие тесты, чтобы максимально подсветить проблему.
Stay based. Stay cool.
#STL #algorithms #cppcore
Знание алгоритмов и структур данных - критично для нашей профессии. Ведь в принципе все, что мы делаем - это берем какие-то явления реального мира, создаем их представления в программе и управляем ими. От нашего понимания эффективного представления данных в памяти компьютера и управления ими зависит количество ресурсов, которое требуется на предоставление какой-то услуги, а значит и доход заказчика. Чем лучше мы знаем эти вещи, тем дешевле предоставлять услугу и тем больше мы ценны как специалисты. И наши доходы растут. Но computer science намного обширнее, чем абстрактные вещи типа теории алгоритмов или архитектуры ПО. Это еще и знание и понимание работы конкретного железа, на котором наш софт выполняется. Что важнее - непонятно. Но только в синергии можно получить топовые результаты.
Согласно теории, проход по массиву и двунаправленному списку происходит за одинаковое алгоритмическое время. O(n). То есть время прохода линейно зависит от количества элементов в структуре. И без применения знаний о принципах работы железа, можно подумать, что они взаимозаменяемы. Просто в одном случае элементы лежат рядом, а в другом - связаны ссылками. А на практике получается, что не совсем линейно и совсем не взаимозаменяемы. Расскажу подробнее о последнем.
Дело в существовании кэша у процессора. Когда мы запрашиваем доступ к одной ячейке памяти, процессор верно предполагает, что нам скорее всего будут нужны и соседние ячейки тоже. Поэтому он копирует целый интервал в памяти, называемый кэш-строкой, в свой промежуточный буфер. А доступ к этому буферу происходит намного быстрее, чем к оперативной памяти. И процессору не нужно ходить за этими рядом лежащими данными в ОЗУ, он их просто берет из кэша.
Элементы же списка хранятся в разных участках памяти и связаны между собой ссылками. В общем виде нет никакой гарантии, что они будут лежать рядом. Если конечно вы не используете кастомный аллокатор. Поэтому за каждым элементом листа процессору приходится ходит в ОЗУ.
Вот и получается, что обработка элементов списка будет происходит медленнее, чем элементов массива.
Написал простенькую программку, которая проверит разницу во времени прохода.
Получилось, что для 100кк чисел в массиве и в листе, время отличается от 1.5 до 2 раз на разных машинах. Нихрена себе такая разница. И это без оптимизаций!
С ними разница вообще 6-8 раз. Это кстати еще один повод для того, чтобы понимать, как именно мы работаем на уровне железа и какие структуры данных хорошо ложатся на разные оптимизации.
Конечно, в реальных приложениях обработка - обычно более дорогая операция, чем просто инкремент и в процентном соотношении затраты на доступ к элементам будут другие. Но для того и пишутся такие тесты, чтобы максимально подсветить проблему.
Stay based. Stay cool.
#STL #algorithms #cppcore
🔥31👍3❤2👎1
Cамая популярная задачка с собеседований
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно стрессовом состоянии находятся. Им бы вспомнить, чем вектор от листа отличается, не говоря уже о том, чтобы сказать, как засунуть слона в холодильник.
На моем пути у меня 2 раза спрашивали логические задачки и оба раза спрашивали одну и ту же. Поэтому название поста в стиле ошибки выжившего. Но, думаю, что она реально такая популярная, поэтому будет полезно подготовиться к ней.
Формулировка.
Вы находитесь в кольцевом поезде. То есть как будто бы 2 конца одного поезда соединили и можно бесконечно теперь в нем гулять. Количество вагонов вам неизвестно. В каждом вагоне есть всего одна лампочка. Она имеет 2 состояния: вкл и выкл. По дефолту они в рандомном порядке включены во всех вагонах, то есть вы заранее не знаете, в каком порядке в вагонах зажжены лампочки. И у каждой лампочки есть выключатель, который исправно работает.
Вас телепортируют в какой-то из вагонов поезда. Ваша задача - найти длину поезда аля количество вагонов. Естественно, вы можете переходить из вагона в вагон.
Такая вот незамысловатая задачка. Рассуждая логически, можно довольно быстро прийти к ответу. Главное - правильно себе вопросы ставить.
Знающих прошу не спойлерить в комментах и не подсказывать. Обсуждения как всегда поощряются. Если вы допетрили до решения, то лучше его при себе оставить.
Решение сброшу завтра в комментах к отдельному посту, посвященному ответу.
Всем удачи!
Train your logic. Stay cool.
#fun
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно стрессовом состоянии находятся. Им бы вспомнить, чем вектор от листа отличается, не говоря уже о том, чтобы сказать, как засунуть слона в холодильник.
На моем пути у меня 2 раза спрашивали логические задачки и оба раза спрашивали одну и ту же. Поэтому название поста в стиле ошибки выжившего. Но, думаю, что она реально такая популярная, поэтому будет полезно подготовиться к ней.
Формулировка.
Вы находитесь в кольцевом поезде. То есть как будто бы 2 конца одного поезда соединили и можно бесконечно теперь в нем гулять. Количество вагонов вам неизвестно. В каждом вагоне есть всего одна лампочка. Она имеет 2 состояния: вкл и выкл. По дефолту они в рандомном порядке включены во всех вагонах, то есть вы заранее не знаете, в каком порядке в вагонах зажжены лампочки. И у каждой лампочки есть выключатель, который исправно работает.
Вас телепортируют в какой-то из вагонов поезда. Ваша задача - найти длину поезда аля количество вагонов. Естественно, вы можете переходить из вагона в вагон.
Такая вот незамысловатая задачка. Рассуждая логически, можно довольно быстро прийти к ответу. Главное - правильно себе вопросы ставить.
Знающих прошу не спойлерить в комментах и не подсказывать. Обсуждения как всегда поощряются. Если вы допетрили до решения, то лучше его при себе оставить.
Решение сброшу завтра в комментах к отдельному посту, посвященному ответу.
Всем удачи!
Train your logic. Stay cool.
#fun
🔥15👍6❤4
Задача про закольцованный поезд
Решение будет в комментариях под этим постом, чтобы в будущем при пролистывании ленты люди не натыкались глазами на него сразу.
Решение будет в комментариях под этим постом, чтобы в будущем при пролистывании ленты люди не натыкались глазами на него сразу.
🔥16👍3🆒2
Сравнение решений задачи
Меня всегда восхищает, как люди доходят до разных решений одной задачи. Это расширяет мои рамки восприятия мира и я могу перенять эти паттерны мышления. Всем спасибо за ваши варианты решений. Даже читаерные. Скорее даже особенно читерные. Потому что редко у меня получается думать outside the box.
Но сегодня разберем кое-что другое. Увидел в комментах, что многие люди приходили к немного иному решению. Не по аналогии с маятником, а скорее со спидометром. От изначального вагона с включенным светом идем вправо и выключаем лампочку. Потом возвращаемся и проверяем лампочку в самом первом вагоне. Далее снова идем вправо, но уже на один вагон дальше, и выключаем там свет. Возвращаемся обратно и делаем проверку света в изначальном вагоне. Условие остановки - в изначальном вагоне погасла лампочка. И длина поезда - последнее количество вагонов на пути обратно.
Проще ли этот вариант? Думаю, что да. В голове нужно всего один счетчик хранить. А для маятника уже 2 нужно. Как говорят физики: "все, что больше одного - это уже много". После прохождения энного вагона я бы точно что-то перепутал.
Но вот вопрос: а для какого решения требуется пройти меньше вагонов? С первого взгляда, какая разница. И там и там ходим туда-сюда. Но если все хорошенько подсчитать, то разница есть и значительная. Я вот и посчитал. Результаты на картинке под постом.
Не преследовал целей принизить чье-то решение. Просто самому было интересно проверить. Еще раз: я восхищаюсь всеми вами, кто придумал решение. Потому что в свое время не смог этого сделать на собесе. Вы крутые)
Stay cool.
Меня всегда восхищает, как люди доходят до разных решений одной задачи. Это расширяет мои рамки восприятия мира и я могу перенять эти паттерны мышления. Всем спасибо за ваши варианты решений. Даже читаерные. Скорее даже особенно читерные. Потому что редко у меня получается думать outside the box.
Но сегодня разберем кое-что другое. Увидел в комментах, что многие люди приходили к немного иному решению. Не по аналогии с маятником, а скорее со спидометром. От изначального вагона с включенным светом идем вправо и выключаем лампочку. Потом возвращаемся и проверяем лампочку в самом первом вагоне. Далее снова идем вправо, но уже на один вагон дальше, и выключаем там свет. Возвращаемся обратно и делаем проверку света в изначальном вагоне. Условие остановки - в изначальном вагоне погасла лампочка. И длина поезда - последнее количество вагонов на пути обратно.
Проще ли этот вариант? Думаю, что да. В голове нужно всего один счетчик хранить. А для маятника уже 2 нужно. Как говорят физики: "все, что больше одного - это уже много". После прохождения энного вагона я бы точно что-то перепутал.
Но вот вопрос: а для какого решения требуется пройти меньше вагонов? С первого взгляда, какая разница. И там и там ходим туда-сюда. Но если все хорошенько подсчитать, то разница есть и значительная. Я вот и посчитал. Результаты на картинке под постом.
Не преследовал целей принизить чье-то решение. Просто самому было интересно проверить. Еще раз: я восхищаюсь всеми вами, кто придумал решение. Потому что в свое время не смог этого сделать на собесе. Вы крутые)
Stay cool.
👍10🔥10❤9
Мини-гайд по себесам.pdf
190.2 KB
Мини-гайд по собеседованиям
Доброго дня, дорогие подписчики. Сегодня у нас радостный день - на канале выходит первый гайд. Не так давно мы в комментах с ребятами осуждали этот вопрос, что нужно сделать чеклист по вопросам с собесов. Собственно, сделал. Скачивайте, читайте, делитесь - все бесплатно в свободном виде. Настолько базовые вещи нужно распространять в массы, чтобы повышать шансы на прохождение собесов.
В общем, особо не знаю, что сказать. Все в документе.
Share knowledge. Stay cool.
#guide
Доброго дня, дорогие подписчики. Сегодня у нас радостный день - на канале выходит первый гайд. Не так давно мы в комментах с ребятами осуждали этот вопрос, что нужно сделать чеклист по вопросам с собесов. Собственно, сделал. Скачивайте, читайте, делитесь - все бесплатно в свободном виде. Настолько базовые вещи нужно распространять в массы, чтобы повышать шансы на прохождение собесов.
В общем, особо не знаю, что сказать. Все в документе.
Share knowledge. Stay cool.
#guide
👍45🔥16❤8
Что значит, что фича устрела или удалена
Новые стандарты языка приносят нам кучу разных новых способов решать наши повседневные задачи. Но не только это. Комитет стандартизации думает не только о новом, но и заботится о прошлом. Как минимум примером является обратная совместимость языка при переходе на новый стандарт. Но это не совсем правда. Бывают случаи, когда ломают обратную совместимость в некоторых моментах. Через удаление определенных фичей. Зачем это делают?
Плюсы - довольно старый язык, который пытается сохранить, до определенной степени, совместимость в еще более старым языком - С. Такая большая история говорит о том, что в языке накапливается определенное количество "багов", то есть функциональности, которая уже устарела и/или вреди пользователю. На самом деле такие "баги" выделяют очень строго, когда есть реальная опасность в их использовании. Или все члены комитета согласятся, что от нее нет толку. А комитет состоит из сотен людей, которые непрерывно дебатируют по поводу будущего языка. И если уж они наконец договорились что-то удалить, то это должно сильно встревожить разрабов и воодушевить их на переход на более безопасные альтернативы, которые уже есть в языке.
Такой процесс происходил например с std::autoptr, std::randomshuffle, ключевым словом register, триграфами и прочим.
Их всех объединяет, что для них есть более предпочтительные альтернативы или в современных реалиях они полностью утратили значимость и просто вносят сложность в язык.
Теперь разберем процесс удаления сущности из стандарта.
Для начала она объявляется устаревшей или deprecated. При попытке компиляции с новым стандартом всплывет ворнинг. А в любой нормальной компании ворнинг роняет через бедро сборку проекта. Об этом подробнее здесь рассказывали. И люди просто вынуждены провести небольшой рефакторинг и заменить устаревшую функциональность, чтобы перейти на новый стандарт. Таким образом народ минимально преобразует код, но зато теперь есть возможность использовать фишки нового стандарта, которые могут быть вполне полезными. Ну или забить и не переходить на него. Так тоже иногда делают. Но я бы не хотел работать в такой компании)
После объявления устаревшей стандарт еще какое-то время поддерживает фичу. Но через 3-5-10 лет ее удалят. И больше ее упоминания в стандарте не будет.
Fix your flaws. Stay cool.
#commercial #cppcore
Новые стандарты языка приносят нам кучу разных новых способов решать наши повседневные задачи. Но не только это. Комитет стандартизации думает не только о новом, но и заботится о прошлом. Как минимум примером является обратная совместимость языка при переходе на новый стандарт. Но это не совсем правда. Бывают случаи, когда ломают обратную совместимость в некоторых моментах. Через удаление определенных фичей. Зачем это делают?
Плюсы - довольно старый язык, который пытается сохранить, до определенной степени, совместимость в еще более старым языком - С. Такая большая история говорит о том, что в языке накапливается определенное количество "багов", то есть функциональности, которая уже устарела и/или вреди пользователю. На самом деле такие "баги" выделяют очень строго, когда есть реальная опасность в их использовании. Или все члены комитета согласятся, что от нее нет толку. А комитет состоит из сотен людей, которые непрерывно дебатируют по поводу будущего языка. И если уж они наконец договорились что-то удалить, то это должно сильно встревожить разрабов и воодушевить их на переход на более безопасные альтернативы, которые уже есть в языке.
Такой процесс происходил например с std::autoptr, std::randomshuffle, ключевым словом register, триграфами и прочим.
Их всех объединяет, что для них есть более предпочтительные альтернативы или в современных реалиях они полностью утратили значимость и просто вносят сложность в язык.
Теперь разберем процесс удаления сущности из стандарта.
Для начала она объявляется устаревшей или deprecated. При попытке компиляции с новым стандартом всплывет ворнинг. А в любой нормальной компании ворнинг роняет через бедро сборку проекта. Об этом подробнее здесь рассказывали. И люди просто вынуждены провести небольшой рефакторинг и заменить устаревшую функциональность, чтобы перейти на новый стандарт. Таким образом народ минимально преобразует код, но зато теперь есть возможность использовать фишки нового стандарта, которые могут быть вполне полезными. Ну или забить и не переходить на него. Так тоже иногда делают. Но я бы не хотел работать в такой компании)
После объявления устаревшей стандарт еще какое-то время поддерживает фичу. Но через 3-5-10 лет ее удалят. И больше ее упоминания в стандарте не будет.
Fix your flaws. Stay cool.
#commercial #cppcore
🔥12❤3👍3
Снова сравниваем решения задачи с поездами
Кто забыл или не в теме, вот эта задача. В прошлом мы уже сравнивали два решения: условный маятник и спидометр. Лучше освежить в памяти контекст, чтобы понятнее был предмет разговора. Однако наша подписчица @tigrisVD сделала несколько улучшений алгоритма спидометра. Вот ее сообщение с пояснениями и кодом.
Первое - базовое улучшение. Вместо того, чтобы возвращаться назад на каждом шаге(что очень расточительно), мы возвращаемся назад, только, если встретили единичку и поменяли ее на ноль. Только в этом случае мы могли поменять самую первую лампочку.
Это уже улучшение уполовинило количество шагов. И сравняло его с маятником.
Но она пошла дальше и придумала следующую оптимизацию: пусть мы встретили единичку, когда шли вправо. Тогда разрешим себе поменять ее на нолик и идти дальше на еще столько же шагов, сколько мы сделали до этой единички. Но если мы встретим еще одну единичку, то возвращаемся обратно и чекаем нулевую ячейку. Таким образом мы сэкономим себе проход до первой ячейки и обратно. А если мы за этот удвоенный период не нашли ее одну единичку, то мы просто обязаны обратно вернуться, чтобы проверить была ли первая новая единичка на нашем пути - тем самым началом.
Там есть некоторые проблемы с тем, что оптимизированные алгоритмы спидометра зависят от входных данных. И в худшем случае, когда в поезде включены все лампочки, последний оптимизированный алгоритм работает лишь в 2 раза лучше базового спидометра. В лучшем случае, когда все лампочки выключены - сложность алгоритмов линейно зависит от количества вагонов, а не квадратично, как у базового спидометра. Базовое улучшение - нужно пройти всего х2 от количества вагонов. А оптимизированный вариант- х4(я там одну ошибочку нашел в коде при подсчете пройденных вагонов, поэтому не в 3, а в 4). Но в рандомных случаях оптимизированный вариант примерно в 2 раза быстрее базового улучшенного и в 4 раза быстрее оригинального спидометра.
Позапускать ее код и посмотреть на цифры можно тут
Спасибо вам, @tigrisVD, за такие интересные решения!
Но ваши улучшения натолкнули и меня на размышления и улучшения, о которых я расскажу завтра.
Impove yourself. Stay cool.
#fun #optimization
Кто забыл или не в теме, вот эта задача. В прошлом мы уже сравнивали два решения: условный маятник и спидометр. Лучше освежить в памяти контекст, чтобы понятнее был предмет разговора. Однако наша подписчица @tigrisVD сделала несколько улучшений алгоритма спидометра. Вот ее сообщение с пояснениями и кодом.
Первое - базовое улучшение. Вместо того, чтобы возвращаться назад на каждом шаге(что очень расточительно), мы возвращаемся назад, только, если встретили единичку и поменяли ее на ноль. Только в этом случае мы могли поменять самую первую лампочку.
Это уже улучшение уполовинило количество шагов. И сравняло его с маятником.
Но она пошла дальше и придумала следующую оптимизацию: пусть мы встретили единичку, когда шли вправо. Тогда разрешим себе поменять ее на нолик и идти дальше на еще столько же шагов, сколько мы сделали до этой единички. Но если мы встретим еще одну единичку, то возвращаемся обратно и чекаем нулевую ячейку. Таким образом мы сэкономим себе проход до первой ячейки и обратно. А если мы за этот удвоенный период не нашли ее одну единичку, то мы просто обязаны обратно вернуться, чтобы проверить была ли первая новая единичка на нашем пути - тем самым началом.
Там есть некоторые проблемы с тем, что оптимизированные алгоритмы спидометра зависят от входных данных. И в худшем случае, когда в поезде включены все лампочки, последний оптимизированный алгоритм работает лишь в 2 раза лучше базового спидометра. В лучшем случае, когда все лампочки выключены - сложность алгоритмов линейно зависит от количества вагонов, а не квадратично, как у базового спидометра. Базовое улучшение - нужно пройти всего х2 от количества вагонов. А оптимизированный вариант- х4(я там одну ошибочку нашел в коде при подсчете пройденных вагонов, поэтому не в 3, а в 4). Но в рандомных случаях оптимизированный вариант примерно в 2 раза быстрее базового улучшенного и в 4 раза быстрее оригинального спидометра.
Позапускать ее код и посмотреть на цифры можно тут
Спасибо вам, @tigrisVD, за такие интересные решения!
Но ваши улучшения натолкнули и меня на размышления и улучшения, о которых я расскажу завтра.
Impove yourself. Stay cool.
#fun #optimization
Telegram
Грокаем C++
Cамая популярная задачка с собеседований
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно…
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно…
🔥10👍5❤3
Линейное решение задачи с поездами
По поводу моих улучшений. Немного поразмыслив, первая моя мысль - что с маятником тоже можно сделать маленькое улучшение такое же, как и базовое улучшение спидометра. Мы не каждый раз поворачиваем и идем в обратную сторону, а только тогда, когда меняем состояние встретившейся лампочки.
Во всех особых случаях, когда все лампочки либо включены, либо выключены, такое улучшение работает за время х2 от количества вагонов. И в среднем работает в 2 раза быстрее, чем оригинальный маятник, и примерно наравне с оптимизированным спидометром(чуточку хуже).
Но! Кажется на основе оптимизированного спидометра я придумал линейный алгоритм, дающий стабильно O(4n) для совершенно рандомных случаев и во всех особых случаях, когда все лампочки либо включены, либо выключены. И где-то О(8n) в худшем случае для конкретно этого алгоритма.
В чем суть. Для оптимизированного спидометра мы разрешали себе идти дальше, когда на очередном проходе на пути от начального вагона мы в первый раз встретили зажженную лампочку, но только до тех пор, пока не встретим еще одну единичку или не закончатся наши разрешенные шаги. А что если пойти дальше, даже после второй зажженной лампочки? Что если каждый раз когда мы встречаем такую лампочку, то разрешаем себе идти еще в 2 раза дальше? Прям каждый раз. Встретили зажженную лампочку - погасили и может идти дальше на столько же вагонов, сколько мы прошли от начала до только что погашенной нами лампочки. По факту, мы идем вперед всегда, пока нам встречаются зажженные лампочки и мы не уходим слишком далеко от последней погашенной нами. Тогда существует всего 2 варианта - рано или поздно мы погасим начальную лампочку или последняя погашенная не была начальной, но мы так и не дошли до следующей зажженной до того, как истекли наши разрешенные вагоны.
В первом случае мы просто пройдем лишний круг и вернемся обратно. Таким образом пройдя 2 лишних круга.
Во втором случае мы идем обратно и проверяем, была ли последняя выключенная нами лампочка той самой начальной. Если не была, то снова начинаем идти вправо, пока не наткнемся на зажженную лампочку.
Таким образом, если зажженные лампочки достаточно часто встречаются, то мы идем в один проход до конца поезда, потом еще удвоенную его длину и обратно. Получается, что нам понадобится 4 длины поезда, чтобы посчитать количество вагонов.
Но есть у такого алгоритма худший случай. Тогда, когда зажженные лампочки стоят ровно на один вагон дальше, чем нам разрешено пройти. Пример: зажженная лампочка находится в 3-м вагоне. После того, как мы ее погасили, нам разрешается идти еще 2 вагона и искать там зажженные лампочки. То есть последний вагон, который мы можем посмотреть на этой итерации - 5-ый. А вот следующая зажженная лампочка будет в шестом. И мы могли бы всего на один вагончик вперед пройти и встретить ее, но согласно алгоритму мы должны вернуться к изначальному вагону. Если после шестого вагона лампочка будет в 12-м, то мы обязаны будем вернуться назад и снова пройти вперед до 12-ого. И так далее. Думаю, суть вы уловили.
Так вот на таких данных сложность повышается до примерно О(8n). Эту чиселку вывел совершенно экспериментально. Возможно знатоки математики и теории алгоритмов выведут более точную зависимость. Чему я очень буду рад)
Вроде бы звучит разумно и тесты все проходятся прекрасно. И сложность на первый взгляд линейная во всех случаях, что не может не радовать.
Прикреплю в комменты полный файл со всеми сравнениями. Но вот ссылка на годболт кому будет так удобнее.
Критика и замечания приветствуются.
Improve yourself. Stay cool.
#fun #optimization
По поводу моих улучшений. Немного поразмыслив, первая моя мысль - что с маятником тоже можно сделать маленькое улучшение такое же, как и базовое улучшение спидометра. Мы не каждый раз поворачиваем и идем в обратную сторону, а только тогда, когда меняем состояние встретившейся лампочки.
Во всех особых случаях, когда все лампочки либо включены, либо выключены, такое улучшение работает за время х2 от количества вагонов. И в среднем работает в 2 раза быстрее, чем оригинальный маятник, и примерно наравне с оптимизированным спидометром(чуточку хуже).
Но! Кажется на основе оптимизированного спидометра я придумал линейный алгоритм, дающий стабильно O(4n) для совершенно рандомных случаев и во всех особых случаях, когда все лампочки либо включены, либо выключены. И где-то О(8n) в худшем случае для конкретно этого алгоритма.
В чем суть. Для оптимизированного спидометра мы разрешали себе идти дальше, когда на очередном проходе на пути от начального вагона мы в первый раз встретили зажженную лампочку, но только до тех пор, пока не встретим еще одну единичку или не закончатся наши разрешенные шаги. А что если пойти дальше, даже после второй зажженной лампочки? Что если каждый раз когда мы встречаем такую лампочку, то разрешаем себе идти еще в 2 раза дальше? Прям каждый раз. Встретили зажженную лампочку - погасили и может идти дальше на столько же вагонов, сколько мы прошли от начала до только что погашенной нами лампочки. По факту, мы идем вперед всегда, пока нам встречаются зажженные лампочки и мы не уходим слишком далеко от последней погашенной нами. Тогда существует всего 2 варианта - рано или поздно мы погасим начальную лампочку или последняя погашенная не была начальной, но мы так и не дошли до следующей зажженной до того, как истекли наши разрешенные вагоны.
В первом случае мы просто пройдем лишний круг и вернемся обратно. Таким образом пройдя 2 лишних круга.
Во втором случае мы идем обратно и проверяем, была ли последняя выключенная нами лампочка той самой начальной. Если не была, то снова начинаем идти вправо, пока не наткнемся на зажженную лампочку.
Таким образом, если зажженные лампочки достаточно часто встречаются, то мы идем в один проход до конца поезда, потом еще удвоенную его длину и обратно. Получается, что нам понадобится 4 длины поезда, чтобы посчитать количество вагонов.
Но есть у такого алгоритма худший случай. Тогда, когда зажженные лампочки стоят ровно на один вагон дальше, чем нам разрешено пройти. Пример: зажженная лампочка находится в 3-м вагоне. После того, как мы ее погасили, нам разрешается идти еще 2 вагона и искать там зажженные лампочки. То есть последний вагон, который мы можем посмотреть на этой итерации - 5-ый. А вот следующая зажженная лампочка будет в шестом. И мы могли бы всего на один вагончик вперед пройти и встретить ее, но согласно алгоритму мы должны вернуться к изначальному вагону. Если после шестого вагона лампочка будет в 12-м, то мы обязаны будем вернуться назад и снова пройти вперед до 12-ого. И так далее. Думаю, суть вы уловили.
Так вот на таких данных сложность повышается до примерно О(8n). Эту чиселку вывел совершенно экспериментально. Возможно знатоки математики и теории алгоритмов выведут более точную зависимость. Чему я очень буду рад)
Вроде бы звучит разумно и тесты все проходятся прекрасно. И сложность на первый взгляд линейная во всех случаях, что не может не радовать.
Прикреплю в комменты полный файл со всеми сравнениями. Но вот ссылка на годболт кому будет так удобнее.
Критика и замечания приветствуются.
Improve yourself. Stay cool.
#fun #optimization
🔥15👍4❤2
Inline namespace
В этом сообщении Михаил попросил нас сделать пост про inline namespace'ы. Это хорошо вписывается в контекст постинга в последние месяцы, когда мы много говорим о линковке и inline. Поэтому держите.
Ключевое слово inline используется либо как подсказка компилятору для встраивания кода функции в место ее вызова, либо как средство обхода ODR. Применять его можно и к функциям, и к переменным(с С++17). Но есть еще одно интересное применение inline. Им мы можем пометить пространство имен. Такая пометка неймспейсов доступна с С++11. Я бы сказал, что судя по названию и уже усвоенной инфе, мы можем понять, что это значит. Но нет. Нихрена не понятно. Поэтому будем разбираться.
Контекст и использование встроенных неймспейсов совсем простой, однако он отличается от поведения других inline сущностей. Все, что объявлено внутри такого пространства имен, считается также членом внешнего неймспейса, которое содержит в себе данный inline namespace. То есть
есть у нас вот такая иерархия. Неймспейс трах. У него есть шаблонная функция тенберг. И в него вложен встроеный неймспейс тибедох. В этом внутреннем неймспейсе шаблонный класс тибедох. И прикол в том, что вместо trah::tibidoh::tibidoh я могу написать просто trah::tibidoh и использовать вложенный класс наряду с функцией trah::tenberg.
То есть в общем и целом, этот механизм позволяет автору кода сделать так, чтобы все объявления во вложенном неймспейсе выглядели и действовали, как объявления внешнего неймспейса. Более того, если есть несколько вложенных встроенных неймспейсов, то все объявления всех вложенных неймспейсов доступны в первом невстроенном пространстве имен.
Для чего это нужно? По сути эта фича призвана решить одну единственную проблему - версионирование библиотеки. Глянем на прошлый пример. Библиотека trah активно развивается и, начиная с какой-то версии, в ней появился класс tibedoh, которого не было в предыдущей версии библиотеки. До появления С++11 это выглядело бы примерно так:
В этом случае нам недоступен класс tibidoh, если мы не используем новую версию либы. А если используем, то using помогает нам не писать оператор разрешения имен и пользоваться классом tibidoh, как если бы он был членом неймспейса trah. То есть вот так trah::tibidoh. Вроде все круто и нет проблем. Но не все так просто.
Мне, как пользователю библиотеки, не желательно что-то объявлять в пространстве имен trah, по аналогии с std. Однако мне разрешается делать полную специализацию для шаблонов непосредственно в trah без последствий. Я, как автор своих классов, лучше знаю, как ими нужно оперировать, чтобы достичь максимального перфоманса. Зная API библиотеки, я думаю, что класс tibedoh объявлен в trah. Окей, пишу:
Но вот проблема. Мне позволяется полностью специализировать шаблоны именно в том пространстве имен, в котором шаблон объявлен. А на самом деле он объявлен во вложенном неймспейсе. Предыдущий код мог бы сработать для неверсионированной либы, но в нашем случае будет просто ошибка компиляции.
Это не единственный пример, когда вложенное пространство имен становится видным пользовательскому коду. Например, ADL(кто знает, тот знает, раскрытие adl не влезет в этом пост) не следует директиве using и при поиске символа из вложенного неймспейса во внешнем. ADL будет очень стараться, но так и не найдет нужный символ, который на самом деле спрятан во вложенном namespace.
Продолжение в комментах
#cpp11 #cppcore
В этом сообщении Михаил попросил нас сделать пост про inline namespace'ы. Это хорошо вписывается в контекст постинга в последние месяцы, когда мы много говорим о линковке и inline. Поэтому держите.
Ключевое слово inline используется либо как подсказка компилятору для встраивания кода функции в место ее вызова, либо как средство обхода ODR. Применять его можно и к функциям, и к переменным(с С++17). Но есть еще одно интересное применение inline. Им мы можем пометить пространство имен. Такая пометка неймспейсов доступна с С++11. Я бы сказал, что судя по названию и уже усвоенной инфе, мы можем понять, что это значит. Но нет. Нихрена не понятно. Поэтому будем разбираться.
Контекст и использование встроенных неймспейсов совсем простой, однако он отличается от поведения других inline сущностей. Все, что объявлено внутри такого пространства имен, считается также членом внешнего неймспейса, которое содержит в себе данный inline namespace. То есть
namespace trah {
inline namespace tibidoh {
template<typename T> class tibidoh{ /* ... */ };
}
template<typename T> void tenberg(T) { /* ... */ }
}есть у нас вот такая иерархия. Неймспейс трах. У него есть шаблонная функция тенберг. И в него вложен встроеный неймспейс тибедох. В этом внутреннем неймспейсе шаблонный класс тибедох. И прикол в том, что вместо trah::tibidoh::tibidoh я могу написать просто trah::tibidoh и использовать вложенный класс наряду с функцией trah::tenberg.
То есть в общем и целом, этот механизм позволяет автору кода сделать так, чтобы все объявления во вложенном неймспейсе выглядели и действовали, как объявления внешнего неймспейса. Более того, если есть несколько вложенных встроенных неймспейсов, то все объявления всех вложенных неймспейсов доступны в первом невстроенном пространстве имен.
Для чего это нужно? По сути эта фича призвана решить одну единственную проблему - версионирование библиотеки. Глянем на прошлый пример. Библиотека trah активно развивается и, начиная с какой-то версии, в ней появился класс tibedoh, которого не было в предыдущей версии библиотеки. До появления С++11 это выглядело бы примерно так:
namespace trah {
# if __version == new
using namespace tibidoh;
# endif
namespace tibidoh {
template<typename T> class tibidoh{ /* ... */ };
}
template<typename T> void tenberg(T) { /* ... */ }
}
В этом случае нам недоступен класс tibidoh, если мы не используем новую версию либы. А если используем, то using помогает нам не писать оператор разрешения имен и пользоваться классом tibidoh, как если бы он был членом неймспейса trah. То есть вот так trah::tibidoh. Вроде все круто и нет проблем. Но не все так просто.
Мне, как пользователю библиотеки, не желательно что-то объявлять в пространстве имен trah, по аналогии с std. Однако мне разрешается делать полную специализацию для шаблонов непосредственно в trah без последствий. Я, как автор своих классов, лучше знаю, как ими нужно оперировать, чтобы достичь максимального перфоманса. Зная API библиотеки, я думаю, что класс tibedoh объявлен в trah. Окей, пишу:
namespace trah {
template <>
class tibidoh<AbraKadabra> {
// ...
};
}Но вот проблема. Мне позволяется полностью специализировать шаблоны именно в том пространстве имен, в котором шаблон объявлен. А на самом деле он объявлен во вложенном неймспейсе. Предыдущий код мог бы сработать для неверсионированной либы, но в нашем случае будет просто ошибка компиляции.
Это не единственный пример, когда вложенное пространство имен становится видным пользовательскому коду. Например, ADL(кто знает, тот знает, раскрытие adl не влезет в этом пост) не следует директиве using и при поиске символа из вложенного неймспейса во внешнем. ADL будет очень стараться, но так и не найдет нужный символ, который на самом деле спрятан во вложенном namespace.
Продолжение в комментах
#cpp11 #cppcore
🔥11👍6❤2🤔1
Сколько весит объект пустого класса?
Это знание не имеет практически никакого смысла. Как и большинство знаний в этом мире. Однако этот вопрос очень любят интервьюеры, а нормисы-программисты никогда может и не создавали пустой класс(пара практических применений у этого есть, но сейчас не об этом).
Самый очевидный и первый приходящий в голову ответ - 0. Просто 0. Пустой класс, 0. Все сходится. И это очень логично. Однако это не соответствует реальности.
Реальность в том, что пустые классы - такие же классы с точки зрения кода программы и с ними можно делать все то же самое, что и с обычными классами. Например, создать массив объектов пустого класса. Это опять же не имеет смысла. Но для компилятора объект неполиморфного класса с определенным набором методов - тоже пустой. Потому что адреса методов не хранятся в самом классе, они подставляются непосредственно в момент вызова этих методов. А вот такие объекты уже можно хранить. Они хоть что-то делать могут.
Так вот. Если бы объект пустого был размером 0 байт, его невозможно было бы проиндексировать в массиве. Порядковый номер элемента в массиве задает сдвиг этого элемента от начала. Любой сдвиг помноженный на ноль будет равен нулю. Хотя бы уже поэтому любой объект должен хоть сколько-нибудь весить.
Ну и следующий по логике ответ - 1 байт - будет верным. 1 байт это минимальный адресуемый размер памяти (даже тип bool занимает 1 байт), поэтому это довольно логично.
Stay reasonable. Stay cool.
#interview #cppcore
Это знание не имеет практически никакого смысла. Как и большинство знаний в этом мире. Однако этот вопрос очень любят интервьюеры, а нормисы-программисты никогда может и не создавали пустой класс(пара практических применений у этого есть, но сейчас не об этом).
Самый очевидный и первый приходящий в голову ответ - 0. Просто 0. Пустой класс, 0. Все сходится. И это очень логично. Однако это не соответствует реальности.
Реальность в том, что пустые классы - такие же классы с точки зрения кода программы и с ними можно делать все то же самое, что и с обычными классами. Например, создать массив объектов пустого класса. Это опять же не имеет смысла. Но для компилятора объект неполиморфного класса с определенным набором методов - тоже пустой. Потому что адреса методов не хранятся в самом классе, они подставляются непосредственно в момент вызова этих методов. А вот такие объекты уже можно хранить. Они хоть что-то делать могут.
Так вот. Если бы объект пустого был размером 0 байт, его невозможно было бы проиндексировать в массиве. Порядковый номер элемента в массиве задает сдвиг этого элемента от начала. Любой сдвиг помноженный на ноль будет равен нулю. Хотя бы уже поэтому любой объект должен хоть сколько-нибудь весить.
Ну и следующий по логике ответ - 1 байт - будет верным. 1 байт это минимальный адресуемый размер памяти (даже тип bool занимает 1 байт), поэтому это довольно логично.
Stay reasonable. Stay cool.
#interview #cppcore
🔥32👍14❤8
anonymous namespace
Раз мы затрагиваем тему static и линковки, я не могу не рассказать про эту фичу. Есть такая штука, как безымянные или анонимные пространства имен. Они из себя представляют примерно следующее:
Как же можно получить доступ к содержимому этого неймспейса, если у него нет имени?
На самом деле имя есть. Его генерирует компилятор. Вот во что преобразуется пример выше внутри компилятора:
Будем разбирать по порядку, потому что здесь все непросто.
Во-первых, все, что мы объявили во всех безымянных пространствах шарится между ними внутри одного и того же скоупа, собственно как и для обычных нэймспейсов. Это благодаря тому, что все безымянные неймспейсы внутри одной единицы трансляции имеют одно и то же уникальное имя, генерируемое компилятором.
Во-вторых, все содержимое безымянных пространств видно из родительского пространства за счет дирекивы using. Благодаря этому мы можем пользоваться всеми членами unnamed namespace, как если бы они были в текущем неймспейсе.
В-третьих, на каждую единицу трансляции будет уникальное имя unique, которое больше никому не будет известно. Это значит, что ни одна другая единица трансляции не сможет получить доступ к intvar и foo, потому что не будет знать это уникальное имя.
И вот здесь ключевой момент. Хоть intvar и foo имеют внешнее связывание, но по сути к ним из другого юнита нельзя получить доступ. Значит они имеют эффект внутреннего связывания. Начиная вроде с 11-го стандарта там даже написано, что все члены безымянных неймспейсов имеют внутреннее связывание. Но это стандарт. Некоторые компиляторы типа VS2015/VS2017 считают все неконстантные переменные и свободные функции внутри безымянных пространств extern. На самом деле тут тонкий и не очень понятный момент, потому что именованные пространства имен содержат члены с внешним связыванием. А также в стандарте написано, что анонимное пространство раскрывается как именованое. Но теперь все объявления внутри почему-то имеют внутреннее связывание. Не эффект внутреннего связывания, а прям оно самое. Со всеми плюшками оптимизаций. Как это работает, мне не очень понятно. Знающие люди, отпишитесь в комментариях пожалуйста.
Что нужно понимать на верхнем уровне. Безымянные пространства имен используются для сокрытия данных от других единиц трансляции, обеспечивая видимость всего, что вы туда запихаете, только в текущей единице.
Если вы начали проводить параллели со static, то вы не ошибаетесь. static имеет тот же самый эффект на функции и на переменные. Он меняет связывание с внешнего на внутреннее. Безымянные неймспейсы делают то же самое, только на стероидах. Поэтому их и использовать можно в тех же сценариях и еще в некоторых других.
Следующий пост будет как раз про отличия unnamed namespace и static
Don't expose your secrets. Stay cool.
#cppcore #cpp11 #compiler
Раз мы затрагиваем тему static и линковки, я не могу не рассказать про эту фичу. Есть такая штука, как безымянные или анонимные пространства имен. Они из себя представляют примерно следующее:
namespace {
int int_var = 0;
void foo() {...}
}Как же можно получить доступ к содержимому этого неймспейса, если у него нет имени?
На самом деле имя есть. Его генерирует компилятор. Вот во что преобразуется пример выше внутри компилятора:
namespace unique{}
using namespace unique;
namespace unique{
int int_var = 0;
void foo() {...}
}Будем разбирать по порядку, потому что здесь все непросто.
Во-первых, все, что мы объявили во всех безымянных пространствах шарится между ними внутри одного и того же скоупа, собственно как и для обычных нэймспейсов. Это благодаря тому, что все безымянные неймспейсы внутри одной единицы трансляции имеют одно и то же уникальное имя, генерируемое компилятором.
Во-вторых, все содержимое безымянных пространств видно из родительского пространства за счет дирекивы using. Благодаря этому мы можем пользоваться всеми членами unnamed namespace, как если бы они были в текущем неймспейсе.
В-третьих, на каждую единицу трансляции будет уникальное имя unique, которое больше никому не будет известно. Это значит, что ни одна другая единица трансляции не сможет получить доступ к intvar и foo, потому что не будет знать это уникальное имя.
И вот здесь ключевой момент. Хоть intvar и foo имеют внешнее связывание, но по сути к ним из другого юнита нельзя получить доступ. Значит они имеют эффект внутреннего связывания. Начиная вроде с 11-го стандарта там даже написано, что все члены безымянных неймспейсов имеют внутреннее связывание. Но это стандарт. Некоторые компиляторы типа VS2015/VS2017 считают все неконстантные переменные и свободные функции внутри безымянных пространств extern. На самом деле тут тонкий и не очень понятный момент, потому что именованные пространства имен содержат члены с внешним связыванием. А также в стандарте написано, что анонимное пространство раскрывается как именованое. Но теперь все объявления внутри почему-то имеют внутреннее связывание. Не эффект внутреннего связывания, а прям оно самое. Со всеми плюшками оптимизаций. Как это работает, мне не очень понятно. Знающие люди, отпишитесь в комментариях пожалуйста.
Что нужно понимать на верхнем уровне. Безымянные пространства имен используются для сокрытия данных от других единиц трансляции, обеспечивая видимость всего, что вы туда запихаете, только в текущей единице.
Если вы начали проводить параллели со static, то вы не ошибаетесь. static имеет тот же самый эффект на функции и на переменные. Он меняет связывание с внешнего на внутреннее. Безымянные неймспейсы делают то же самое, только на стероидах. Поэтому их и использовать можно в тех же сценариях и еще в некоторых других.
Следующий пост будет как раз про отличия unnamed namespace и static
Don't expose your secrets. Stay cool.
#cppcore #cpp11 #compiler
🔥13👍7❤4🤯1
anonymous namespace vs static
Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом static, поэтому сегодня обсудим, какие между ними различия. Поехали!
1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.
А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.
А для кода в глобальном скоупе они делают похожие вещи. Поэтому проще запомнить, что для сокрытия данных нужно использовать безымянные пространства, просто потому что они ни для чего больше не нужны.
2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).
3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.
4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.
5️⃣Был такой прикол, что некоторые шаблонные аргументы не могут быть внутренне связными сущностями. Помните, что шаблонный аргумент становится частью инстанцированного типа. Но сущности с внутренним связыванием не видны другим единицам трансляции, поэтому это дело не соберется.
Например
В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.
Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.
Также круто скрывать определения классов от ненужных глаз.
Но вот на счет свободных функций не уверен. Одна, две еще можно. Но обычно их определения удобно располагать рядышком с использующим их кодом. И группировка их вместе будет уменьшать читабельность кода.
В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)
Use proper tools. Stay cool.
#cppcore #cpp17 #design
Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом static, поэтому сегодня обсудим, какие между ними различия. Поехали!
1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.
А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.
А для кода в глобальном скоупе они делают похожие вещи. Поэтому проще запомнить, что для сокрытия данных нужно использовать безымянные пространства, просто потому что они ни для чего больше не нужны.
2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).
3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.
4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.
5️⃣Был такой прикол, что некоторые шаблонные аргументы не могут быть внутренне связными сущностями. Помните, что шаблонный аргумент становится частью инстанцированного типа. Но сущности с внутренним связыванием не видны другим единицам трансляции, поэтому это дело не соберется.
Например
template <int const& Size>
class test {};
static int Size1 = 10;
namespace {
int Size2 = 10;
}
test<Size1> t1; // ERROR!!!
test<Size2> t2;
В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.
Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.
Также круто скрывать определения классов от ненужных глаз.
Но вот на счет свободных функций не уверен. Одна, две еще можно. Но обычно их определения удобно располагать рядышком с использующим их кодом. И группировка их вместе будет уменьшать читабельность кода.
В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)
Use proper tools. Stay cool.
#cppcore #cpp17 #design
🔥11👍7❤4
Anonymous namespace в хэдэрах
Безымянные пространства имен звучат, как хорошая альтернатива static. Была даже тема, что в С++11 задеприкейтили static для свободных функций и переменных в угоду замены на anonymous namespace. Однако от этой идеи отказались, чтобы сильно с сишечкой не расходиться.
Когда нам рассказывают про такой мощный инструмент и облизывают его со всех сторон, то нужно проверить, реально ли он так хорош во всех ситуациях. Сегодня поговорим про его использование в хэдэрах.
Открываем С++ Core Guidelines и видим, что там написано "не используйте анонимные пространства имен в заголовочных файлах". Эх, а так хотелось...
Это рекомендация практически полностью основана на том факте, что сущности в безымянных неймспейсах имеют внутреннее связывание. И это может привести к следующим проблемам:
1️⃣ Раздувается бинарный файл. Во всех единицах трансляции, куда включен ваш хэдэр, будут содержаться копии сущностей из анонимного пространства имен. На копии тратится память и, ну вы поняли...
2️⃣ Вы рискуете нарваться на неопределенное поведение. Рассмотрим такой пример:
Правило для встроенных функций заключается в том, что их определения во всех единицах трансляции должны быть идентичными. В примере выше каждая единица перевода имеет отдельный экземпляр pi. И результирующее определении twoPiR будет выбрано рандомно из всех юнитов, поэтому в нем будет локальным адрес pi из какого-то одного юнита. Соотвественно, использование локального адреса одной единицы трансляции в другой ведет к UB.
Конечно, даже без анонимного пространства имен это было бы неопределенное поведение здесь (поскольку const означает внутреннюю линковку по умолчанию), но основной принцип сохраняется. Любое использование в заголовке чего-либо в неназванном пространстве имен (или любого объекта const, определенного в заголовке), скорее всего, вызовет неопределенное поведение. Является ли это реальной проблемой или нет, зависит, от того, как будет обработана переменная pi. Скорее всего в этом конкретном случае компилятор просто вставит значение 3.14159 в место использования. И тогда никакой зависимости от других единиц трансляции не будет. Но для более сложных объектов не стоит надеятся на такие оптимизации.
3️⃣ Внутри одной единицы трансляции все анонимные неймспейсы на самом деле являются одним пространством с уникальным для юнита именем. Это значит, если в двух заголовочниках у вас будут такие строчки:
и вы подключите их в один исходник, то произойдет нарушение ODR. У вас будут два определения одной и той же сущности внутри одного пространства имен.
Сложно контролировать имена объектов и функций, которые мы располагаем в заголовочниках, чтобы они не пересекались с именами у других хэдэрах. Поэтому, если в какой-то момент вы будете вынуждены подключить два заголовочника(код которых уже где-то используется) с пересекающимися именами в своих anonymous namespace в один цппшник, то будут проблемки. Придется переименовывать какую-то половину сущностей во всем проекте, где они использовались.
До С++17 у нас особо не было универсального и удобного способа распространять код только через хэдэра. Теперь же есть inline переменные. И мы можем определять внешнесвязные сущности в хэдэрах. Это полностью решило все перечисленные здесь проблемы.
Поэтому используйте inline функции и переменные в хэдэрах и радуйтесь жизни!
Enjoy life. Stay cool.
#cpp11 #cpp17 #compiler #cppcore
Безымянные пространства имен звучат, как хорошая альтернатива static. Была даже тема, что в С++11 задеприкейтили static для свободных функций и переменных в угоду замены на anonymous namespace. Однако от этой идеи отказались, чтобы сильно с сишечкой не расходиться.
Когда нам рассказывают про такой мощный инструмент и облизывают его со всех сторон, то нужно проверить, реально ли он так хорош во всех ситуациях. Сегодня поговорим про его использование в хэдэрах.
Открываем С++ Core Guidelines и видим, что там написано "не используйте анонимные пространства имен в заголовочных файлах". Эх, а так хотелось...
Это рекомендация практически полностью основана на том факте, что сущности в безымянных неймспейсах имеют внутреннее связывание. И это может привести к следующим проблемам:
1️⃣ Раздувается бинарный файл. Во всех единицах трансляции, куда включен ваш хэдэр, будут содержаться копии сущностей из анонимного пространства имен. На копии тратится память и, ну вы поняли...
2️⃣ Вы рискуете нарваться на неопределенное поведение. Рассмотрим такой пример:
namespace {
double const pi = 3.14159;
}
inline double twoPiR( double r ) { return 2.0 * pi * r; }Правило для встроенных функций заключается в том, что их определения во всех единицах трансляции должны быть идентичными. В примере выше каждая единица перевода имеет отдельный экземпляр pi. И результирующее определении twoPiR будет выбрано рандомно из всех юнитов, поэтому в нем будет локальным адрес pi из какого-то одного юнита. Соотвественно, использование локального адреса одной единицы трансляции в другой ведет к UB.
Конечно, даже без анонимного пространства имен это было бы неопределенное поведение здесь (поскольку const означает внутреннюю линковку по умолчанию), но основной принцип сохраняется. Любое использование в заголовке чего-либо в неназванном пространстве имен (или любого объекта const, определенного в заголовке), скорее всего, вызовет неопределенное поведение. Является ли это реальной проблемой или нет, зависит, от того, как будет обработана переменная pi. Скорее всего в этом конкретном случае компилятор просто вставит значение 3.14159 в место использования. И тогда никакой зависимости от других единиц трансляции не будет. Но для более сложных объектов не стоит надеятся на такие оптимизации.
3️⃣ Внутри одной единицы трансляции все анонимные неймспейсы на самом деле являются одним пространством с уникальным для юнита именем. Это значит, если в двух заголовочниках у вас будут такие строчки:
namespace {
int x;
}и вы подключите их в один исходник, то произойдет нарушение ODR. У вас будут два определения одной и той же сущности внутри одного пространства имен.
Сложно контролировать имена объектов и функций, которые мы располагаем в заголовочниках, чтобы они не пересекались с именами у других хэдэрах. Поэтому, если в какой-то момент вы будете вынуждены подключить два заголовочника(код которых уже где-то используется) с пересекающимися именами в своих anonymous namespace в один цппшник, то будут проблемки. Придется переименовывать какую-то половину сущностей во всем проекте, где они использовались.
До С++17 у нас особо не было универсального и удобного способа распространять код только через хэдэра. Теперь же есть inline переменные. И мы можем определять внешнесвязные сущности в хэдэрах. Это полностью решило все перечисленные здесь проблемы.
Поэтому используйте inline функции и переменные в хэдэрах и радуйтесь жизни!
Enjoy life. Stay cool.
#cpp11 #cpp17 #compiler #cppcore
🔥11👍7❤6
Количество островов
Начинаем давно напрашивающуюся рубрику на канале - #задачки. В рамках рубрики будем рассматривать решения абсолютно разных задач - и алгоритмических, и логических и математических, если попадутся интересные. На совсем интересные задачи будет отводиться много времени и постов, как например с поездами, где реально много всего можно пообсуждать. На остальные - кажется одного поста будет достаточно.
Но я так и не придумал универсального способа публикации решения. Чтобы и людям было что пообсуждать и не растягивать на 2 дня не самые сложные для решения задачи. Поэтому прошу вас оставить свое мнение под этим постом, как можно публиковать решение, учитывая необходимость для других пообсуждать что-то в комментах без спойлеров.
Если по сабжу. Задачка довольно популярная среди народа, хотя и ни разу мне не попадалась на собесах. Решение довольно тривиальное, но нужно иметь большую насмотреннсть и количество решенных алгоритмических задач, чтобы самостоятельно с нуля дойти до решения.
Суть. Дана бинарная матрица n x m для карта, где каждая ячейка репрезентует либо сушу(единичка), либо море (нолик). Нужно вернуть количество островов на карте.
Остров - суша, окруженная водой и сформирована из смежных ячеек суши по вертикали и горизонтали(не по диагонали!). Предполагается, что за пределами карты везде вода.
И еще напишите, не помешает ли вам наличие заблюренных комментов с решениям людей, которые не уверены в том, что решили самым оптимальным способом. Потому что было бы прикольно, чтобы такие люди скидывали свои решения на условное ревью. Пока этого не будет, но если особых возражений не будет, то введем такую возможность.
Stay cool.
Начинаем давно напрашивающуюся рубрику на канале - #задачки. В рамках рубрики будем рассматривать решения абсолютно разных задач - и алгоритмических, и логических и математических, если попадутся интересные. На совсем интересные задачи будет отводиться много времени и постов, как например с поездами, где реально много всего можно пообсуждать. На остальные - кажется одного поста будет достаточно.
Но я так и не придумал универсального способа публикации решения. Чтобы и людям было что пообсуждать и не растягивать на 2 дня не самые сложные для решения задачи. Поэтому прошу вас оставить свое мнение под этим постом, как можно публиковать решение, учитывая необходимость для других пообсуждать что-то в комментах без спойлеров.
Если по сабжу. Задачка довольно популярная среди народа, хотя и ни разу мне не попадалась на собесах. Решение довольно тривиальное, но нужно иметь большую насмотреннсть и количество решенных алгоритмических задач, чтобы самостоятельно с нуля дойти до решения.
Суть. Дана бинарная матрица n x m для карта, где каждая ячейка репрезентует либо сушу(единичка), либо море (нолик). Нужно вернуть количество островов на карте.
Остров - суша, окруженная водой и сформирована из смежных ячеек суши по вертикали и горизонтали(не по диагонали!). Предполагается, что за пределами карты везде вода.
И еще напишите, не помешает ли вам наличие заблюренных комментов с решениям людей, которые не уверены в том, что решили самым оптимальным способом. Потому что было бы прикольно, чтобы такие люди скидывали свои решения на условное ревью. Пока этого не будет, но если особых возражений не будет, то введем такую возможность.
Stay cool.
🔥13👍4❤2
Количество Островов
С первого наивного взгляда не очень понятно, как подступиться к задаче. Каким-то образом мы должны каждую встретившуюся единичку отнести к определенной группе и посчитать количество этих групп. Самый главный вопрос - как выявить отдельную группу?
На этот вопрос в принципе дан ответ - остров состоит из смежных клеток. То есть, встретив кусочек острова, мы можем пройти к любому другому кусочку, двигаясь только вверх-вниз и вправо-влево. Так формируется группа ячеек - остров.
Каким-то образом нам нужно запоминать маршрут, чтобы не приходить туда, где уже были и одновременно с этим приходить в самые удаленные части острова. Первое решается изменением 1 на 0. То есть, проходя по клетке, мы ее затапливаем и соотвественно больше не можем по ней проходить. С помощью этого приема кстати мы не сможем посчитать один остров 2 раза. Второе решается применением ключевого подхода для задачи - обход графа. А точнее сказать в нашем случае - дерева.
Как из острова получить дерево?
Для начала осознаем, что остров - это граф. Не Монтекристо, а математическая абстракция реальной системы любой природы, объекты которой обладают парными связями. Кусочек суши - вершина. С ней могут быть смежны другие кусочки суши, которые тоже являются вершинами. А переходы между ними - ребра.
Теперь про дерево. Дерево - связный ациклический граф. То что остров - связный граф, думаю, вопросов нет. А вот требование ацикличности не удовлетворяется для острова в базе. Остров из 4-х клеток квадратиком уже будут циклическим графом. Но тут вступают в игру наши изменения - когда мы топим кусочки суши, мы разрываем циклы. Мы не сможем прийти на тот же кусочек суши второй раз, потому что он потоплен.
С этим разобрались. Теперь про обход дерева.
У нас есть несколько алгоритмов обходов деревьев, но базовых два - поиск в глубину и поиск в ширину. Поиск в глубину в данных условиях реализовать проще, поэтому буду использовать его. Его суть состоит в том, чтобы идти «вглубь» графа, насколько это возможно. Алгоритм поиска описывается рекурсивно: перебираем все исходящие из рассматриваемой вершины рёбра. Если ребро ведёт в вершину, которая не была рассмотрена ранее, то запускаем алгоритм от этой нерассмотренной вершины, а после возвращаемся и продолжаем перебирать рёбра. Возврат происходит в том случае, если в рассматриваемой вершине не осталось рёбер, которые ведут в нерассмотренную вершину.
Как только мы встретили единичку, мы начинаем наш обход. Топим текущую ячейку и запускаем рекурсию для всех смежных ячеек. На очередном витке рекурсии мы проверяем, действительно ли сейчас под нами земля. Если да, то топим ее и снова запускаем рекурсию. Если нет, то останавливаем проход по этой ветке и возвращаемся из рекурсии.
Таким образом впервые встретив единичку, мы потопим весь остров и пойдем дальше искать острова. Счетчик будем увеличивать каждый раз, когда встретили единичку.
Вот и все. Довольно длинное объяснение получилось, но само решение простое.
Учитывая, что мы пробегаем всю матрицу по строкам, дважды не проходим по одной и той же клетке суши, а на одну и ту же клетку смотрим не более 4-х раз, то сложность этого алгоритма по времени O(n*m)
По поводу сложности по памяти не так все однозначно, как кажется на первый взгляд. Дело в том, что рекурсия - это по факту наслоение функции на стек вызовов. И хоть мы не используем явной дополнительной памяти в виде контейнеров, мы используем стек вызовов программы в качестве такой дополнительной памяти. В худшем случае, когда вся карта - это один большой остров нам нужно O(n*m) фреймов стека, чтобы хранить информацию о текущем распространении путей обхода.
Solve problems. Stay cool.
С первого наивного взгляда не очень понятно, как подступиться к задаче. Каким-то образом мы должны каждую встретившуюся единичку отнести к определенной группе и посчитать количество этих групп. Самый главный вопрос - как выявить отдельную группу?
На этот вопрос в принципе дан ответ - остров состоит из смежных клеток. То есть, встретив кусочек острова, мы можем пройти к любому другому кусочку, двигаясь только вверх-вниз и вправо-влево. Так формируется группа ячеек - остров.
Каким-то образом нам нужно запоминать маршрут, чтобы не приходить туда, где уже были и одновременно с этим приходить в самые удаленные части острова. Первое решается изменением 1 на 0. То есть, проходя по клетке, мы ее затапливаем и соотвественно больше не можем по ней проходить. С помощью этого приема кстати мы не сможем посчитать один остров 2 раза. Второе решается применением ключевого подхода для задачи - обход графа. А точнее сказать в нашем случае - дерева.
Как из острова получить дерево?
Для начала осознаем, что остров - это граф. Не Монтекристо, а математическая абстракция реальной системы любой природы, объекты которой обладают парными связями. Кусочек суши - вершина. С ней могут быть смежны другие кусочки суши, которые тоже являются вершинами. А переходы между ними - ребра.
Теперь про дерево. Дерево - связный ациклический граф. То что остров - связный граф, думаю, вопросов нет. А вот требование ацикличности не удовлетворяется для острова в базе. Остров из 4-х клеток квадратиком уже будут циклическим графом. Но тут вступают в игру наши изменения - когда мы топим кусочки суши, мы разрываем циклы. Мы не сможем прийти на тот же кусочек суши второй раз, потому что он потоплен.
С этим разобрались. Теперь про обход дерева.
У нас есть несколько алгоритмов обходов деревьев, но базовых два - поиск в глубину и поиск в ширину. Поиск в глубину в данных условиях реализовать проще, поэтому буду использовать его. Его суть состоит в том, чтобы идти «вглубь» графа, насколько это возможно. Алгоритм поиска описывается рекурсивно: перебираем все исходящие из рассматриваемой вершины рёбра. Если ребро ведёт в вершину, которая не была рассмотрена ранее, то запускаем алгоритм от этой нерассмотренной вершины, а после возвращаемся и продолжаем перебирать рёбра. Возврат происходит в том случае, если в рассматриваемой вершине не осталось рёбер, которые ведут в нерассмотренную вершину.
Как только мы встретили единичку, мы начинаем наш обход. Топим текущую ячейку и запускаем рекурсию для всех смежных ячеек. На очередном витке рекурсии мы проверяем, действительно ли сейчас под нами земля. Если да, то топим ее и снова запускаем рекурсию. Если нет, то останавливаем проход по этой ветке и возвращаемся из рекурсии.
Таким образом впервые встретив единичку, мы потопим весь остров и пойдем дальше искать острова. Счетчик будем увеличивать каждый раз, когда встретили единичку.
Вот и все. Довольно длинное объяснение получилось, но само решение простое.
Учитывая, что мы пробегаем всю матрицу по строкам, дважды не проходим по одной и той же клетке суши, а на одну и ту же клетку смотрим не более 4-х раз, то сложность этого алгоритма по времени O(n*m)
По поводу сложности по памяти не так все однозначно, как кажется на первый взгляд. Дело в том, что рекурсия - это по факту наслоение функции на стек вызовов. И хоть мы не используем явной дополнительной памяти в виде контейнеров, мы используем стек вызовов программы в качестве такой дополнительной памяти. В худшем случае, когда вся карта - это один большой остров нам нужно O(n*m) фреймов стека, чтобы хранить информацию о текущем распространении путей обхода.
Solve problems. Stay cool.
👍13🔥5❤2
И еще раз про именование сущностей
В прошлых постах тут и тут мы говорили больше про чистоту и понятность кода. Здесь поговорим немного про безопасность.
Люди часто пишут код, описывая больше технические детали, а не абстракции и способы взаимодействия с ними. Типа если функция хочет обрабатывать токены слов и их частоту в тексте, люди напишут void func(std::unordered_map<std::string, size_t> token_frequency). Что в этом плохого? Ну повторю себя отсюда, что так уменьшаются возможности по подробному описанию сущности и раскрываются ненужные для верхнеуровневого чтения кода детали. Но это полбеды. Еще одна проблема - я могу передать в эту функцию любое неупорядоченное отображение строки в число. С совершенно другим смыслом. По случайности, неосторожности, из любопытства. Неважно. Объект будет нести другой смысл, его не для этого создавали. Однако я могу его использовать, как аргумент для этой функции. Пример может показаться игрушечным и безобидным. Это лишь, чтобы показать суть. В своем самописном условном телеграмм-боте вы можете не запариваться над такими вещами. Там нет особого смысла в безопасности и размножении сущностей.
А по-настоящему раскрывается эта проблема там, где есть запрос на безопасность. Например, в приложениях, широко использующих криптографию. Обычно там есть несколько методов шифрования и несколько типов объектов, которые этому шифрованию подвергаются. Очевидно, что ключи для разных алгоритмов должны иметь разный тип. Даже тот же RSA не имеет фиксированного размера для ключей, а для AES - имеет. Но вот не так очевидно, что ключи одного алгоритма должны иметь разные типы для каждого из объектов, которые будут зашифрованы этими ключами. Условно, для банковского приложения, ключ AES для шифрования сообщений пользователя и его банковской истории должны иметь разный тип, хотя структура ключа одна и та же. Делается это для того, чтобы программист оперировал именно теми сущностями, которые нужны именно под эту конкретную задачу. Чтобы не было случайно или специально в виде ключа для сообщений был подсунут ключ для банковской истории. Нужно осознанно создать объект подходящего класса и оперировать им в подходящих местах. Все это повышает безопасность приложения и данных.
Есть же алиасинг, скажете вы. Зачем явно плодить сущности? Проблема юзингов и тайпдефов в том, что они не отличимы для компилятора от типов оригиналов. И соотвественно, они взаимозаменяемы. Да, синонимы хороши для понятности кода. Но если мы хотим обезопасить наш код - этот способ не пойдет.
Не нужно оборачивать каждый контейнер в кастомный класс, это излишне. Но нужно проектировать системы так, чтобы их нельзя было использовать не по заданному сценарию.
Stay safe. Stay cool.
#goodpractice #design
В прошлых постах тут и тут мы говорили больше про чистоту и понятность кода. Здесь поговорим немного про безопасность.
Люди часто пишут код, описывая больше технические детали, а не абстракции и способы взаимодействия с ними. Типа если функция хочет обрабатывать токены слов и их частоту в тексте, люди напишут void func(std::unordered_map<std::string, size_t> token_frequency). Что в этом плохого? Ну повторю себя отсюда, что так уменьшаются возможности по подробному описанию сущности и раскрываются ненужные для верхнеуровневого чтения кода детали. Но это полбеды. Еще одна проблема - я могу передать в эту функцию любое неупорядоченное отображение строки в число. С совершенно другим смыслом. По случайности, неосторожности, из любопытства. Неважно. Объект будет нести другой смысл, его не для этого создавали. Однако я могу его использовать, как аргумент для этой функции. Пример может показаться игрушечным и безобидным. Это лишь, чтобы показать суть. В своем самописном условном телеграмм-боте вы можете не запариваться над такими вещами. Там нет особого смысла в безопасности и размножении сущностей.
А по-настоящему раскрывается эта проблема там, где есть запрос на безопасность. Например, в приложениях, широко использующих криптографию. Обычно там есть несколько методов шифрования и несколько типов объектов, которые этому шифрованию подвергаются. Очевидно, что ключи для разных алгоритмов должны иметь разный тип. Даже тот же RSA не имеет фиксированного размера для ключей, а для AES - имеет. Но вот не так очевидно, что ключи одного алгоритма должны иметь разные типы для каждого из объектов, которые будут зашифрованы этими ключами. Условно, для банковского приложения, ключ AES для шифрования сообщений пользователя и его банковской истории должны иметь разный тип, хотя структура ключа одна и та же. Делается это для того, чтобы программист оперировал именно теми сущностями, которые нужны именно под эту конкретную задачу. Чтобы не было случайно или специально в виде ключа для сообщений был подсунут ключ для банковской истории. Нужно осознанно создать объект подходящего класса и оперировать им в подходящих местах. Все это повышает безопасность приложения и данных.
Есть же алиасинг, скажете вы. Зачем явно плодить сущности? Проблема юзингов и тайпдефов в том, что они не отличимы для компилятора от типов оригиналов. И соотвественно, они взаимозаменяемы. Да, синонимы хороши для понятности кода. Но если мы хотим обезопасить наш код - этот способ не пойдет.
Не нужно оборачивать каждый контейнер в кастомный класс, это излишне. Но нужно проектировать системы так, чтобы их нельзя было использовать не по заданному сценарию.
Stay safe. Stay cool.
#goodpractice #design
🔥11👍3❤1