Снова сравниваем решения задачи с поездами
Кто забыл или не в теме, вот эта задача. В прошлом мы уже сравнивали два решения: условный маятник и спидометр. Лучше освежить в памяти контекст, чтобы понятнее был предмет разговора. Однако наша подписчица @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амая популярная задачка с собеседований
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно…
Это вообще отдельный жанр в собеседованиях - логические задачки. Есть те, кто любит задавать их каждому кандидату. Но большинство обходит не используют их при найме людей. Оно и понятно, на собесах люди в довольно…
Линейное решение задачи с поездами
По поводу моих улучшений. Немного поразмыслив, первая моя мысль - что с маятником тоже можно сделать маленькое улучшение такое же, как и базовое улучшение спидометра. Мы не каждый раз поворачиваем и идем в обратную сторону, а только тогда, когда меняем состояние встретившейся лампочки.
Во всех особых случаях, когда все лампочки либо включены, либо выключены, такое улучшение работает за время х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
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
Сколько весит объект пустого класса?
Это знание не имеет практически никакого смысла. Как и большинство знаний в этом мире. Однако этот вопрос очень любят интервьюеры, а нормисы-программисты никогда может и не создавали пустой класс(пара практических применений у этого есть, но сейчас не об этом).
Самый очевидный и первый приходящий в голову ответ - 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
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
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
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
Количество островов
Начинаем давно напрашивающуюся рубрику на канале - #задачки. В рамках рубрики будем рассматривать решения абсолютно разных задач - и алгоритмических, и логических и математических, если попадутся интересные. На совсем интересные задачи будет отводиться много времени и постов, как например с поездами, где реально много всего можно пообсуждать. На остальные - кажется одного поста будет достаточно.
Но я так и не придумал универсального способа публикации решения. Чтобы и людям было что пообсуждать и не растягивать на 2 дня не самые сложные для решения задачи. Поэтому прошу вас оставить свое мнение под этим постом, как можно публиковать решение, учитывая необходимость для других пообсуждать что-то в комментах без спойлеров.
Если по сабжу. Задачка довольно популярная среди народа, хотя и ни разу мне не попадалась на собесах. Решение довольно тривиальное, но нужно иметь большую насмотреннсть и количество решенных алгоритмических задач, чтобы самостоятельно с нуля дойти до решения.
Суть. Дана бинарная матрица n x m для карта, где каждая ячейка репрезентует либо сушу(единичка), либо море (нолик). Нужно вернуть количество островов на карте.
Остров - суша, окруженная водой и сформирована из смежных ячеек суши по вертикали и горизонтали(не по диагонали!). Предполагается, что за пределами карты везде вода.
И еще напишите, не помешает ли вам наличие заблюренных комментов с решениям людей, которые не уверены в том, что решили самым оптимальным способом. Потому что было бы прикольно, чтобы такие люди скидывали свои решения на условное ревью. Пока этого не будет, но если особых возражений не будет, то введем такую возможность.
Stay cool.
Начинаем давно напрашивающуюся рубрику на канале - #задачки. В рамках рубрики будем рассматривать решения абсолютно разных задач - и алгоритмических, и логических и математических, если попадутся интересные. На совсем интересные задачи будет отводиться много времени и постов, как например с поездами, где реально много всего можно пообсуждать. На остальные - кажется одного поста будет достаточно.
Но я так и не придумал универсального способа публикации решения. Чтобы и людям было что пообсуждать и не растягивать на 2 дня не самые сложные для решения задачи. Поэтому прошу вас оставить свое мнение под этим постом, как можно публиковать решение, учитывая необходимость для других пообсуждать что-то в комментах без спойлеров.
Если по сабжу. Задачка довольно популярная среди народа, хотя и ни разу мне не попадалась на собесах. Решение довольно тривиальное, но нужно иметь большую насмотреннсть и количество решенных алгоритмических задач, чтобы самостоятельно с нуля дойти до решения.
Суть. Дана бинарная матрица n x m для карта, где каждая ячейка репрезентует либо сушу(единичка), либо море (нолик). Нужно вернуть количество островов на карте.
Остров - суша, окруженная водой и сформирована из смежных ячеек суши по вертикали и горизонтали(не по диагонали!). Предполагается, что за пределами карты везде вода.
И еще напишите, не помешает ли вам наличие заблюренных комментов с решениям людей, которые не уверены в том, что решили самым оптимальным способом. Потому что было бы прикольно, чтобы такие люди скидывали свои решения на условное ревью. Пока этого не будет, но если особых возражений не будет, то введем такую возможность.
Stay cool.
Количество Островов
С первого наивного взгляда не очень понятно, как подступиться к задаче. Каким-то образом мы должны каждую встретившуюся единичку отнести к определенной группе и посчитать количество этих групп. Самый главный вопрос - как выявить отдельную группу?
На этот вопрос в принципе дан ответ - остров состоит из смежных клеток. То есть, встретив кусочек острова, мы можем пройти к любому другому кусочку, двигаясь только вверх-вниз и вправо-влево. Так формируется группа ячеек - остров.
Каким-то образом нам нужно запоминать маршрут, чтобы не приходить туда, где уже были и одновременно с этим приходить в самые удаленные части острова. Первое решается изменением 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.
И еще раз про именование сущностей
В прошлых постах тут и тут мы говорили больше про чистоту и понятность кода. Здесь поговорим немного про безопасность.
Люди часто пишут код, описывая больше технические детали, а не абстракции и способы взаимодействия с ними. Типа если функция хочет обрабатывать токены слов и их частоту в тексте, люди напишут 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
Ловим исключения в constructor initializer list
Исключение в конструкторе может быть довольно противной вещью. Если исключение кинулось в конструкторе - объект считается не созданным и его деструктор не вызывается. Это может приводить к утечкам ресурсов и другим неприятностям. Здесь нам приходит на помощь конструкция try-catch, которая разруливает такие нештатные ситуации. А что, если у нас в конструкторе используется список инициализации? И исключение бросится в нем? Как в этом случае обезопасить создание объекта?
Я сам был в легком шоке, когда узнал ответ на эти вопросы. Кажется, что плюсы можно учить вечно)
Есть такая штука, как function-try-block. И выглядит она так:
Этот function-try-block связывает блок catch со всем телом конструктора и! списком иницализации. То есть если из тела конструктора или из базового конструктора или из любого конструктора для поля класса из списка инициализации будет выброшено исключение, то управление передается в единый блок catch.
Гарантируется, что перед входом в секцию catch будет уничтожены все полностью созданные поля и базы класса.
В принципе, раз это function-try-block, то он подходит к использованию в любых функциях, что увеличивает количество кейсов использования блока.
Очень важный момент, что при появлении исключения и перехода в блок catch, после завершения этого блока текущее исключение неявно пробрасывается дальше и его нужно ловить в вызывающем коде. В документации явно прописано, что главная цель function-try-block - ответить на исключение в списке инициализации с помощью логирования, модификации текущего объекта эксепшена, проброса другого исключения или завершения программы. Поэтому, хоть эту конструкцию можно использовать во всех функциях, она редко используется в других ситуациях.
В целом, такая альтернатива привычному блоку try-catch ощутимо увеличивает безопасность кода, особенно, если используются нетривиальные типы и списки инициализации. Да и просто смотрится необычно и свежо. И джунов можно попугать новыми страшными конструкциями)
Stay exploring. Stay cool.
#cppcore #exception
Исключение в конструкторе может быть довольно противной вещью. Если исключение кинулось в конструкторе - объект считается не созданным и его деструктор не вызывается. Это может приводить к утечкам ресурсов и другим неприятностям. Здесь нам приходит на помощь конструкция 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
В стародавние времена, когда еще мамонты ходили по земле, был выпущен стандарт С++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
В этом посте я верхнеуровнево обрисовал возможные применения ключевого слова 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
Когда в процессе может существовать несколько потоков(а сейчас обычно это условие выполняется), то очевидно, что эти потоки надо как-то идентифицировать. Они будут конкурировать друг с другом за ресурсы и планировщику нужно знать, кому именно нужно выдавать заветное процессорное время. И действительно, у каждого потока в системе есть свой уникальный номер(лучше сказать идентификатор). И мы, как прикладные разработчики, можем даже его получить. С появлением С++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
Не так-то и просто придумать практические кейсы использования 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
Не так давно была опубликована серия постов на тему категорий выражений, move семантики и оптимизаций. Мы подготовили гайд, который включает в себя весь материал статей по этой теме, вопросы подписчиков и ответы в одном документе!
#guide
Гайд по inline.pdf
377.2 KB
Гайд по inline
Аттракцион невиданной щедрости! Сразу два гайда подряд!
Недавно мы закончили основную серию постов про ключевое слово inline. Собственно, логично все это выделить в отдельный документ, чтобы вы могли все найти в одном месте и не прыгать по постам. Собственно, так и родился этот гайд. Собрали все посты, ответы на вопросы и замечания подписчиков, добавили плавные переходы между статьями, чтобы повествование было целостным. Ну и добавили больше объяснений и комментариев, вышло на 3-4 поста больше, чем просто компиляция статей с канала. Так, что забирайте, распространяйте и радуйтесь жизни!
Enjoy life. Stay cool.
#guide
Аттракцион невиданной щедрости! Сразу два гайда подряд!
Недавно мы закончили основную серию постов про ключевое слово inline. Собственно, логично все это выделить в отдельный документ, чтобы вы могли все найти в одном месте и не прыгать по постам. Собственно, так и родился этот гайд. Собрали все посты, ответы на вопросы и замечания подписчиков, добавили плавные переходы между статьями, чтобы повествование было целостным. Ну и добавили больше объяснений и комментариев, вышло на 3-4 поста больше, чем просто компиляция статей с канала. Так, что забирайте, распространяйте и радуйтесь жизни!
Enjoy life. Stay cool.
#guide
Уникален ли std::thread::id среди всех процессов?
std::thread::id используется как уникальный идентификатор потока внутри вашего приложения. Но тут возникает интересный вопрос. А вот допустим я запустил 2 инстанса моего многопоточного приложения. Могу ли я гаранировать, что айди потоков будут уникальны между двумя инстансами? Например, я хочу какую-то общую для всех инстансов логику сделать, основанную на идентификаторах потоков. Могу ли я положиться на их уникальность? Или даже более общий вопрос: уникален ли std::thread::id среди всех процессов в системе?
Начнем с того, что стандарт С++ ничего не знает про процессы. Точно так же, как и до С++11, стандарт ничего не знал про потоки. У нас нет никаких стандартных инструментов(syscall - это не плюсовый инструмент) для работы с процессами, их запуском или для общения между процессами. И раз это не специфицировано стандратном, мы не можем ничего гарантировать. Потому что никаких гарантий и нет. Единственная гарантия стандарта относительно std::thread::id - идентификаторы - уникальны для каждого потока выполнения и могут переиспользоваться из уничтоженных потоков. Но давайте посмотрим немного глубже, на основу std::thread. Возможно там мы найдем ответ.
И тут есть 2 основных варианта. Для unix-подобных систем std::thread реализован на основе pthreads. Для виндовса это будет Windows Thread API.
В доках pthreads написано: "Thread IDs are guaranteed to be unique only within a process". Так что на юникс системах идентификатор потока уникален только в пределах одного процесса и может повторяться в разных процессах.
А вот доках Win32 API написано следующее: "Until the thread terminates, the thread identifier uniquely identifies the thread throughout the system". Оказывается, что на винде айди потока уникален среди всех процессов. Эти айдишники выдаются из одного пула, поэтому их значения синхронизированы сквозь все процессы.
Как всегда, вы вольны выбирать между кроссплатформеностью и возможностью использовать нужные особенности конкретной системы. Но интересно, что у двух систем такие разные подходы к этому вопросу.
Stay unique all over the world. Stay cool.
#cpp11 #cppcore #OS #multitasking
std::thread::id используется как уникальный идентификатор потока внутри вашего приложения. Но тут возникает интересный вопрос. А вот допустим я запустил 2 инстанса моего многопоточного приложения. Могу ли я гаранировать, что айди потоков будут уникальны между двумя инстансами? Например, я хочу какую-то общую для всех инстансов логику сделать, основанную на идентификаторах потоков. Могу ли я положиться на их уникальность? Или даже более общий вопрос: уникален ли std::thread::id среди всех процессов в системе?
Начнем с того, что стандарт С++ ничего не знает про процессы. Точно так же, как и до С++11, стандарт ничего не знал про потоки. У нас нет никаких стандартных инструментов(syscall - это не плюсовый инструмент) для работы с процессами, их запуском или для общения между процессами. И раз это не специфицировано стандратном, мы не можем ничего гарантировать. Потому что никаких гарантий и нет. Единственная гарантия стандарта относительно std::thread::id - идентификаторы - уникальны для каждого потока выполнения и могут переиспользоваться из уничтоженных потоков. Но давайте посмотрим немного глубже, на основу std::thread. Возможно там мы найдем ответ.
И тут есть 2 основных варианта. Для unix-подобных систем std::thread реализован на основе pthreads. Для виндовса это будет Windows Thread API.
В доках pthreads написано: "Thread IDs are guaranteed to be unique only within a process". Так что на юникс системах идентификатор потока уникален только в пределах одного процесса и может повторяться в разных процессах.
А вот доках Win32 API написано следующее: "Until the thread terminates, the thread identifier uniquely identifies the thread throughout the system". Оказывается, что на винде айди потока уникален среди всех процессов. Эти айдишники выдаются из одного пула, поэтому их значения синхронизированы сквозь все процессы.
Как всегда, вы вольны выбирать между кроссплатформеностью и возможностью использовать нужные особенности конкретной системы. Но интересно, что у двух систем такие разные подходы к этому вопросу.
Stay unique all over the world. Stay cool.
#cpp11 #cppcore #OS #multitasking
Что нужно возвращать из оператора присваивания?
Пост скорее для новичков, но менее интересным он от этого не становится. В основном все пишут, как принято. Но естественно, у каждой самой маленькой детали под собой есть основание.
Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 2 особенных члена классов - копирующий и перемещающий операторы присваивания. Первый нужен, что скопировать содержимое одного объекта в другой и по договоренности обычно принимает константную lvalue ссылку на объект того же класса. Второй нужен для перемещения ресурсов из одного объекта в другой и принимает rvalue reference на объект того же класса.
Ну вот скопировали или переместили мы ресурсы. Как и любая функция, этот оператор должен что-то возвращать. И тут есть несколько вариантов: ничего не возвращать(void), возвращать в каком-то виде объект вообще другого класса, возвращать объект по значению, по неконстантной ссылке, по константной ссылке и по rvalue ссылке. Довольно много вариантов, поэтому будем отметать их в порядке очевидной непригодности.
Во всем посте дальше я буду писать про копирующий оператор, чтобы не распыляться, но несложно будет перейти к логике перемещающего оператора. Но в начале разберемся с семантикой. Я, как пользователь класса, ожидаю, что объект из которого я копирую не изменится, после выполнения оператора два объекта будут семантически идентичны, и чтобы я с объектом не сделал внутри одной строчки, он не потеряет свое новоприобретенное значение(если я явно не вызову std::move). С этим определились, поехали разбирать кейсы.
1️⃣ Можем вернуть объект вообще другого класса. Семантика такого решения будет самой неочевидной для пользователей класса. И хоть я могу представить что-то подобное для других операторов, например, если вычесть друг из друга 2 даты, то получим какое-то число, а не дату. Но для оператора присваивания я даже кейса никакого не могу привести, поэтому сразу скипаем этот вариант.
2️⃣ Можем вернуть объект по rvalue reference. В этом случае я могу потерять новоприобретенные ресурсы, если передам результат оператора в функцию, которая принимает аргумент по значению или по rvalue reference.
Это нарушает ожидаемую семантику поведения копирующего оператора, поэтому тоже не подходит.
3️⃣ Можем вернуть объект по значению. Основной и решающий, на мой взгляд, недостаток - это снижение производительности на лишнее копирование объекта и его удаление.
В итоге мы видим 2 лишних копирования и 2 лишних вызова деструктора. Этого достаточно, чтобы отбросить вариант. Лишних я говорю, потому что знаю просто, что их можно избежать. Просто даже по виду выражения c = b = a; видно, что тут просто 2 присваивания. И я не ожидаю, что вылезет еще какое-то копирование. Если неосознанно пользоваться таким оператором, то можно снижать перфоманс приложения, даже не осознавая этого.
Остальные пункты в один пост не влезет. Придется выделить во вторую часть.
Stay conscious about small things. Stay cool.
#cppcore
Пост скорее для новичков, но менее интересным он от этого не становится. В основном все пишут, как принято. Но естественно, у каждой самой маленькой детали под собой есть основание.
Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 2 особенных члена классов - копирующий и перемещающий операторы присваивания. Первый нужен, что скопировать содержимое одного объекта в другой и по договоренности обычно принимает константную lvalue ссылку на объект того же класса. Второй нужен для перемещения ресурсов из одного объекта в другой и принимает rvalue reference на объект того же класса.
Ну вот скопировали или переместили мы ресурсы. Как и любая функция, этот оператор должен что-то возвращать. И тут есть несколько вариантов: ничего не возвращать(void), возвращать в каком-то виде объект вообще другого класса, возвращать объект по значению, по неконстантной ссылке, по константной ссылке и по rvalue ссылке. Довольно много вариантов, поэтому будем отметать их в порядке очевидной непригодности.
Во всем посте дальше я буду писать про копирующий оператор, чтобы не распыляться, но несложно будет перейти к логике перемещающего оператора. Но в начале разберемся с семантикой. Я, как пользователь класса, ожидаю, что объект из которого я копирую не изменится, после выполнения оператора два объекта будут семантически идентичны, и чтобы я с объектом не сделал внутри одной строчки, он не потеряет свое новоприобретенное значение(если я явно не вызову std::move). С этим определились, поехали разбирать кейсы.
1️⃣ Можем вернуть объект вообще другого класса. Семантика такого решения будет самой неочевидной для пользователей класса. И хоть я могу представить что-то подобное для других операторов, например, если вычесть друг из друга 2 даты, то получим какое-то число, а не дату. Но для оператора присваивания я даже кейса никакого не могу привести, поэтому сразу скипаем этот вариант.
2️⃣ Можем вернуть объект по rvalue reference. В этом случае я могу потерять новоприобретенные ресурсы, если передам результат оператора в функцию, которая принимает аргумент по значению или по rvalue reference.
struct Class {
Class(int num) : a{num} {std::cout << "Ctor" << "n";}
Class&& operator=(const B& other) { a = other.a; return std::move(*this);}
Class(Class&& other) {a = other.a; other.a = 0;}
int a;
};
void func(Class obj) {
std::cout << obj.a << "n";
}
int main() {
Class a{2}, b{3};
func(b = a);
std::cout << b.a << "n";
}
//OUTPUT:
Ctor
Ctor
2
0
Это нарушает ожидаемую семантику поведения копирующего оператора, поэтому тоже не подходит.
3️⃣ Можем вернуть объект по значению. Основной и решающий, на мой взгляд, недостаток - это снижение производительности на лишнее копирование объекта и его удаление.
struct Class {
Class(int num) : a{num} {std::cout << "Ctor" << "n";}
Class(const Class& other) {a = other.a; std::cout << "Copy Ctor" << "n";}
Class operator=(const Class& other) { a = other.a; std::cout << "Copy Assign" << "n"; return *this;}
Class(Class&& other) {a = other.a; other.a = 0;}
~Class() {std::cout << "Dtor" << "n";}
int a;
};
int main() {
Class a{2}, b{3}, c{4};
c = b = a;
std::cout << a.a << " " << b.a << " " << c.a << "n";
}
//OUTPUT:
Ctor
Ctor
Ctor
Copy Assign
Copy Ctor
Copy Assign
Copy Ctor
Dtor
Dtor
2 2 2
Dtor
Dtor
Dtor
В итоге мы видим 2 лишних копирования и 2 лишних вызова деструктора. Этого достаточно, чтобы отбросить вариант. Лишних я говорю, потому что знаю просто, что их можно избежать. Просто даже по виду выражения c = b = a; видно, что тут просто 2 присваивания. И я не ожидаю, что вылезет еще какое-то копирование. Если неосознанно пользоваться таким оператором, то можно снижать перфоманс приложения, даже не осознавая этого.
Остальные пункты в один пост не влезет. Придется выделить во вторую часть.
Stay conscious about small things. Stay cool.
#cppcore
Что нужно возвращать из оператора присваивания? Ч2
В прошлом посте мы поговорили о том, что точно не нужно возвращать из оператора присваивания, потому что это нарушает его семантику. У нас остались для рассмотрения 3 варианта: void, неконстантная ссылка и константная ссылка. Их всех корректно использовать, только какие-то будут накладывать определенные ограничения.
Самый банальный пример - цепочка присваивания. Это конструкция вида: a = b = c;. Если честно, то никогда такой штукой не пользовался. Однако надо учитывать, что кому-то могут понадобиться такие конструкции. Очевидно, что если оператор присваивания будет возвращать void, цепочкой присваиваний нельзя будет пользоваться для такого класса.
Ну или может быть вы хотите присвоить значение объекту и потом для конкретно этого объекта вызвать метод. Например так:
Если метод вернет void, то такая штука просто не скомпилируется. Это кстати довольно удобно делать в условиях. Типа того:
Мы хотим использовать результат SomeFunc дальше по коду, поэтому хотим присвоить его именованной переменной. Но и хотим сразу проверить, валидный ли объект. Эти операции удобно делать сразу налету в условии. А если оператор вернет void, то этим удобным инструментом нельзя будет пользоваться. Конечно, этот пример скорее для перемещающего присваивания, но суть та же.
Теперь осталось разобраться с константностью. На самом деле даже далеко уходить не надо. Возьмем кейс с условием. В случае константной ссылки мы не сможем выполнять неконстантные методы, которые собственно и нельзя вызывать у константных ссылок. Потому что подразумевается, что константная ссылка - read-only сущность и нам запрещено через нее изменять объект, на который указывает ссылка. Это несколько ограничивает наши возможности.
Ну и остается только неконстантная ссылка. У нее нет side эффектов семантически и она позволяет выполнять все операции, которые нам были недоступны выше.
Однако помимо всего прочего есть еще одна причина делать возвращаемое значение неконстантной ссылкой. Такую форму и семантику реализуют операторы присваивания для тривиальных типов. Все мы в нашем плюсовом юношестве наигрались с интами и флотами и просто на интуитивном уровне понимаем, как для них все работает. И очень круто, когда пользовательский класс перенимает такое же поведение. Потому что пользователю будет намного проще работать с таким классом. Меньше времени на понимание работы с объектом и памяти на сохранение информации о нем - больше производительность программиста. Это конвертируется в денежку. Всем это выгодно.
Поэтому без особой надобности не изменяйте этот де-факто стандартный вид возвращаемого значения. Вы сможете юзать весь объем возможностей использования класса и сделаете его проще для понимания.
Use all of your abilities. Stay cool.
#cppcore
В прошлом посте мы поговорили о том, что точно не нужно возвращать из оператора присваивания, потому что это нарушает его семантику. У нас остались для рассмотрения 3 варианта: void, неконстантная ссылка и константная ссылка. Их всех корректно использовать, только какие-то будут накладывать определенные ограничения.
Самый банальный пример - цепочка присваивания. Это конструкция вида: a = b = c;. Если честно, то никогда такой штукой не пользовался. Однако надо учитывать, что кому-то могут понадобиться такие конструкции. Очевидно, что если оператор присваивания будет возвращать void, цепочкой присваиваний нельзя будет пользоваться для такого класса.
Ну или может быть вы хотите присвоить значение объекту и потом для конкретно этого объекта вызвать метод. Например так:
(obj = temp_obj).foo();
Если метод вернет void, то такая штука просто не скомпилируется. Это кстати довольно удобно делать в условиях. Типа того:
if ((obj = SomeFunc()).isValid()) {...}
Мы хотим использовать результат SomeFunc дальше по коду, поэтому хотим присвоить его именованной переменной. Но и хотим сразу проверить, валидный ли объект. Эти операции удобно делать сразу налету в условии. А если оператор вернет void, то этим удобным инструментом нельзя будет пользоваться. Конечно, этот пример скорее для перемещающего присваивания, но суть та же.
Теперь осталось разобраться с константностью. На самом деле даже далеко уходить не надо. Возьмем кейс с условием. В случае константной ссылки мы не сможем выполнять неконстантные методы, которые собственно и нельзя вызывать у константных ссылок. Потому что подразумевается, что константная ссылка - read-only сущность и нам запрещено через нее изменять объект, на который указывает ссылка. Это несколько ограничивает наши возможности.
Ну и остается только неконстантная ссылка. У нее нет side эффектов семантически и она позволяет выполнять все операции, которые нам были недоступны выше.
Однако помимо всего прочего есть еще одна причина делать возвращаемое значение неконстантной ссылкой. Такую форму и семантику реализуют операторы присваивания для тривиальных типов. Все мы в нашем плюсовом юношестве наигрались с интами и флотами и просто на интуитивном уровне понимаем, как для них все работает. И очень круто, когда пользовательский класс перенимает такое же поведение. Потому что пользователю будет намного проще работать с таким классом. Меньше времени на понимание работы с объектом и памяти на сохранение информации о нем - больше производительность программиста. Это конвертируется в денежку. Всем это выгодно.
Поэтому без особой надобности не изменяйте этот де-факто стандартный вид возвращаемого значения. Вы сможете юзать весь объем возможностей использования класса и сделаете его проще для понимания.
Use all of your abilities. Stay cool.
#cppcore