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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Задача про закольцованный поезд

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

Меня всегда восхищает, как люди доходят до разных решений одной задачи. Это расширяет мои рамки восприятия мира и я могу перенять эти паттерны мышления. Всем спасибо за ваши варианты решений. Даже читаерные. Скорее даже особенно читерные. Потому что редко у меня получается думать outside the box.

Но сегодня разберем кое-что другое. Увидел в комментах, что многие люди приходили к немного иному решению. Не по аналогии с маятником, а скорее со спидометром. От изначального вагона с включенным светом идем вправо и выключаем лампочку. Потом возвращаемся и проверяем лампочку в самом первом вагоне. Далее снова идем вправо, но уже на один вагон дальше, и выключаем там свет. Возвращаемся обратно и делаем проверку света в изначальном вагоне. Условие остановки - в изначальном вагоне погасла лампочка. И длина поезда - последнее количество вагонов на пути обратно.

Проще ли этот вариант? Думаю, что да. В голове нужно всего один счетчик хранить. А для маятника уже 2 нужно. Как говорят физики: "все, что больше одного - это уже много". После прохождения энного вагона я бы точно что-то перепутал.

Но вот вопрос: а для какого решения требуется пройти меньше вагонов? С первого взгляда, какая разница. И там и там ходим туда-сюда. Но если все хорошенько подсчитать, то разница есть и значительная. Я вот и посчитал. Результаты на картинке под постом.

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

Stay cool.
Мини-гайд по себесам.pdf
190.2 KB
Мини-гайд по собеседованиям

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

В общем, особо не знаю, что сказать. Все в документе.

Share knowledge. Stay cool.

#guide
Что значит, что фича устрела или удалена

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

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

Такой процесс происходил например с std::autoptr, std::randomshuffle, ключевым словом register, триграфами и прочим.

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

Теперь разберем процесс удаления сущности из стандарта.

Для начала она объявляется устаревшей или deprecated. При попытке компиляции с новым стандартом всплывет ворнинг. А в любой нормальной компании ворнинг роняет через бедро сборку проекта. Об этом подробнее здесь рассказывали. И люди просто вынуждены провести небольшой рефакторинг и заменить устаревшую функциональность, чтобы перейти на новый стандарт. Таким образом народ минимально преобразует код, но зато теперь есть возможность использовать фишки нового стандарта, которые могут быть вполне полезными. Ну или забить и не переходить на него. Так тоже иногда делают. Но я бы не хотел работать в такой компании)

После объявления устаревшей стандарт еще какое-то время поддерживает фичу. Но через 3-5-10 лет ее удалят. И больше ее упоминания в стандарте не будет.

Fix your flaws. Stay cool.

#commercial #cppcore
Снова сравниваем решения задачи с поездами

Кто забыл или не в теме, вот эта задача. В прошлом мы уже сравнивали два решения: условный маятник и спидометр. Лучше освежить в памяти контекст, чтобы понятнее был предмет разговора. Однако наша подписчица @tigrisVD сделала несколько улучшений алгоритма спидометра. Вот ее сообщение с пояснениями и кодом.

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

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

Там есть некоторые проблемы с тем, что оптимизированные алгоритмы спидометра зависят от входных данных. И в худшем случае, когда в поезде включены все лампочки, последний оптимизированный алгоритм работает лишь в 2 раза лучше базового спидометра. В лучшем случае, когда все лампочки выключены - сложность алгоритмов линейно зависит от количества вагонов, а не квадратично, как у базового спидометра. Базовое улучшение - нужно пройти всего х2 от количества вагонов. А оптимизированный вариант- х4(я там одну ошибочку нашел в коде при подсчете пройденных вагонов, поэтому не в 3, а в 4). Но в рандомных случаях оптимизированный вариант примерно в 2 раза быстрее базового улучшенного и в 4 раза быстрее оригинального спидометра.

Позапускать ее код и посмотреть на цифры можно тут

Спасибо вам, @tigrisVD, за такие интересные решения!

Но ваши улучшения натолкнули и меня на размышления и улучшения, о которых я расскажу завтра.

