Количество Островов
С первого наивного взгляда не очень понятно, как подступиться к задаче. Каким-то образом мы должны каждую встретившуюся единичку отнести к определенной группе и посчитать количество этих групп. Самый главный вопрос - как выявить отдельную группу?
На этот вопрос в принципе дан ответ - остров состоит из смежных клеток. То есть, встретив кусочек острова, мы можем пройти к любому другому кусочку, двигаясь только вверх-вниз и вправо-влево. Так формируется группа ячеек - остров.
Каким-то образом нам нужно запоминать маршрут, чтобы не приходить туда, где уже были и одновременно с этим приходить в самые удаленные части острова. Первое решается изменением 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
Просто генерируем числа
Обычно каждый раз, когда хочется просто сгенерировать число, приходится танцевать с бубном. Ну ладно не просто. А в плюсовом стиле. Для сишечки есть прекрасный генератор rand(). НО! Им неудобно пользоваться, если нужно число из определенного ренджа. И для нормальной рандомизации нужно еще писать srand(time(0)). И это ладно еще, можно запомнить(хотя у меня никогда не получалось). Но вот это
Простите, перебор. Может у кого-то и хватает мощностей мозга спокойно воспроизвести эту конструкцию, чтобы просто С@КА СГЕНЕРИРОВАТЬ ОДНО ЧИСЛО. Но у меня нет. Приходится каждый раз лезть в гугл в поисках того самого рабочего примера. Ставьте лайк, если мы с вами похожи.
Однако есть свет в конце тоннеля! Свет называется std::experimental::randint. Это шаблонная функция, которая принимает рендж чисел от и до. Важное уточнение, что uint8_t и int8_t не подходят для использования этим генератором. Ну и если первое число больше второго - поведение неопределено. Но это все неважно, потому что с этой функцией генерация целого числа происходит вот так:
ВСЁ. Это вот настолько просто, насколько и должно быть. Естественно, под капотом там тот же тред-локал инстанс std::uniform_int_distribution.
Эта фича дергается из <experimental/random> и доступна с 17 версии плюсов. Поэтому большинству из нас она будет доступна.
Cтоит также понимать все недостатки экспериментальных библиотек и не полагаться на них в критически важных местах. Не зря их назвали экспериментальными.
Однако приятно, что разработчики стандартной библиотеки учитывают боль своих пользователей и работают над удобством использования ее инструментов.
Make the world comfortable. Stay cool.
Обычно каждый раз, когда хочется просто сгенерировать число, приходится танцевать с бубном. Ну ладно не просто. А в плюсовом стиле. Для сишечки есть прекрасный генератор rand(). НО! Им неудобно пользоваться, если нужно число из определенного ренджа. И для нормальной рандомизации нужно еще писать srand(time(0)). И это ладно еще, можно запомнить(хотя у меня никогда не получалось). Но вот это
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution distrib(1, 6);
auto number = distrib(gen);
Простите, перебор. Может у кого-то и хватает мощностей мозга спокойно воспроизвести эту конструкцию, чтобы просто С@КА СГЕНЕРИРОВАТЬ ОДНО ЧИСЛО. Но у меня нет. Приходится каждый раз лезть в гугл в поисках того самого рабочего примера. Ставьте лайк, если мы с вами похожи.
Однако есть свет в конце тоннеля! Свет называется std::experimental::randint. Это шаблонная функция, которая принимает рендж чисел от и до. Важное уточнение, что uint8_t и int8_t не подходят для использования этим генератором. Ну и если первое число больше второго - поведение неопределено. Но это все неважно, потому что с этой функцией генерация целого числа происходит вот так:
int random_number = std::experimental::randint(100, 999);
ВСЁ. Это вот настолько просто, насколько и должно быть. Естественно, под капотом там тот же тред-локал инстанс std::uniform_int_distribution.
Эта фича дергается из <experimental/random> и доступна с 17 версии плюсов. Поэтому большинству из нас она будет доступна.
Cтоит также понимать все недостатки экспериментальных библиотек и не полагаться на них в критически важных местах. Не зря их назвали экспериментальными.
Однако приятно, что разработчики стандартной библиотеки учитывают боль своих пользователей и работают над удобством использования ее инструментов.
Make the world comfortable. Stay cool.
Варианты запуска потоков
В прошлом я рассказал, что можно запускать поток не только свободной функцией, но и методом какого-то объекта. Во время публикации этого поста у меня появилось ощущение, что со стороны не совсем понятно, как компилятор различает аргументы функции, саму функцию и объект, из метода которого потенциально поток может быть запущен. Поэтому сегодняшний пост по это.
В С++ есть такое понятие - callable objects. Это просто сущности, которые можно вызвать. Это функции, лямбды, функторы, методы и так далее. Как можно единообразно вызывать все эти сущности? Сходу в голову ничего не приходит, но в стандартной библиотеке начиная с С++17 есть такая функция std::invoke. Вот ее сигнатура:
template< class F, class... Args>
std::invokeresultt<F, Args...> invoke(F&& f, Args&&... args) noexcept
И существуют правила, по которым эта функция парсит свои аргументы. Но не только она, а все сущности, которые как бы "вовлекают" в работу callable объект. Открывайте окна, потому что щас немного духоты будет.
Пусть INVOKE(f, t1, t2, ..., tN) - выражение, обозначающее вовлечение в работу функционального объекта f. Тогда:
Если f - указатель на метод класса T:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в (t1.*f)(t2, ..., tN).
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в ((*t1).*f)(t2, ..., tN).
Если N=1 и f - указатель на член класса:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в t1.*f.
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в (*t1).*f.
Во всех других случая INVOKE разворачивается в обычный вызов функции f(t1, t2, ..., tN).
То есть конструктор std::thread, который внутри себя дергает std::invoke, на основе вот этих простых правил и решает, что ему на самом деле передали и как себя дальше вести.
Пройдитесь глазами по схеме выбора еще раз, чтобы лучше уложить ее себе в голову, потому что она не прям простая. Тут кстати есть отсылочка к моему давнему посту про вызов метода объекта через указатель. Там их целая серия, поэтому, если не смотрели, посмотрите. Получилось очень прикольно, на мой взгляд)
Так что создавайте потоки осознанно и так, как больше подходит именно в вашей ситуации. Потому что способов - тьма.
Stay versatile. Stay cool.
#multitasking
В прошлом я рассказал, что можно запускать поток не только свободной функцией, но и методом какого-то объекта. Во время публикации этого поста у меня появилось ощущение, что со стороны не совсем понятно, как компилятор различает аргументы функции, саму функцию и объект, из метода которого потенциально поток может быть запущен. Поэтому сегодняшний пост по это.
В С++ есть такое понятие - callable objects. Это просто сущности, которые можно вызвать. Это функции, лямбды, функторы, методы и так далее. Как можно единообразно вызывать все эти сущности? Сходу в голову ничего не приходит, но в стандартной библиотеке начиная с С++17 есть такая функция std::invoke. Вот ее сигнатура:
template< class F, class... Args>
std::invokeresultt<F, Args...> invoke(F&& f, Args&&... args) noexcept
И существуют правила, по которым эта функция парсит свои аргументы. Но не только она, а все сущности, которые как бы "вовлекают" в работу callable объект. Открывайте окна, потому что щас немного духоты будет.
Пусть INVOKE(f, t1, t2, ..., tN) - выражение, обозначающее вовлечение в работу функционального объекта f. Тогда:
Если f - указатель на метод класса T:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в (t1.*f)(t2, ..., tN).
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в ((*t1).*f)(t2, ..., tN).
Если N=1 и f - указатель на член класса:
t1 - объект типа Т, или ссылка на объект типа Т,
или ссылка на наследника типа Т, то этот INVOKE превращается в t1.*f.
t1 - ни что из перечисленного выше(то есть указатель на объект), то INVOKE превращается в (*t1).*f.
Во всех других случая INVOKE разворачивается в обычный вызов функции f(t1, t2, ..., tN).
То есть конструктор std::thread, который внутри себя дергает std::invoke, на основе вот этих простых правил и решает, что ему на самом деле передали и как себя дальше вести.
Пройдитесь глазами по схеме выбора еще раз, чтобы лучше уложить ее себе в голову, потому что она не прям простая. Тут кстати есть отсылочка к моему давнему посту про вызов метода объекта через указатель. Там их целая серия, поэтому, если не смотрели, посмотрите. Получилось очень прикольно, на мой взгляд)
Так что создавайте потоки осознанно и так, как больше подходит именно в вашей ситуации. Потому что способов - тьма.
Stay versatile. Stay cool.
#multitasking
Пример выстрела в лицо с помощью std::auto_ptr
Здесь мы рассмотрели проблемы std::auto_ptr. Проблемы понятные, но сегодня на практике разберем, как все может пойти не плану.
Прикол в том, что разработчики реализаций стандартной библиотеки понимали, люди так или иначе хотели пользоваться контейнерами, которые в себе содержали auto_ptr. Желание очевидное и поэтому разработчики делали такие реализации, которые сглаживают углы и косяки авто указателя. И в целом, хоть это и нестандарт, но с ними можно было работать.
Отчасти поэтому привести хороший пример того, как может выстрелить в ногу использование auto_ptr в контейнерах - задача не самая тривиальная. Но я вот попробую показать все эти интересности(см картинку).
Реализуем простенький класс с правилом нуля. Создадим вектор из авто указателей на объекты этого класса. Проинициализируем его.
И попробуем отсортировать. После вывода отсортированных элементов понимаем, что все сработало как надо и чиселки выстроились по порядку.
Однако теперь попробуем отсортировать с копирующей лямбдой. И вот тут-то мы и сегфолтнемся. При сортировке указатели будут копироваться в лямбду, то есть будут передавать туда владение ресурсом, а в контейнере будет оставаться фига. Поэтому при попытке снова получить доступ к полю ресурса мы наткнемся на нулевой указатель и упадем.
Да, сложно представить, что кто-то в проде напишет вторую лямбду(копирующую), но чем черт не шутит. Да и это лишь пример. Реальные ситуации могут быть сложные и неочевидные, поэтому будет легко попасться в ловушку. Это все-таки благосклонность компилятора, а не стандартная штука, на которую можно положиться в любых обстоятельствах.
Всем мув-семантики и непадающих программ.
Prevent your downfalls. Stay cool.
#compiler #STL #NONSTANDARD
Здесь мы рассмотрели проблемы std::auto_ptr. Проблемы понятные, но сегодня на практике разберем, как все может пойти не плану.
Прикол в том, что разработчики реализаций стандартной библиотеки понимали, люди так или иначе хотели пользоваться контейнерами, которые в себе содержали auto_ptr. Желание очевидное и поэтому разработчики делали такие реализации, которые сглаживают углы и косяки авто указателя. И в целом, хоть это и нестандарт, но с ними можно было работать.
Отчасти поэтому привести хороший пример того, как может выстрелить в ногу использование auto_ptr в контейнерах - задача не самая тривиальная. Но я вот попробую показать все эти интересности(см картинку).
Реализуем простенький класс с правилом нуля. Создадим вектор из авто указателей на объекты этого класса. Проинициализируем его.
И попробуем отсортировать. После вывода отсортированных элементов понимаем, что все сработало как надо и чиселки выстроились по порядку.
Однако теперь попробуем отсортировать с копирующей лямбдой. И вот тут-то мы и сегфолтнемся. При сортировке указатели будут копироваться в лямбду, то есть будут передавать туда владение ресурсом, а в контейнере будет оставаться фига. Поэтому при попытке снова получить доступ к полю ресурса мы наткнемся на нулевой указатель и упадем.
Да, сложно представить, что кто-то в проде напишет вторую лямбду(копирующую), но чем черт не шутит. Да и это лишь пример. Реальные ситуации могут быть сложные и неочевидные, поэтому будет легко попасться в ловушку. Это все-таки благосклонность компилятора, а не стандартная штука, на которую можно положиться в любых обстоятельствах.
Всем мув-семантики и непадающих программ.
Prevent your downfalls. Stay cool.
#compiler #STL #NONSTANDARD
Реальная ценность ссылок
Да и помните, как вводятся ссылки в учебных целях? Помню читал, что они нужны, чтобы внутри функции изменять переменную и эти изменения отразились в вызывающем блоке кода. И это удобнее указателя, потому что не нужно его разыменовывать. Все. С таким подходом естественно, что никто нихрена не понимает предназначения ссылок. Может только я так криво читал книжки. Это очень на меня похоже. Но раз я такой есть, значит есть и другие, похожие на меня. Поэтому этот пост для всех тех, кто читаешь книжки затылком, или просто новичков, которые не писали много кода.
Как бы функционал-то правильный, никто не спорит. С помощью ссылки в функции действительно можно проводить манипуляции с тем же самым объектом, что мы в нее передали и эти изменения отражаются на этом оригинальном объекте. На этом все основано. Но из этого поначалу довольно сложно сформулировать реальные кейсы использования ссылок. Собсна, погнали их разбирать.
У них есть 5 функции(ну или это я столько придумал)
1️⃣ Предотвращение лишнего копирования объекта при передаче в функцию. Ссылка - обертка над указателем, поэтому она занимает всего 4|8 байт и позволяет получить доступ к памяти, где находится объект. Опять же, наверняка в книжках объясняется, что ссылка - это обертка, но, как мне кажется, черезчур большой акцент делается на возможности изменения объекта, на который ссылается псевдоним. В очень многих ситуациях, когда в функции один из параметров - ссылка, она используется как read-only сущность. Тогда она помечена как const. Поэтому она лишь задает значение другим сущностям. А раз так, то нам в целом и нужен этот функционал изменения оригинального объекта и можно подумать, что в этом случае нужно по значению принимать аргумент. А вот нет. Тогда у нас будет дополнительное копирование. Нам такого не нужно. Мы общество без лишних копирований! Поэтому можно воспользоваться свойством, что ссылка - обертка над указателем. А значит мы можем с ее помощью без копирований задать значение другим объектам.
2️⃣ Output параметры. Уже ближе к способности изменения оригинального объекта. Иногда не хватает одного возвращаемого значения в функции, поэтому прибегают к использованию output параметров, чтобы функция передала нужную информацию через них. Есть конечно туплы или можно запилить отдельный класс, который в себе будет инкапсулировать нужные параметры, и возвращать его. Но туплы не очень информативные, так как у их элементов нет своих имен, а постоянно плодить сущности - не всегда удобно. Поэтому на помощь могут прийти output параметры. В этом случае они передаются по неконстантной ссылке. Просто в функцию передаются "пустые" объекты, то есть только что дефолтно созданные. И в этой функции на них нанизывается нужная информация. Которую мы потом может достать с помощью такой ссылочной семантики.
3️⃣ Изменение оригинального объекта. Эт вот та история, с которой я начал. Но! По моему опыту, это не самый популярный кейс использования ссылок. Попробую объяснить. Для кастомных классов очень часто приходится использовать std::shared_ptr, если время его жизни больше времени жизни скоупа. Тогда этот указатель везде передается по константной ссылке, хотя и объект, на который он указывает, можно изменять. Если нужно изменить какой-то объект, который создан на стеке, это нужно скорее делать через его собственные методы, а не сторонние функции. Так объект становится актором и проще воспринимать действия, которые происходят с ним происходят. Это вот та самая инкапсуляция.
4️⃣ Предоставление доступа к содержимому класса, без раскрытия приватных членов. Таким свойством обладает неконстантный operator[] для контейнеров STL. Вектору опасно предоставлять доступ к буфферу данных, где хранятся все объекты. Но более менее безопасно давать доступ к отдельным элементам для возможности их модификации. Это очень похожая на прошлый пример механика. Только в качестве текущего стейта выступает объект со своим содержимым, а модифицирующей функцией - текущая функция, в которой применяем operator[].
ПРОДОЛЖЕНИЕ В КОММЕНТАХ
#cppcore #goodpractice #design #STL
Да и помните, как вводятся ссылки в учебных целях? Помню читал, что они нужны, чтобы внутри функции изменять переменную и эти изменения отразились в вызывающем блоке кода. И это удобнее указателя, потому что не нужно его разыменовывать. Все. С таким подходом естественно, что никто нихрена не понимает предназначения ссылок. Может только я так криво читал книжки. Это очень на меня похоже. Но раз я такой есть, значит есть и другие, похожие на меня. Поэтому этот пост для всех тех, кто читаешь книжки затылком, или просто новичков, которые не писали много кода.
Как бы функционал-то правильный, никто не спорит. С помощью ссылки в функции действительно можно проводить манипуляции с тем же самым объектом, что мы в нее передали и эти изменения отражаются на этом оригинальном объекте. На этом все основано. Но из этого поначалу довольно сложно сформулировать реальные кейсы использования ссылок. Собсна, погнали их разбирать.
У них есть 5 функции(ну или это я столько придумал)
1️⃣ Предотвращение лишнего копирования объекта при передаче в функцию. Ссылка - обертка над указателем, поэтому она занимает всего 4|8 байт и позволяет получить доступ к памяти, где находится объект. Опять же, наверняка в книжках объясняется, что ссылка - это обертка, но, как мне кажется, черезчур большой акцент делается на возможности изменения объекта, на который ссылается псевдоним. В очень многих ситуациях, когда в функции один из параметров - ссылка, она используется как read-only сущность. Тогда она помечена как const. Поэтому она лишь задает значение другим сущностям. А раз так, то нам в целом и нужен этот функционал изменения оригинального объекта и можно подумать, что в этом случае нужно по значению принимать аргумент. А вот нет. Тогда у нас будет дополнительное копирование. Нам такого не нужно. Мы общество без лишних копирований! Поэтому можно воспользоваться свойством, что ссылка - обертка над указателем. А значит мы можем с ее помощью без копирований задать значение другим объектам.
2️⃣ Output параметры. Уже ближе к способности изменения оригинального объекта. Иногда не хватает одного возвращаемого значения в функции, поэтому прибегают к использованию output параметров, чтобы функция передала нужную информацию через них. Есть конечно туплы или можно запилить отдельный класс, который в себе будет инкапсулировать нужные параметры, и возвращать его. Но туплы не очень информативные, так как у их элементов нет своих имен, а постоянно плодить сущности - не всегда удобно. Поэтому на помощь могут прийти output параметры. В этом случае они передаются по неконстантной ссылке. Просто в функцию передаются "пустые" объекты, то есть только что дефолтно созданные. И в этой функции на них нанизывается нужная информация. Которую мы потом может достать с помощью такой ссылочной семантики.
3️⃣ Изменение оригинального объекта. Эт вот та история, с которой я начал. Но! По моему опыту, это не самый популярный кейс использования ссылок. Попробую объяснить. Для кастомных классов очень часто приходится использовать std::shared_ptr, если время его жизни больше времени жизни скоупа. Тогда этот указатель везде передается по константной ссылке, хотя и объект, на который он указывает, можно изменять. Если нужно изменить какой-то объект, который создан на стеке, это нужно скорее делать через его собственные методы, а не сторонние функции. Так объект становится актором и проще воспринимать действия, которые происходят с ним происходят. Это вот та самая инкапсуляция.
4️⃣ Предоставление доступа к содержимому класса, без раскрытия приватных членов. Таким свойством обладает неконстантный operator[] для контейнеров STL. Вектору опасно предоставлять доступ к буфферу данных, где хранятся все объекты. Но более менее безопасно давать доступ к отдельным элементам для возможности их модификации. Это очень похожая на прошлый пример механика. Только в качестве текущего стейта выступает объект со своим содержимым, а модифицирующей функцией - текущая функция, в которой применяем operator[].
ПРОДОЛЖЕНИЕ В КОММЕНТАХ
#cppcore #goodpractice #design #STL
Переворачиваем число
Сегодня у нас будет задачка. Формулируется просто - дано 32-битное целое число x. Верните число, десятичные цифры которого стоят в обратном порядке относительно числа x. Если будет переполнение, верните 0.
Решение, в целом, довольно очевидное. И скорее это задача для братьев наших меньших, кто еще не достиг дзена плюсового(ну хотя бы 23.87% от него). Однако здесь очень много нюансов, на которых вы можете споткнуться. Именно в этом и "сложность" задачи и ее польза. Поэтому нужно очень тщательно тестировать решение, чтобы оно было действительно корректным.
Но без одного уточнения решение будет совсем легким. Нельзя использовать переменные, которые занимают больше 4-х байтов в памяти.
Также у меня появились мысли по формату публикаций задач на канале. Под постом с непосредственно задачей будут идти обсуждения для тех, кто не очень понимает, в правильную ли сторону он мыслит, и хочет обсудит обсудить "большие штрихи" в решении.
Далее я буду отправлять следом за задачей еще один пост, в котором люди могут присылать свои решения на всеобщее ревью. Разделение нужно, чтобы собственно отделить сомневающихся и уже разобравшихся с деталями.
Ответ будет публиковаться вечером в комментах к еще одному посту. Это предотвратит явное появление ответа в ленте для тех, кто присоединится к каналу позже, и возможно тоже захочет порешать без спойлеров.
Погнали решать)
Challenge your problems. Stay cool.
#задачки
Сегодня у нас будет задачка. Формулируется просто - дано 32-битное целое число x. Верните число, десятичные цифры которого стоят в обратном порядке относительно числа x. Если будет переполнение, верните 0.
Решение, в целом, довольно очевидное. И скорее это задача для братьев наших меньших, кто еще не достиг дзена плюсового(ну хотя бы 23.87% от него). Однако здесь очень много нюансов, на которых вы можете споткнуться. Именно в этом и "сложность" задачи и ее польза. Поэтому нужно очень тщательно тестировать решение, чтобы оно было действительно корректным.
Но без одного уточнения решение будет совсем легким. Нельзя использовать переменные, которые занимают больше 4-х байтов в памяти.
Также у меня появились мысли по формату публикаций задач на канале. Под постом с непосредственно задачей будут идти обсуждения для тех, кто не очень понимает, в правильную ли сторону он мыслит, и хочет обсудит обсудить "большие штрихи" в решении.
Далее я буду отправлять следом за задачей еще один пост, в котором люди могут присылать свои решения на всеобщее ревью. Разделение нужно, чтобы собственно отделить сомневающихся и уже разобравшихся с деталями.
Ответ будет публиковаться вечером в комментах к еще одному посту. Это предотвратит явное появление ответа в ленте для тех, кто присоединится к каналу позже, и возможно тоже захочет порешать без спойлеров.
Погнали решать)
Challenge your problems. Stay cool.
#задачки
Защищенные методы vs защищенные поля
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
ООП - вещь занятная и многогранная. Почему-то пришла в голову аналогия с математикой: есть начальные заданные правила(принципы и аксиомы), благодаря которым выводятся весьма нетривиальные следствия. Сегодня поговорим о довольно базовом следствии, которое однако далеко не у всех в голове есть.
Есть у вас класс и вы хотите дать его наследникам и только им возможность изменять поля класса. И у нас для этого есть два подхода: объявить поля protected и дать возможность наследникам изменять их напрямую или ввести protected методы, которые определяют полный набор изменений этих полей. Какой вариант более предпочтительный и почему?
Сразу раскрою карты: лучше определять защищенный интерфейс вместо прямого доступа к полям. Для этого есть несколько причин:
💥 Как только член класса становится более "доступен", чем private, вы даете гарантии другим классам о том, как этот член будет себя вести. Точнее никаких гарантий вы не даете. Поскольку поле совершенно неконтролируемо, размещение его "в дикой природе" открывает вашему классу и классам, которые наследуют от вашего класса или взаимодействуют с ним, прекрасный вид на саванну, а точнее море ошибок. Нет никакого способа узнать, когда меняется поле, нет никакого способа контролировать, кто или что его меняет.
💥 Если вы хотите хоть что-то похожее на безопасное приложения, вам нужны всякого рода проверки . Иногда они могут занимать несколько строчек кода. И если ваш код зависит от какого-то состояния класса, то при каждом использовании защищенного поля, вам нужно проверять, все ли с ним в порядке, все ли валидно. Насколько же проще вынести все такие проверки в защищенный метод и закрыть у себя в голове этот гештальт.
💥Когда вы определяете защищенный интерфейс, вы автоматически ограничиваете наследников в использовании ваших приватных членов, которые могли бы быть защищенными. А ограничения в программировании - это хорошо! Чем меньше вы позволяете сделать непотребств со своим классом или использовать его вне предполагаемых сценариях использования - тем лучше!
💥 Это даже тестирование упрощает. Есть четкие сценарии поведения, которые можно проверить на корректность. Очень легко с такими исходными данными писать тесты. А легкость тестирования мотивирует его в принципе делать, а не класть на него лысину(ставь лайк, если понял game of words).
💥 Количество наследников у класса может быть много и все будут завязаны на самостоятельном управлении protected членами. А если в будущем обработка этих полей изменится, то необходимы будут изменения во всех наследниках. Защищенный интерфейс делает все такие изменения локализованными в родительском классе, то есть в одном месте. Это снижает стоимость внесения изменений.
Инкапсуляция - гениальная вещь. Придерживаясь этого принципа, вы защищайте свои данные непреднамеренного изменения и позволяете изменениям не распространяться за пределы одного класса.
Protect your secrets. Stay cool.
#OOP #goodpractice #design
shared_ptr и массивы
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
⚡️Использовать лямбду:
⚡️Ну или воспользоваться уже готовым вариантом:
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
shared_ptr<int[]> sp(new int[10]);
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
std::unique_ptr<int[]> up(new int[10]); // вызовется корректный delete[]
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
template <class T>
struct DynamicArrayWrapper {
DynamicArrayWrapper(size_t size) : ptr{new T[size]} {}
~DynamicArrayWrapper() {delete[] ptr;}
T * ptr;
};
std::shared_ptr<DynamicArrayWrapper> sp{10};
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
template< typename T >
struct array_deleter
{
void operator ()( T const * p)
{
delete[] p;
}
};
std::shared_ptr<int> sp(new int[10], array_deleter<int>());
⚡️Использовать лямбду:
std::shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
⚡️Ну или воспользоваться уже готовым вариантом:
std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory