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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Сколько весит объект пустого класса?

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

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

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

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

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

Stay reasonable. Stay cool.

#interview #cppcore
anonymous namespace

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

namespace {
int int_var = 0;
void foo() {...}
}


Как же можно получить доступ к содержимому этого неймспейса, если у него нет имени?

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

namespace unique{}
using namespace unique;
namespace unique{
int int_var = 0;
void foo() {...}
}


Будем разбирать по порядку, потому что здесь все непросто.

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

Во-вторых, все содержимое безымянных пространств видно из родительского пространства за счет дирекивы using. Благодаря этому мы можем пользоваться всеми членами unnamed namespace, как если бы они были в текущем неймспейсе.

В-третьих, на каждую единицу трансляции будет уникальное имя unique, которое больше никому не будет известно. Это значит, что ни одна другая единица трансляции не сможет получить доступ к intvar и foo, потому что не будет знать это уникальное имя.

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

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

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

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

Don't expose your secrets. Stay cool.

#cppcore #cpp11 #compiler
anonymous namespace vs static

Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом static, поэтому сегодня обсудим, какие между ними различия. Поехали!

1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.

А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.

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

2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).

3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.

4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.

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

Например

template <int const& Size>
class test {};

static int Size1 = 10;

namespace {
int Size2 = 10;
}

test<Size1> t1; // ERROR!!!
test<Size2> t2;


В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.

Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.

Также круто скрывать определения классов от ненужных глаз.

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

В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)

Use proper tools. Stay cool.

#cppcore #cpp17 #design
Anonymous namespace в хэдэрах

Безымянные пространства имен звучат, как хорошая альтернатива static. Была даже тема, что в С++11 задеприкейтили static для свободных функций и переменных в угоду замены на anonymous namespace. Однако от этой идеи отказались, чтобы сильно с сишечкой не расходиться.

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

Открываем С++ Core Guidelines и видим, что там написано "не используйте анонимные пространства имен в заголовочных файлах". Эх, а так хотелось...

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

1️⃣ Раздувается бинарный файл. Во всех единицах трансляции, куда включен ваш хэдэр, будут содержаться копии сущностей из анонимного пространства имен. На копии тратится память и, ну вы поняли...

2️⃣ Вы рискуете нарваться на неопределенное поведение. Рассмотрим такой пример:

namespace {
double const pi = 3.14159;
}

inline double twoPiR( double r ) { return 2.0 * pi * r; }


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

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

3️⃣ Внутри одной единицы трансляции все анонимные неймспейсы на самом деле являются одним пространством с уникальным для юнита именем. Это значит, если в двух заголовочниках у вас будут такие строчки:

namespace {
int x;
}


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

Сложно контролировать имена объектов и функций, которые мы располагаем в заголовочниках, чтобы они не пересекались с именами у других хэдэрах. Поэтому, если в какой-то момент вы будете вынуждены подключить два заголовочника(код которых уже где-то используется) с пересекающимися именами в своих anonymous namespace в один цппшник, то будут проблемки. Придется переименовывать какую-то половину сущностей во всем проекте, где они использовались.

До С++17 у нас особо не было универсального и удобного способа распространять код только через хэдэра. Теперь же есть inline переменные. И мы можем определять внешнесвязные сущности в хэдэрах. Это полностью решило все перечисленные здесь проблемы.

Поэтому используйте inline функции и переменные в хэдэрах и радуйтесь жизни!

Enjoy life. Stay cool.

#cpp11 #cpp17 #compiler #cppcore
Количество островов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Stay safe. Stay cool.

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

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

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

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


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

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

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

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

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

Stay exploring. Stay cool.

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

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

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

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

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

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

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

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

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

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

Fix your mistakes. Stay cool.

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

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

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

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

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

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

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

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

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

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

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

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

See all sides of the coin. Stay cool.

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

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

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

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

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


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

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

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

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

Identify yourself. Stay cool.

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

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

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

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

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

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

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

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

Know the right tool. Stay cool.

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

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

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

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

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

Enjoy life. Stay cool.

#guide
Уникален ли 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
Что нужно возвращать из оператора присваивания?

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

Коротенький рекап. Операторы присваивания нужны для перенятия свойств других объектов. В общем случае даже объектов других классов. Но нас интересуют 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, цепочкой присваиваний нельзя будет пользоваться для такого класса.

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

(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::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
Пример выстрела в лицо с помощью std::auto_ptr

Здесь мы рассмотрели проблемы 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