Impove 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
Inline namespace

В этом сообщении Михаил попросил нас сделать пост про 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
Сколько весит объект пустого класса?

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

Самый очевидный и первый приходящий в голову ответ - 0. Просто 0. Пустой класс, 0. Все сходится. И это очень логично. Однако это не соответствует реальности.

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

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

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

Stay reasonable. Stay cool.

#interview #cppcore
anonymous namespace

Раз мы затрагиваем тему 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, потому что не будет знать это уникальное имя.

И вот здесь ключевой момент. Хоть int
var и foo имеют внешнее связывание, но по сути к ним из другого юнита нельзя получить доступ. Значит они имеют эффект внутреннего связывания. Начиная вроде с 11-го стандарта там даже написано, что все члены безымянных неймспейсов имеют внутреннее связывание. Но это стандарт. Некоторые компиляторы типа VS2015/VS2017 считают все неконстантные переменные и свободные функции внутри безымянных пространств extern. На самом деле тут тонкий и не очень понятный момент, потому что именованные пространства имен содержат члены с внешним связыванием. А также в стандарте написано, что анонимное пространство раскрывается как именованое. Но теперь все объявления внутри почему-то имеют внутреннее связывание. Не эффект внутреннего связывания, а прям оно самое. Со всеми плюшками оптимизаций. Как это работает, мне не очень понятно. Знающие люди, отпишитесь в комментариях пожалуйста.

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

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

Следующий пост будет как раз про отличия unnamed namespace и static

Don't expose your secrets. Stay cool.

#cppcore #cpp11 #compiler
anonymous namespace vs static

Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом 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
Anonymous namespace в хэдэрах

Безымянные пространства имен звучат, как хорошая альтернатива 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
Количество островов

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

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

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

Суть. Дана бинарная матрица n x m для карта, где каждая ячейка репрезентует либо сушу(единичка), либо море (нолик). Нужно вернуть количество островов на карте.

Остров - суша, окруженная водой и сформирована из смежных ячеек суши по вертикали и горизонтали(не по диагонали!). Предполагается, что за пределами карты везде вода.

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

Stay cool.
Количество Островов

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

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

Каким-то образом нам нужно запоминать маршрут, чтобы не приходить туда, где уже были и одновременно с этим приходить в самые удаленные части острова. Первое решается изменением 1 на 0. То есть, проходя по клетке, мы ее затапливаем и соотвественно больше не можем по ней проходить. С помощью этого приема кстати мы не сможем посчитать один остров 2 раза. Второе решается применением ключевого подхода для задачи - обход графа. А точнее сказать в нашем случае - дерева.

Как из острова получить дерево?

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

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

С этим разобрались. Теперь про обход дерева.

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

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

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

Вот и все. Довольно длинное объяснение получилось, но само решение простое.

Учитывая, что мы пробегаем всю матрицу по строкам, дважды не проходим по одной и той же клетке суши, а на одну и ту же клетку смотрим не более 4-х раз, то сложность этого алгоритма по времени O(n*m)

По поводу сложности по памяти не так все однозначно, как кажется на первый взгляд. Дело в том, что рекурсия - это по факту наслоение функции на стек вызовов. И хоть мы не используем явной дополнительной памяти в виде контейнеров, мы используем стек вызовов программы в качестве такой дополнительной памяти. В худшем случае, когда вся карта - это один большой остров нам нужно O(n*m) фреймов стека, чтобы хранить информацию о текущем распространении путей обхода.

Solve problems. Stay cool.
И еще раз про именование сущностей

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

Люди часто пишут код, описывая больше технические детали, а не абстракции и способы взаимодействия с ними. Типа если функция хочет обрабатывать токены слов и их частоту в тексте, люди напишут void func(std::unordered_map<std::string, size_t> token_frequency). Что в этом плохого? Ну повторю себя отсюда, что так уменьшаются возможности по подробному описанию сущности и раскрываются ненужные для верхнеуровневого чтения кода детали. Но это полбеды. Еще одна проблема - я могу передать в эту функцию любое неупорядоченное отображение строки в число. С совершенно другим смыслом. По случайности, неосторожности, из любопытства. Неважно. Объект будет нести другой смысл, его не для этого создавали. Однако я могу его использовать, как аргумент для этой функции. Пример может показаться игрушечным и безобидным. Это лишь, чтобы показать суть. В своем самописном условном телеграмм-боте вы можете не запариваться над такими вещами. Там нет особого смысла в безопасности и размножении сущностей.

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

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

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

Stay safe. Stay cool.

#goodpractice #design
Ловим исключения в constructor initializer list

Исключение в конструкторе может быть довольно противной вещью. Если исключение кинулось в конструкторе - объект считается не созданным и его деструктор не вызывается. Это может приводить к утечкам ресурсов и другим неприятностям. Здесь нам приходит на помощь конструкция try-catch, которая разруливает такие нештатные ситуации. А что, если у нас в конструкторе используется список инициализации? И исключение бросится в нем? Как в этом случае обезопасить создание объекта?

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

Есть такая штука, как function-try-block. И выглядит она так:
struct Type  
{
Type(const OtherType& )
try : a_{a}, b_{b}
{ ...}
catch (const std:exception& e) {...}
private:
OtherType a_;
AnotherType b_;
};


Этот function-try-block связывает блок catch со всем телом конструктора и! списком иницализации. То есть если из тела конструктора или из базового конструктора или из любого конструктора для поля класса из списка инициализации будет выброшено исключение, то управление передается в единый блок catch.

Гарантируется, что перед входом в секцию catch будет уничтожены все полностью созданные поля и базы класса.

В принципе, раз это function-try-block, то он подходит к использованию в любых функциях, что увеличивает количество кейсов использования блока.

Очень важный момент, что при появлении исключения и перехода в блок catch, после завершения этого блока текущее исключение неявно пробрасывается дальше и его нужно ловить в вызывающем коде. В документации явно прописано, что главная цель function-try-block - ответить на исключение в списке инициализации с помощью логирования, модификации текущего объекта эксепшена, проброса другого исключения или завершения программы. Поэтому, хоть эту конструкцию можно использовать во всех функциях, она редко используется в других ситуациях.

В целом, такая альтернатива привычному блоку try-catch ощутимо увеличивает безопасность кода, особенно, если используются нетривиальные типы и списки инициализации. Да и просто смотрится необычно и свежо. И джунов можно попугать новыми страшными конструкциями)

Stay exploring. Stay cool.

#cppcore #exception
В чем проблема auto_ptr

В стародавние времена, когда еще мамонты ходили по земле, был выпущен стандарт С++98, в стандартной библиотеке которого был один интересный умный указатель std::auto_ptr. Этот шаблон был порождением страхов и мучений. Ибо еще со времен, когда даже динозавры существовали, этим динозаврам было сложно управлять обычными сырыми указателями. И всегда было стремление обезопасить работу с ними в С++. И вот настал тот момент, когда появилось первое стандартное средство безопасного управления памятью. Однако первый блин, как это часто бывает, получился комом...

Конкретно std::auto_ptr реализовывал семантику владения объектом. То есть в конструкторе указатель захватывался, а в деструкторе всегда освобождался. Применение идиомы RAII в самой красе. Но давайте-ка взглянем пристальнее на механизм передачи владения.

Что мы имеет в С++98/03. Мы имеем 2 специальных метода, которые помогают перенимать характеристики другого объекта. Это копирующий конструктор и копирующий оператор присваивания. На этом все. Как можно на этих двух сущностях имплементировать передачу владения?

Очень и очень криво. Копирование на то и копирование, что не изменяет исходный объект. По крайней мере это подразумевают пользователи классов. Однако в целом, мы можем почти все что угодно делать внутри копирующих методов. Ну вот создатели auto_ptr и сделали грязь. Там конструктор и оператор присваивания принимают не константную ссылку, как обычно, а просто ссылку. Внутри они копируют указатель на ресурс из переданного объекта и помещают его в текущий. А указатель на ресурс переданного объекта зануляют.

То есть. Копирующий конструктор и оператор присваивания std::auto_ptr изменяют объект, который в них передается. При чем ладно изменяет. Они делают его пустышкой. Таким образом им больше нельзя пользоваться, так как ресурса там больше нет. Такое контринтуитивное поведение и является основной проблемой auto_ptr.

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

Конечно, это все было из-за отсутствия move-семантики. Язык просто был недостаточно мощным, чтобы реализовать семантику владения.

Однако в С++11 проблема была решена. Введена мув-семантика и std::unique_ptr, в котором реализовали идею авто указателя. При небольшом рефакторинге и замене std::auto_ptr на unique_ptr проблем больше не было. И потребность в использовании бустовых аналогов тоже отпала.

И комитет решил на радостях задеприкейтить auto_ptr, а затем и вовсе удалил его в С++17 за ненабностью.

Fix your mistakes. Stay cool.

#inteview #cpp11 #cpp17
Статические методы класса

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

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

Термин цикл жизни не совсем корректно применять к функциям, но я все же попробую. Жизнь обычного метода, точнее время, когда его можно использовать, ограничено жизнью объекта класса. Есть объект - можно вызвать метод. Нет объекта - нельзя. Со статическими методами другая петрушка. Они доступны всему коду, который видит определение класса, и вызвать их можно в любое время. Даже до входа в main(не то, чтобы это сильно необычно, но утверждение все равно верное).

По поводу линковки тут все просто - тип линковки совпадает с линковкой класса. По умолчанию она внешняя. Но какой-нибудь anonymous namespace может это изменить. Опять же параллели с обычными свободными функциями.

Пока писал этот пост, мне в голову пришла интересная идея. А что, если рассматривать статический метод не как свободную функцию, прикрепленную к классу? Точнее даже не так. А что если рассматривать ВСЕ методы класса, как обычные свободные функции с возможностью заглядывать с приватные члены? На самом деле ведь так и есть: нет никакого метода, в языке С нет такого понятия(это я к тому, что все плюсовые оопшные концепции реализованы с помощью сишных инструментов). Зато есть функции. А класс - это просто данные и функции, которым разрешили работать с этими данными. Теперь вопрос: как с этой точки зрения отличаются статические и нестатические методы?

И на самом деле в базе они отличаются лишь одной вещью: в нестатические методы передается первым параметром this(указатель на объект, из которого вызывается метод), а в статические не передается. И ВСЁ! Все остальные отличия идут от этого.

Например, статические методы не могут быть виртуальными. Оно и понятно, ведь у них нет объекта, из которого они всегда могут достать указатель vptr.

Или еще статические методы не могут быть помечены const. Не удивительно. Ведь на самом деле константные методы - функции, в которые передали const Type *, то есть указатель на константный объект. А его не завезли в статический метод. По этой же причине volatile к таким методам не применим.

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

Попробую даже продемонстрировать это. Возьмем класс и определим у него статический и нестатический методы, которые будут делать одно и то же: увеличивать поле класса за счет входного параметра. Чтобы сделать тоже самое для статического метода, придется ему передать указатель на объект касса. И мы можем использовать для вызова обоих методов один и тот же тип указателя на функцию(см на картинку). Получается, что в нестатическом методе просто к именам, которые совпадают с членами или методами класса, компилятор неявно приписывается this->. Вот и вся история.

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

See all sides of the coin. Stay cool.

#compiler #cppcore #hardcore
std::thread::id

Когда в процессе может существовать несколько потоков(а сейчас обычно это условие выполняется), то очевидно, что эти потоки надо как-то идентифицировать. Они будут конкурировать друг с другом за ресурсы и планировщику нужно знать, кому именно нужно выдавать заветное процессорное время. И действительно, у каждого потока в системе есть свой уникальный номер(лучше сказать идентификатор). И мы, как прикладные разработчики, можем даже его получить. С появлением С++11 у нас есть стандартное средство для этого - std::thread::id. Это тип, который и представляет собой идентификатор потока.

Его можно получить двумя способами - снаружи потока и внутри него.

Снаружи мы должны иметь объект std::thread, у которого хотим узнать id, и вызвать у него метод get_id(). Если с объектом не связан никакой поток исполнения, то вернется дефолтно-сконструированный объект, обозначающий "я девушка свободная, не обременненная никаким исполнением".

Внутри id можно достать с помощью свободной функции std::this_thread::get_id().


Объекты типа std::thread::id могут свободно копироваться и сравниваться. Иначе их нельзя было бы использовать, как идентификаторы. Если два объекта типа std::thread::id равны, то они репрезентуют один и тот же тред. Или оба являются свободными девушками. Если объекты не равны, то они представляют разные треды или один из них создан конструктором по умолчанию.

Стандартная библиотека никак не ограничивает вас в проверке одинаковые ли идентификаторы или нет: для объектов std::thread::id предусмотрен полный набор операторов сравнения, которые предоставляют возможность упорядочить все отличные друг от друга значения. Эта возможность позволяет использовать id в качестве ключей в ассоциативных контейнерах, они могут быть отсортированы и тд. Все вытекающие плюшки. Операторы сравнения обеспечивают свойство транзитивности для объектов. То есть, если a < b и b < c, то a < c.

Также у нас из коробки есть возможность получить хэш id с помощью std::hash<std::thread::id>, что позволяет использовать идентификаторы потоков в хэш-таблицах, аля unordered контейнерах.

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

Identify yourself. Stay cool.

#multitasking #cpp11
Зачем нужен std::thread::id?

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

Но давайте отталкиваться от операций, которые мы можем выполнять с айдишниками.

Мы их можем сравнивать, что дает нам возможность хранить их в упорядоченных ассоциативных контейнерах и, в целом, сравнивать 2 значения идентификаторов. Также мы можем взять хэш от thread::id, поэтому можем хранить их в неупорядоченных ассоциативных контейнерах. Также мы можем сериализовать эти айдишники в наследников std::basic_ostream ака писать в какие-то потоки (например stdout или файл).

Последнее явно нам намекает на логирование событий разных тредов. И это наверное самый популярный вариант их использования. Например, можно логировать скорости обработки запросов, какие-то thread-specific инкременты, cpu usage, возникающие ошибки и тд. Эти данные помогут при аналитике сервиса или при отладке.

Теперь про сравнения. Два различных потока должны иметь разные айдишники. Это поможет нам проверить, выполняется ли какая-то функция в нужном потоке. Например, мы создали какой-то класс и в его конструкторе сохраняем айди потока, в котором он был создан. Опять же, если у нас есть класс, который накапливает статистику по потоку, он не то чтобы должен быть thread-safe, его просто нельзя использовать в потоках, отличных от того, в котором объект был создан. Это будет некорректным поведением. Поэтому перед каждый сохранением статов можно проверять равенство std::this_thread::get_id() и сохраненного значения идентификатора.
Какие-нибудь асинхронные штуки, типа асинхронных клиентов, тоже могут быть сильно завязаны на конкретном потоке и для них тоже нужно постоянно проверять валидность текущего потока.

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

Хоть это и очень узкоспециализированные use case'ы, лучше знать, какие инструментны нужно использовать для решения таких задач.

Know the right tool. Stay cool.

#multitasking
Категории_выражений_и_move_семантика.pdf
5.4 MB
Гайд по категориям выражений и move семантике

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

#guide
Гайд по inline.pdf
377.2 KB
Гайд по inline

Аттракцион невиданной щедрости! Сразу два гайда подряд!

Недавно мы закончили основную серию постов про ключевое слово inline. Собственно, логично все это выделить в отдельный документ, чтобы вы могли все найти в одном месте и не прыгать по постам. Собственно, так и родился этот гайд. Собрали все посты, ответы на вопросы и замечания подписчиков, добавили плавные переходы между статьями, чтобы повествование было целостным. Ну и добавили больше объяснений и комментариев, вышло на 3-4 поста больше, чем просто компиляция статей с канала. Так, что забирайте, распространяйте и радуйтесь жизни!

Enjoy life. Stay cool.

#guide