Что не нарушает ABI класса?
В комментах к предыдущему посту @MayerArtur удачно ванганул тему поста, который я писал в момент публикации его коммента. Поэтому этот пост обязан был выйти сегодня 😁. Вчера мы поговорили о том, что делать нельзя, если мы хотим сохранить стабильный ABI. Сегодня коротко пройдемся по тому, что делать можно. Завершим, так сказать, тему с ABI, чтобы картинка полная у вас была. Тут будет все в перемешку: и для хедеров, и для файлов реализации. Поехали:
✅ Изменять реализацию метода. Довольно очевидно. Если не трогать сигнатуру, то внутри можно хоть хороводы водить с гусями, ничего для внешнего наблюдателя не изменится.
✅ Добавлять новые публичные невиртуальные методы. Это новая функциональность, которая никак не мешает находить и пользоваться старой.
✅ Добавлять новые конструкторы. Same thing. Ничто не помешает создать объект старым способом.
✅ Добавлять новые енамы в класс.
✅ Добавлять новые поля в существующие енамы. Сишные енамы - это всего лишь красивенькие цифры, памяти они не занимают и компилятор их встраивает в код в виде обычных чисел.
✅ Добавление новых статических полей и изменение их порядка. Дело в том, что статические поля аллоцируются в области глобальных данных и никак не влияют на репрезентацию объекта в памяти.
✅ Добавлять новые классы и функции в файл. Obviously.
✅ Изменять параметр по умолчанию. Это тоже ничего не меняет, можно пользоваться как и прежде, но перекомпиляция нужна, чтобы новый параметр встал на место старого.
🥴 Чет есть какая-то инфа по поводу того, что удаление приватных невиртуальных методов может не сломать ABI, если они не вызываются и никогда не вызывались никакими инлайн мембер-функциями. Но учитывая, что в стандарте написано, что любые изменения в приватных полях и методах ведут к перекомпиляции, а также, что компилятор может встраивать методы без вашего на то разрешения, я бы это не брал в расчет.
Как-то так. Не густо. Но и не пусто.
На этом, думаю, тему завершаем на какое-то время разговор про бинарный интерфейс. Верхнеуровнево затронули самые важные моменты. Возможно в будущем будем возвращаться к отдельным деталям и прорабатывать их.
Stay compatible. Stay cool.
#design #hardcore #cppcore
В комментах к предыдущему посту @MayerArtur удачно ванганул тему поста, который я писал в момент публикации его коммента. Поэтому этот пост обязан был выйти сегодня 😁. Вчера мы поговорили о том, что делать нельзя, если мы хотим сохранить стабильный ABI. Сегодня коротко пройдемся по тому, что делать можно. Завершим, так сказать, тему с ABI, чтобы картинка полная у вас была. Тут будет все в перемешку: и для хедеров, и для файлов реализации. Поехали:
✅ Изменять реализацию метода. Довольно очевидно. Если не трогать сигнатуру, то внутри можно хоть хороводы водить с гусями, ничего для внешнего наблюдателя не изменится.
✅ Добавлять новые публичные невиртуальные методы. Это новая функциональность, которая никак не мешает находить и пользоваться старой.
✅ Добавлять новые конструкторы. Same thing. Ничто не помешает создать объект старым способом.
✅ Добавлять новые енамы в класс.
✅ Добавлять новые поля в существующие енамы. Сишные енамы - это всего лишь красивенькие цифры, памяти они не занимают и компилятор их встраивает в код в виде обычных чисел.
✅ Добавление новых статических полей и изменение их порядка. Дело в том, что статические поля аллоцируются в области глобальных данных и никак не влияют на репрезентацию объекта в памяти.
✅ Добавлять новые классы и функции в файл. Obviously.
✅ Изменять параметр по умолчанию. Это тоже ничего не меняет, можно пользоваться как и прежде, но перекомпиляция нужна, чтобы новый параметр встал на место старого.
🥴 Чет есть какая-то инфа по поводу того, что удаление приватных невиртуальных методов может не сломать ABI, если они не вызываются и никогда не вызывались никакими инлайн мембер-функциями. Но учитывая, что в стандарте написано, что любые изменения в приватных полях и методах ведут к перекомпиляции, а также, что компилятор может встраивать методы без вашего на то разрешения, я бы это не брал в расчет.
Как-то так. Не густо. Но и не пусто.
На этом, думаю, тему завершаем на какое-то время разговор про бинарный интерфейс. Верхнеуровнево затронули самые важные моменты. Возможно в будущем будем возвращаться к отдельным деталям и прорабатывать их.
Stay compatible. Stay cool.
#design #hardcore #cppcore
Базовая формулировка Pimpl Idiom
Пускай у нас есть класс, который выполняет определенную фильтрацию изображения. Для определенности положим, что это фильтр удаления шумов. Для этого нам нужен будет видимый класс NoiseReductionFilter для использования функциональности фильтра, и класс имплементации NoiseReductionFilterImpl, который будет инкапсулировать конкретную реализацию фильтра. Зачем нам вообще нужно такое разделение? Этим классом будет пользоваться потенциально много народу, поэтому мы не хотим раскрывать хоть какие-нибудь детали реализации, чтобы люди не делали своих предположений о реализации и не делали опасных низкоуровневых трюков. Это может навредить нашей интеллектуальной собственности или репутации проекта, если его будут неправильно использовать. Причина немного надутая, но большие проекты просто обязаны заботиться о таких вещах. Окей пишем(осторожно псевдокод):
В чем проблема этого кода? Для начала, мы тут вообще ничего не скрыли. Тип NoiseReductionFilterImpl должен быть определен в момент компиляции и определен он в известном файле NoiseReductionFilterImpl.hpp, к котором все, кому ни попадя имеют доступ. Ни о какой конфиденциальности речи быть не может.
Image processing - очень быстро развивающаяся область. Сейчас все больше проектов переходят с консервативных методов к использованию нейросетей. Поэтому, очевидно, что этот класс тоже будет довольно активно развиваться. А мы хотим поддерживать ABI совместимость с проектами пользователей и не ломать их проекты своими новыми версиями. В данном случае такого не получится сделать, потому что очень большой перечень изменений может сломать ABI и реально поменять реализацию без поломки бинарной совместимости невозможно.
Какое здесь может быть решение проблемы?
Убрать подключение NoiseReductionFilterImpl.hpp и сделать закрытый член класса указателем на тип NoiseReductionFilterImpl. Но раз мы убрали заголовочник с объявлением типа, тогда мы не можем использовать указатель на этот тип. Или можем?
Еще как можем. Есть такое понятие, как forward declaration. Мы можем сказать компилятору, что есть вот такой класс NoiseReductionFilterImpl и мы даём слово, что опишем и определим его, но пока не скажем тебе где. И тогда мы можем объявить приватный член класса, как указатель на NoiseReductionFilterImpl, но никак не использовать его. И нам это сойдет с рук. Компилятор потом сам отыщет определение этого класса и удачно разрезолвит все символы. Сейчас покажу, как это будет выглядеть.
Скажу сразу, этот код нельзя использовать как он есть здесь. Он просто, чтобы показать концепцию, и не использует необходимых фичей новых стандартов. Что здесь происходит. Мы вынесли определение в файл реализации. А значит никто, кроме нас не сможет увидеть или даже намек почуять, как реализована функциональность (не берем в расчет реверс-инженеров). Это будут просто инструкции, которые будут подгружаться во время выполнения кода клиента.
Обычно файл NoiseReductionFilter.hpp не меняется, так как это публичное апи, а указатель на реализацию дает нам возможность вертеть имплементацией как нам хочется.
Но есть и негативные последствия использования идиомы.
Продолжение в комментах…
#cppcore #design #howitworks #memory
Пускай у нас есть класс, который выполняет определенную фильтрацию изображения. Для определенности положим, что это фильтр удаления шумов. Для этого нам нужен будет видимый класс NoiseReductionFilter для использования функциональности фильтра, и класс имплементации NoiseReductionFilterImpl, который будет инкапсулировать конкретную реализацию фильтра. Зачем нам вообще нужно такое разделение? Этим классом будет пользоваться потенциально много народу, поэтому мы не хотим раскрывать хоть какие-нибудь детали реализации, чтобы люди не делали своих предположений о реализации и не делали опасных низкоуровневых трюков. Это может навредить нашей интеллектуальной собственности или репутации проекта, если его будут неправильно использовать. Причина немного надутая, но большие проекты просто обязаны заботиться о таких вещах. Окей пишем(осторожно псевдокод):
// NoiseReductionFilter.hpp
#include "NoiseReductionFilterImpl.hpp"
struct NoiseReductionFilter {
Filter();
private:
NoiseReductionFilterImpl impl;
};
// NoiseReductionFilter.cpp
NoiseReductionFilter::Filter() {
impl.FilterImpl();
}
В чем проблема этого кода? Для начала, мы тут вообще ничего не скрыли. Тип NoiseReductionFilterImpl должен быть определен в момент компиляции и определен он в известном файле NoiseReductionFilterImpl.hpp, к котором все, кому ни попадя имеют доступ. Ни о какой конфиденциальности речи быть не может.
Image processing - очень быстро развивающаяся область. Сейчас все больше проектов переходят с консервативных методов к использованию нейросетей. Поэтому, очевидно, что этот класс тоже будет довольно активно развиваться. А мы хотим поддерживать ABI совместимость с проектами пользователей и не ломать их проекты своими новыми версиями. В данном случае такого не получится сделать, потому что очень большой перечень изменений может сломать ABI и реально поменять реализацию без поломки бинарной совместимости невозможно.
Какое здесь может быть решение проблемы?
Убрать подключение NoiseReductionFilterImpl.hpp и сделать закрытый член класса указателем на тип NoiseReductionFilterImpl. Но раз мы убрали заголовочник с объявлением типа, тогда мы не можем использовать указатель на этот тип. Или можем?
Еще как можем. Есть такое понятие, как forward declaration. Мы можем сказать компилятору, что есть вот такой класс NoiseReductionFilterImpl и мы даём слово, что опишем и определим его, но пока не скажем тебе где. И тогда мы можем объявить приватный член класса, как указатель на NoiseReductionFilterImpl, но никак не использовать его. И нам это сойдет с рук. Компилятор потом сам отыщет определение этого класса и удачно разрезолвит все символы. Сейчас покажу, как это будет выглядеть.
// NoiseReductionFilter.hpp
struct NoiseReductionFilter {
Filter();
private:
struct NoiseReductionFilterImpl; // forward declaration
NoiseReductionFilterImpl * impl;
};
// NoiseReductionFilter.cpp
struct NoiseReductionFilter::NoiseReductionFilterImpl {
// implement functionality
};
NoiseReductionFilter::NoiseReductionFilter() : impl (new NoiseReductionFilter::NoiseReductionFilterImpl){}
NoiseReductionFilter::~NoiseReductionFilter() {
delete impl;
impl = nullptr;
}
void NoiseReductionFilter::filter() {
impl->FilterImpl();
}
Скажу сразу, этот код нельзя использовать как он есть здесь. Он просто, чтобы показать концепцию, и не использует необходимых фичей новых стандартов. Что здесь происходит. Мы вынесли определение в файл реализации. А значит никто, кроме нас не сможет увидеть или даже намек почуять, как реализована функциональность (не берем в расчет реверс-инженеров). Это будут просто инструкции, которые будут подгружаться во время выполнения кода клиента.
Обычно файл NoiseReductionFilter.hpp не меняется, так как это публичное апи, а указатель на реализацию дает нам возможность вертеть имплементацией как нам хочется.
Но есть и негативные последствия использования идиомы.
Продолжение в комментах…
#cppcore #design #howitworks #memory
Еще про именование сущностей
В прошлом мы поговорили о том, что нужно создавать абстракции из деталей реализации. Не нужно тащить везде вместе кучу разных переменных. Нужно их объединить в именованный класс. Это повышает читаемость кода и скрывает ненужную на данном уровне понимания информацию.
Однако мы не всегда можем это сделать. Классы должны быть неделимыми с архитектурной точки зрения. Не нужно объединять все в одно. Но даже когда мы имеем завершенный и правильно спроектированный класс, встает проблема, что в названии множества из таких классов не записано их предназначение. Ну тут как бы все логично. Разработчики класса матрицы не знают, как конкретно будет использоваться их класс. С их точки зрения они предоставляют инструмент, кирпичик, который потом ляжет в основу программ пользователей. Проблема проявляется именно на уровне пользовательского кода. Представим, что полноценная программа - это часовой механизм. Неведомая куча каких-то шестеренок, пружинок и прочих финтифлюшек. Глядя на каждый компонент механизма, я понимаю, что он из себя представляет. Но я не понимаю главного: Нахера он нужен именно в этом месте? Какую роль выполняет? Вот также с сущностями в коде. Конечно, давать обстоятельные имена объектам - дело благородное и богоугодное. Но хотелось бы всегда давать имена классам, которые мы используем. Чисто как еще один инструмент для превращения кода в текст на естественном языке. И в плюсах так делать можно!
Алиасинг типов, введенный в С++11, дает нам удобный механизм определения синонимов типов. Это как typedef, только на стероидах. Не будем сейчас говорить о их различиях. Нам важна суть. Мы можем написать:
using ResponseFrequencyByInstance = std::unordered_map<instance_id, size_t>;
И получим человеческое название для структуры, которая хранит частоту ответов, присылаемых от разных инстансов сервиса. И нам уже не важно, что конкретно лежит в основе этой структуры. По названию видно ее предназначение, а нам только это и нужно. А имена этих объектов могут включать уже другую информацию, такую как: единицу измерения отрезка времени(секунды, миллисекунды) или указание конкретного сервиса, для которого собирается статистика. Конечно, всегда рядом с именем объекта мы постоянно должны будем помнить его тип, чтобы получить всю информацию о нем. Однако современные IDE сильно облегчили нашу жизнь в этом плане. Простым наведением курсора на объект мы увидим плашку с типом этого объекта.
Теперь мы можем давать понятные имена любым классам на свой выбор, если видим потребность в более детальной проработке смыслов.
Новичков скорее всего будет отпугивать обилие использование этих алиасов, но это фактически необходимость для поддержания ясности кода и действительно рабочая вещь.
Stay helpful. Stay cool.
#cpp11 #design #goodpractice
В прошлом мы поговорили о том, что нужно создавать абстракции из деталей реализации. Не нужно тащить везде вместе кучу разных переменных. Нужно их объединить в именованный класс. Это повышает читаемость кода и скрывает ненужную на данном уровне понимания информацию.
Однако мы не всегда можем это сделать. Классы должны быть неделимыми с архитектурной точки зрения. Не нужно объединять все в одно. Но даже когда мы имеем завершенный и правильно спроектированный класс, встает проблема, что в названии множества из таких классов не записано их предназначение. Ну тут как бы все логично. Разработчики класса матрицы не знают, как конкретно будет использоваться их класс. С их точки зрения они предоставляют инструмент, кирпичик, который потом ляжет в основу программ пользователей. Проблема проявляется именно на уровне пользовательского кода. Представим, что полноценная программа - это часовой механизм. Неведомая куча каких-то шестеренок, пружинок и прочих финтифлюшек. Глядя на каждый компонент механизма, я понимаю, что он из себя представляет. Но я не понимаю главного: Нахера он нужен именно в этом месте? Какую роль выполняет? Вот также с сущностями в коде. Конечно, давать обстоятельные имена объектам - дело благородное и богоугодное. Но хотелось бы всегда давать имена классам, которые мы используем. Чисто как еще один инструмент для превращения кода в текст на естественном языке. И в плюсах так делать можно!
Алиасинг типов, введенный в С++11, дает нам удобный механизм определения синонимов типов. Это как typedef, только на стероидах. Не будем сейчас говорить о их различиях. Нам важна суть. Мы можем написать:
using ResponseFrequencyByInstance = std::unordered_map<instance_id, size_t>;
И получим человеческое название для структуры, которая хранит частоту ответов, присылаемых от разных инстансов сервиса. И нам уже не важно, что конкретно лежит в основе этой структуры. По названию видно ее предназначение, а нам только это и нужно. А имена этих объектов могут включать уже другую информацию, такую как: единицу измерения отрезка времени(секунды, миллисекунды) или указание конкретного сервиса, для которого собирается статистика. Конечно, всегда рядом с именем объекта мы постоянно должны будем помнить его тип, чтобы получить всю информацию о нем. Однако современные IDE сильно облегчили нашу жизнь в этом плане. Простым наведением курсора на объект мы увидим плашку с типом этого объекта.
Теперь мы можем давать понятные имена любым классам на свой выбор, если видим потребность в более детальной проработке смыслов.
Новичков скорее всего будет отпугивать обилие использование этих алиасов, но это фактически необходимость для поддержания ясности кода и действительно рабочая вещь.
Stay helpful. Stay cool.
#cpp11 #design #goodpractice
Реальное предназначение inline
В этом посте мы говорили о том, почему встраивание функций - важная задача для перформанса приложения. И что ключевое слово inline изначально предназначалось для того, чтобы указывать компилятору, какую функцию ему нужно встроить. Но программы уже давно намного умнее людей в очень специфических задачах. И компилятор стал настолько умным, что он теперь без нашего прямого указания может самостоятельно встраивать функции, которые даже не помечены inline. А также имеет полное право игнорировать наши прямые указания на встраивание. В случае прямого указания он обязан выполнить проверку возможности встраивания, но при оптимизациях компилятор и так это делает.
Но тогда смысл ключевого слова inline несколько теряется в тумане. Все равно все используют оптимизации в продакшене. Тогда есть ли реальная польза от использования inline?
Есть! Сейчас все разберем.
В чем прикол. Прикол в том, что для того, чтобы компилятор смог встроить функцию, ее определение ОБЯЗАНО быть видно в той единице трансляции, в которой она используется. Именно на этапе компиляции. Как можно встроить код, которого нет сейчас в доступе?
Почему это нельзя сделать на этапе линковки? Линкер резолвит проблему символов. Он сопоставляет имена с их содержимым. Линкер от слова link - связка. Для встраивания функции нужно иметь доступ к ее исходникам и информации вокруг вызова функции. Такого доступа у линкера нет. Да и задачи кодогенерации у него нет.
Что нужно, чтобы на этапе компиляции, компилятор видел определение функции? Ее можно определить в цппшнике, тогда все будет четко. Но такую функцию нельзя переиспользовать. Она будет тупо скрыта от всех других единиц трансляции. Ее можно было бы переиспользовать. Тогда нужно было бы везде forward declaration вставлять, что очень неудобно. И она видна будет только во время линковки. Во время компиляции ни одна другая единица трансляции ее не увидит. Поэтому нам это не подходит.
Тогда второй способ с потенциальной возможностью переиспользования: вынести определение в хэдер. Тогда всем единицам трансляции, которые подключают хэдер, будет доступно определение нашей функции. Но вот есть проблема - тогда во всех единицах трансляции будет определение нашей функции. А это нарушение ODR.
Как выходить из ситуации? Можно пометить функцию как static. Тогда в каждой единице трансляции будет своя копия функции. Но это ведет к дублированию кода функции и увеличению размера бинарника. Это нам не подходит.
Выходит, что у нас только одно решение. Разрешить inline функциям находиться в хэдерах и не нарушать ODR! Тогда нам нужны некоторые оговорки: мы разрешаем определению одной и той же inline функции быть в разных единицах трансляции, но тогда все эти определения должны быть идентичные. Потому что как бы предполагается, что они все определены в одном месте КОДА. Линкер потом объединяет все определения функции в одно(на самом деле выбирает одно из них, а другие откидывает). И вот у нас уже один экземпляр функции на всю программу.
Что мы имеем по итогу. Если мы хотим поместить определение обычной функции в хэдэр, то нам настучит по башке линкер со своим multiple definition и мы уйдем грустные в закат. Но теперь у нас есть другой вид функций, которые как бы должны быть встроены, но никто этого не гарантирует, и которые можно определять в хэдерах. Такие функции могут быть встроены с той же вероятностью, что и все остальные, поэтому от этой части смысла никакого нет. Получается, что мы можем пометить нашу функцию inline и тогда она ее просто можно будет определять в заголовочниках. Гениально.
Ох и непростая тема! Советую пару раз прочитать этот пост, чтобы хорошо все усвоить. Информация очень глубокая и фундаментальная. Пишите в комментах, что непонятно. И замечания тоже пишите.
Dig deeper. Stay cool.
#cppcore #compiler #hardcore #design #howitworks
В этом посте мы говорили о том, почему встраивание функций - важная задача для перформанса приложения. И что ключевое слово inline изначально предназначалось для того, чтобы указывать компилятору, какую функцию ему нужно встроить. Но программы уже давно намного умнее людей в очень специфических задачах. И компилятор стал настолько умным, что он теперь без нашего прямого указания может самостоятельно встраивать функции, которые даже не помечены inline. А также имеет полное право игнорировать наши прямые указания на встраивание. В случае прямого указания он обязан выполнить проверку возможности встраивания, но при оптимизациях компилятор и так это делает.
Но тогда смысл ключевого слова inline несколько теряется в тумане. Все равно все используют оптимизации в продакшене. Тогда есть ли реальная польза от использования inline?
Есть! Сейчас все разберем.
В чем прикол. Прикол в том, что для того, чтобы компилятор смог встроить функцию, ее определение ОБЯЗАНО быть видно в той единице трансляции, в которой она используется. Именно на этапе компиляции. Как можно встроить код, которого нет сейчас в доступе?
Почему это нельзя сделать на этапе линковки? Линкер резолвит проблему символов. Он сопоставляет имена с их содержимым. Линкер от слова link - связка. Для встраивания функции нужно иметь доступ к ее исходникам и информации вокруг вызова функции. Такого доступа у линкера нет. Да и задачи кодогенерации у него нет.
Что нужно, чтобы на этапе компиляции, компилятор видел определение функции? Ее можно определить в цппшнике, тогда все будет четко. Но такую функцию нельзя переиспользовать. Она будет тупо скрыта от всех других единиц трансляции. Ее можно было бы переиспользовать. Тогда нужно было бы везде forward declaration вставлять, что очень неудобно. И она видна будет только во время линковки. Во время компиляции ни одна другая единица трансляции ее не увидит. Поэтому нам это не подходит.
Тогда второй способ с потенциальной возможностью переиспользования: вынести определение в хэдер. Тогда всем единицам трансляции, которые подключают хэдер, будет доступно определение нашей функции. Но вот есть проблема - тогда во всех единицах трансляции будет определение нашей функции. А это нарушение ODR.
Как выходить из ситуации? Можно пометить функцию как static. Тогда в каждой единице трансляции будет своя копия функции. Но это ведет к дублированию кода функции и увеличению размера бинарника. Это нам не подходит.
Выходит, что у нас только одно решение. Разрешить inline функциям находиться в хэдерах и не нарушать ODR! Тогда нам нужны некоторые оговорки: мы разрешаем определению одной и той же inline функции быть в разных единицах трансляции, но тогда все эти определения должны быть идентичные. Потому что как бы предполагается, что они все определены в одном месте КОДА. Линкер потом объединяет все определения функции в одно(на самом деле выбирает одно из них, а другие откидывает). И вот у нас уже один экземпляр функции на всю программу.
Что мы имеем по итогу. Если мы хотим поместить определение обычной функции в хэдэр, то нам настучит по башке линкер со своим multiple definition и мы уйдем грустные в закат. Но теперь у нас есть другой вид функций, которые как бы должны быть встроены, но никто этого не гарантирует, и которые можно определять в хэдерах. Такие функции могут быть встроены с той же вероятностью, что и все остальные, поэтому от этой части смысла никакого нет. Получается, что мы можем пометить нашу функцию inline и тогда она ее просто можно будет определять в заголовочниках. Гениально.
Ох и непростая тема! Советую пару раз прочитать этот пост, чтобы хорошо все усвоить. Информация очень глубокая и фундаментальная. Пишите в комментах, что непонятно. И замечания тоже пишите.
Dig deeper. Stay cool.
#cppcore #compiler #hardcore #design #howitworks
anonymous namespace vs static
Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом static, поэтому сегодня обсудим, какие между ними различия. Поехали!
1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.
А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.
А для кода в глобальном скоупе они делают похожие вещи. Поэтому проще запомнить, что для сокрытия данных нужно использовать безымянные пространства, просто потому что они ни для чего больше не нужны.
2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).
3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.
4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.
5️⃣Был такой прикол, что некоторые шаблонные аргументы не могут быть внутренне связными сущностями. Помните, что шаблонный аргумент становится частью инстанцированного типа. Но сущности с внутренним связыванием не видны другим единицам трансляции, поэтому это дело не соберется.
Например
В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.
Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.
Также круто скрывать определения классов от ненужных глаз.
Но вот на счет свободных функций не уверен. Одна, две еще можно. Но обычно их определения удобно располагать рядышком с использующим их кодом. И группировка их вместе будет уменьшать читабельность кода.
В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)
Use proper tools. Stay cool.
#cppcore #cpp17 #design
Вчера мы поговорили о том, что такое анонимные пространства имен. Эта фича обеспечивает внутреннее связывание всем сущностям, которые находятся в нем. Эффекты очень схожи с ключевым словом static, поэтому сегодня обсудим, какие между ними различия. Поехали!
1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.
А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.
А для кода в глобальном скоупе они делают похожие вещи. Поэтому проще запомнить, что для сокрытия данных нужно использовать безымянные пространства, просто потому что они ни для чего больше не нужны.
2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).
3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.
4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.
5️⃣Был такой прикол, что некоторые шаблонные аргументы не могут быть внутренне связными сущностями. Помните, что шаблонный аргумент становится частью инстанцированного типа. Но сущности с внутренним связыванием не видны другим единицам трансляции, поэтому это дело не соберется.
Например
template <int const& Size>
class test {};
static int Size1 = 10;
namespace {
int Size2 = 10;
}
test<Size1> t1; // ERROR!!!
test<Size2> t2;
В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.
Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.
Также круто скрывать определения классов от ненужных глаз.
Но вот на счет свободных функций не уверен. Одна, две еще можно. Но обычно их определения удобно располагать рядышком с использующим их кодом. И группировка их вместе будет уменьшать читабельность кода.
В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)
Use proper tools. Stay cool.
#cppcore #cpp17 #design
И еще раз про именование сущностей
В прошлых постах тут и тут мы говорили больше про чистоту и понятность кода. Здесь поговорим немного про безопасность.
Люди часто пишут код, описывая больше технические детали, а не абстракции и способы взаимодействия с ними. Типа если функция хочет обрабатывать токены слов и их частоту в тексте, люди напишут 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
Реальная ценность ссылок
Да и помните, как вводятся ссылки в учебных целях? Помню читал, что они нужны, чтобы внутри функции изменять переменную и эти изменения отразились в вызывающем блоке кода. И это удобнее указателя, потому что не нужно его разыменовывать. Все. С таким подходом естественно, что никто нихрена не понимает предназначения ссылок. Может только я так криво читал книжки. Это очень на меня похоже. Но раз я такой есть, значит есть и другие, похожие на меня. Поэтому этот пост для всех тех, кто читаешь книжки затылком, или просто новичков, которые не писали много кода.
Как бы функционал-то правильный, никто не спорит. С помощью ссылки в функции действительно можно проводить манипуляции с тем же самым объектом, что мы в нее передали и эти изменения отражаются на этом оригинальном объекте. На этом все основано. Но из этого поначалу довольно сложно сформулировать реальные кейсы использования ссылок. Собсна, погнали их разбирать.
У них есть 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
Защищенные методы 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
Construct on first use idiom
Давайте здесь по-подробнее остановимся. Вещь важная. Предыдущий пост. #опытным
Название говорящее и говорит оно нам, что объект будет конструироваться при первом использовании, а не когда-то заранее. То есть это ленивые вычисления.
Суть в том, чтобы создавать объект только в тот момент, когда он нам понадобиться. Так мы можем четко контролировать момент его инициализации. Делается это с помощью статических локальных переменных.
Мы помним, что они инициализируются при первом вызове функции и существуют они до смерти всей программы. Таким образом, если мы из функции будем возвращать ссылку на эту переменную, то есть сделаем такой геттер, то мы функционально будем иметь глобальную переменную, для которой мы контролируем начало ее жизни.
Вернемся к примеру и посмотрим, как это выглядит. Было так:
а теперь стало так:
Переменная
Теперь следите за руками: мы берем и оборачивает переменную, задающую значение, в функцию-геттер, которая просто выдает наружу значение этой переменной. Но инициализироваться
Теперь результат компиляции не зависит от порядка файлов, которые передаются на вход. Что так
Если у класса есть статическое поле и создание класса зависит от этого статического поля, то попробуйте перенести это поле внутрь статической функции(пример из этого поста):
Теперь во всех местах использования бывшего статического поля, мы вызывает статический метод. Таким образом наша мапа создается ровно по первому нашему хотению и создавать статический объект класса InitializationTest теперь абсолютно безопасно.
Если у вас есть 2 статических объекта пользовательского типа и инициализация одного из них предполагает использование другого, то можно сделать так(пример нагло украден у подписчика Бобра из этого коммента)
В этом примере создание объекта класса AnotherSingleton зависит от объекта Singleton. Поэтому мы запрещаем плебесам создавать объекты класса Singleton, а создаем его один раз в статической функции геттера инстанса объекта и дальше везде используем только этот инстанс.
Заключение в комментах
Solve your problems. Stay cool.
#cppcore #goodpractice #design
Давайте здесь по-подробнее остановимся. Вещь важная. Предыдущий пост. #опытным
Название говорящее и говорит оно нам, что объект будет конструироваться при первом использовании, а не когда-то заранее. То есть это ленивые вычисления.
Суть в том, чтобы создавать объект только в тот момент, когда он нам понадобиться. Так мы можем четко контролировать момент его инициализации. Делается это с помощью статических локальных переменных.
Мы помним, что они инициализируются при первом вызове функции и существуют они до смерти всей программы. Таким образом, если мы из функции будем возвращать ссылку на эту переменную, то есть сделаем такой геттер, то мы функционально будем иметь глобальную переменную, для которой мы контролируем начало ее жизни.
Вернемся к примеру и посмотрим, как это выглядит. Было так:
// source.cpp
int quad(int n) {
return n * n;
}
auto staticA = quad(5);
// main.cpp
#include <iostream>
extern int staticA;
auto staticB = staticA;
int main() {
std::cout << "staticB: " << staticB << std::endl;
}
а теперь стало так:
// source.cpp
int quad(int n) {
return n * n;
}
int& GetStaticA() {
static int staticA = quad(5);
return staticA;
}
// main.cpp
#include <iostream>
int& GetStaticA();
static auto staticB = GetStaticA();
// just omit main
Переменная
staticB
зависит от значения staticA
и это может вызвать проблемы, если инициализации staticB
произойдет первой.Теперь следите за руками: мы берем и оборачивает переменную, задающую значение, в функцию-геттер, которая просто выдает наружу значение этой переменной. Но инициализироваться
staticA
будет ровно в момент первого вызова функции GetStaticA
. Таким образом, мы форсим рантайм инициализировать staticA первым
при любых обстоятельствах.Теперь результат компиляции не зависит от порядка файлов, которые передаются на вход. Что так
g++ main.cpp source.cpp
, что так g++ source.cpp main.cpp
, результат будет staticB: 25
.Если у класса есть статическое поле и создание класса зависит от этого статического поля, то попробуйте перенести это поле внутрь статической функции(пример из этого поста):
using Map = std::map<std::string, std::unique_ptr<InitializationTest>>;
class InitializationTest {
public:
static Map& GetMap() {
static Map map;
return map;
}
static bool Create(std::string ID) {
GetMap().insert({ID, std::move(std::unique_ptr<InitializationTest>{new InitializationTest})});
return true;
}
private:
static Map map;
Test() = default;
};
static bool creation_result = InitializationTest::Create("qwe");
int main() {}
Теперь во всех местах использования бывшего статического поля, мы вызывает статический метод. Таким образом наша мапа создается ровно по первому нашему хотению и создавать статический объект класса InitializationTest теперь абсолютно безопасно.
Если у вас есть 2 статических объекта пользовательского типа и инициализация одного из них предполагает использование другого, то можно сделать так(пример нагло украден у подписчика Бобра из этого коммента)
// singleton.h
class Singleton {
public:
static Singleton& instance() {
static Singleton inst{};
return inst;
}
int makeSomethingUsefull(){}
private:
Singleton() = default;
};
//another_singleton.h
#include "singleton.h"
class AnotherSingleton {
public:
static AnotherSingleton& instance() {;
static AnotherSingleton inst{Singleton::instance().makeSomethingUsefull()};
return inst;
}
private:
AnotherSingleton(int param) : data{param} {};
int data;
};
В этом примере создание объекта класса AnotherSingleton зависит от объекта Singleton. Поэтому мы запрещаем плебесам создавать объекты класса Singleton, а создаем его один раз в статической функции геттера инстанса объекта и дальше везде используем только этот инстанс.
Заключение в комментах
Solve your problems. Stay cool.
#cppcore #goodpractice #design
Telegram
Грокаем C++
Решение static initialization order fiasco
Раз есть проблема - должно быть и решение. Сегодня поговорим о паре-тройке вариантов. Пост вдохновлен этим комментом нашего подписчика Антона.
Очевидно, что в комментах немного поразгоняли эту тему. Поэтому вот…
Раз есть проблема - должно быть и решение. Сегодня поговорим о паре-тройке вариантов. Пост вдохновлен этим комментом нашего подписчика Антона.
Очевидно, что в комментах немного поразгоняли эту тему. Поэтому вот…
Проблема Construct on first use idiom
#опытным
Прошлый пост показывает решение проблемы static initialization order fiasco. Однако даже этот прием имеет свои проблемы.
Дело в том, что мы сильно фокусировались на инициализации объекта и решали проблемы с ней. Но как насчет разрушения объекта? Мы подумали об этом? Not really.
Давайте возьмем классы, которые могут быть использованы для создания и статических объектов и любых других.
У нас все также 2 класса, но они уже не синглтоны, а могут создаваться в какой угодно области. Нам нужны статические объекты этих классов. И мы, как умные дяди, оградили себя от проблемы инициализации статиков, используя construct on first use idiom. Однако замечу, что в деструкторах наших классов они используют глобальную переменную another_global. И например, для объектов с автоматическим временем жизни это вообще не проблема, они свободно создаются и разрушаются.
Но что же будет, если так получится, что another_global удалится раньше, чем статические объекты наших классов? Правильно. Static deinitialization order fiasco. Обращение к уже разрушенному объекту - такое же UB, как и обращение к еще не инициализированному.
Кому-то очень сильно сейчас может свести багскулы, потому что логирование в деструкторах объектов, которые могут быть статиками - очень частая вещь, а соотвественно и потенциальная проблема. Подписчики могут подтвердить это в комментах.
Я сознательно тут в пример не ставлю синглтоны, потому что для них еще как-то можно осознать потенциальную проблему самостоятельно: объект один, мы четко понимаем, как он себя ведет, и можем подумать о его разрушении. Но в сегодняшнем примере при создании подобных классов обычно сильно не задумываются, что объект могут создать в статической области, а значит и о статической деинициализации не думают. Такая невнимательность может привести к трудноотловимым багам.
И это проблема не идиомы в целом, а подхода к созданию объекта. Есть и другой способ это делать:
Обратите внимание на магию. Мы внутри статических функций определяем не статические объекты, а статические указатели, к которым при первом вызове прикрепляем динамически созданные объекты. Вроде ничего кардинально не поменялось, но это на первый взгляд.
Мы никогда не вызываем delete. В конце программы разрушится только указатель, но не объект, на который он указывает. Обычно такая ситуация называется data leak, но в этом случае "вы не понимаете, это другое". Потому что при завершении программы ОС сама освобождает всю память, которая была занята программой и на самом деле ничего не утекает. Утечка памяти - это постоянное увеличение использования памяти программы со временем ее жизни. А тут мы один раз захватили эту память(и только эту!), но просто не отдали. Потребление памяти в течение программы не увеличивается. Как говорится: "Это норма!".
Этот вариант конечно не подойдет для тех случаев, если вам прям обязательно как-то сигнализировать о разрушении всех-превсех объектов этого класса и без этого никуда. Но он совершенно точно избавит вас от потенциальных проблем деинициализации(ее просто не будет хехе), если вам не важен деструктор статических объектов.
See drawbacks of your solutions. Stay cool.
#goodpractice #design #cppcore
#опытным
Прошлый пост показывает решение проблемы static initialization order fiasco. Однако даже этот прием имеет свои проблемы.
Дело в том, что мы сильно фокусировались на инициализации объекта и решали проблемы с ней. Но как насчет разрушения объекта? Мы подумали об этом? Not really.
Давайте возьмем классы, которые могут быть использованы для создания и статических объектов и любых других.
// ClassA.h
class ClassA {
public:
int makeSomethingUsefull(){}
~ClassA() { another_global.use_it();}
};
static ClassA& GetStaticClassA() {
static ClassA inst{};
return inst;
}
//another_singleton.h
#include "singleton.h"
class ClassB {
public:
ClassB(int param) : data{param} {};
~ClassB() { another_global.use_it();}
private:
int data;
};
static ClassB& GetStaticClassB() {;
static ClassB inst{GetStaticClassA().makeSomethingUsefull()};
return inst;
}
У нас все также 2 класса, но они уже не синглтоны, а могут создаваться в какой угодно области. Нам нужны статические объекты этих классов. И мы, как умные дяди, оградили себя от проблемы инициализации статиков, используя construct on first use idiom. Однако замечу, что в деструкторах наших классов они используют глобальную переменную another_global. И например, для объектов с автоматическим временем жизни это вообще не проблема, они свободно создаются и разрушаются.
Но что же будет, если так получится, что another_global удалится раньше, чем статические объекты наших классов? Правильно. Static deinitialization order fiasco. Обращение к уже разрушенному объекту - такое же UB, как и обращение к еще не инициализированному.
Кому-то очень сильно сейчас может свести багскулы, потому что логирование в деструкторах объектов, которые могут быть статиками - очень частая вещь, а соотвественно и потенциальная проблема. Подписчики могут подтвердить это в комментах.
Я сознательно тут в пример не ставлю синглтоны, потому что для них еще как-то можно осознать потенциальную проблему самостоятельно: объект один, мы четко понимаем, как он себя ведет, и можем подумать о его разрушении. Но в сегодняшнем примере при создании подобных классов обычно сильно не задумываются, что объект могут создать в статической области, а значит и о статической деинициализации не думают. Такая невнимательность может привести к трудноотловимым багам.
И это проблема не идиомы в целом, а подхода к созданию объекта. Есть и другой способ это делать:
// ClassA.h
// Here Class A definition
static ClassA& GetStaticClassA() {
static ClassA* inst = new ClassA{};
return *inst;
}
//another_singleton.h
#include "singleton.h"
// Here ClassB definition
static ClassB& GetStaticClassB() {;
static ClassB* inst = new ClassB{GetStaticClassA().makeSomethingUsefull()};
return *inst;
}
Обратите внимание на магию. Мы внутри статических функций определяем не статические объекты, а статические указатели, к которым при первом вызове прикрепляем динамически созданные объекты. Вроде ничего кардинально не поменялось, но это на первый взгляд.
Мы никогда не вызываем delete. В конце программы разрушится только указатель, но не объект, на который он указывает. Обычно такая ситуация называется data leak, но в этом случае "вы не понимаете, это другое". Потому что при завершении программы ОС сама освобождает всю память, которая была занята программой и на самом деле ничего не утекает. Утечка памяти - это постоянное увеличение использования памяти программы со временем ее жизни. А тут мы один раз захватили эту память(и только эту!), но просто не отдали. Потребление памяти в течение программы не увеличивается. Как говорится: "Это норма!".
Этот вариант конечно не подойдет для тех случаев, если вам прям обязательно как-то сигнализировать о разрушении всех-превсех объектов этого класса и без этого никуда. Но он совершенно точно избавит вас от потенциальных проблем деинициализации(ее просто не будет хехе), если вам не важен деструктор статических объектов.
See drawbacks of your solutions. Stay cool.
#goodpractice #design #cppcore
Named Constructor Idiom
Конструкторы - вещь хорошая, но довольно ограниченная. Если вы хотите создать объект класса ТяжеленнаяФиговина, то логично было бы ввести разные конструкторы для разных систем измерения. Ну например, чтобы фиговина могла создаваться из киллограммов и из фунтов. Но это невозможно.
Дело в том, что конструктор класса - такая же функция, как и все остальные. У него есть имя(имя класса), список аргументов(включая неявный this) и пустое возвращаемое значение. В языке С++ конструкторы вообще ничего не возвращают, но они так или иначе реализованы на обычных ассемблерных функциях, а они имеют все эти обязательные характеристики.
А раз это функция и неизменяемым именем, то различать разные конструкторы мы можем только с помощью разного списка параметров.
И это становится проблемой, когда конструкторов много и их уже сложно отличать друг от друга, либо как в нашем примере с ТяжелойФиговиной мы просто не можем два конструктра с одинаковым списком параметров, но эти параметры будут иметь разное назначение.
Допустим, что с фиговиной помогут справиться strong typedefs. Но со сложностью различий конструкторов для пользователя они не помогут. А вот что может помочь.
Named Constructor Idiom. Давайте дадим имена конструкторам!
Точнее мы немного схитрим. Добавим именные статические функции-фабрики, которые и будут конструировать наши объекты, и переместим все конструкторы в private секцию.
Покажу на примере фиговины, чтобы было по-проще и по-короче
Да, придется немного букав пописать, но для столкнувшихся с такой проблемой это - выход. Можно еще поиграться с возвращаемым значением. Например, сделать его уникальным указателем. Но это уже детали.
Make convenient interfaces. Stay cool.
#design
Конструкторы - вещь хорошая, но довольно ограниченная. Если вы хотите создать объект класса ТяжеленнаяФиговина, то логично было бы ввести разные конструкторы для разных систем измерения. Ну например, чтобы фиговина могла создаваться из киллограммов и из фунтов. Но это невозможно.
Дело в том, что конструктор класса - такая же функция, как и все остальные. У него есть имя(имя класса), список аргументов(включая неявный this) и пустое возвращаемое значение. В языке С++ конструкторы вообще ничего не возвращают, но они так или иначе реализованы на обычных ассемблерных функциях, а они имеют все эти обязательные характеристики.
А раз это функция и неизменяемым именем, то различать разные конструкторы мы можем только с помощью разного списка параметров.
И это становится проблемой, когда конструкторов много и их уже сложно отличать друг от друга, либо как в нашем примере с ТяжелойФиговиной мы просто не можем два конструктра с одинаковым списком параметров, но эти параметры будут иметь разное назначение.
Допустим, что с фиговиной помогут справиться strong typedefs. Но со сложностью различий конструкторов для пользователя они не помогут. А вот что может помочь.
Named Constructor Idiom. Давайте дадим имена конструкторам!
Точнее мы немного схитрим. Добавим именные статические функции-фабрики, которые и будут конструировать наши объекты, и переместим все конструкторы в private секцию.
Покажу на примере фиговины, чтобы было по-проще и по-короче
struct HeavyThing {
static HeavyThing ConstructFromKilos(float kilos) {
return HeavyThing(kilos);
}
static HeavyThing ConstructFromPounds(float pounds) {
return HeavyThing(0,453592 * pounds);
}
private:
HeavyThing(float kilos) : kilos_{kilos} {}
float kilos_;
};
int main() {
HeavyThing a = HeavyThing::ConstructFromKilos(100500.0);
HeavyThing b = HeavyThing::ConstructFromPounds(12345678.0);
}
Да, придется немного букав пописать, но для столкнувшихся с такой проблемой это - выход. Можно еще поиграться с возвращаемым значением. Например, сделать его уникальным указателем. Но это уже детали.
Make convenient interfaces. Stay cool.
#design
Неименованные параметры функций
С++ позволяет не указывать имена параметров функций, если они не используются в коде.
Это можно делать и в объявлении функции, и в ее определении.
Важный момент, что отсутствие имени параметра не говорит о том, что параметра нет и его не нужно передавать. Для вызова такой функции вы должны передать в нее аргумент соответствующего типа. Даже если он ничего не делает полезного.
Но вот вопрос возникает тогда. Если параметр ничего не делает, нахрена он тогда вообще нужен?
На самом деле много кейсов, где неименованный параметр может пригодится.
💥 Допустим, у вас есть функция, которая используется в очень многих местах кода, может даже через какие-нибудь указатели на функцию. И в один момент времени часть функционала стала ненужной и один или несколько параметров стали ненужны. Править все вызовы этой функции было бы болью, особенно если туда вовлечены function поинтеры. Вместо этого вы можете сделать эти параметры безымянными, чтобы явно в коде показать, что этот параметр не используется. Его и нельзя даже будет использовать.
💥 Заглушки. Зачастую для тестирования функциональности применяют сущности-болванки, которые внешне ведут себя, как нормальные ребята, но на самом деле они лодыри и ничего путного не делают. Это нужно для мокания соседних модулей, чтобы протестировать только функциональность выбранного набора модулей. Такие заглушки должны выглядеть подобающе, то есть полностью повторять апи замоканой сущности, но могут не делать никакой полезной работы. Поэтому можно в этом апи сделать безымянные параметры, чтобы еще раз подчеркнуть, что они не используются.
💥 Иногда существующие сущности в коде требуют коллбэки определенного вида. И вам в своем коллбэке возможно не нужно использовать весь набор параметров. Но для соблюдения апи вы должны их указать в сигнатуре своего обратного вызова. В этом случае можно сделать эти параметры безымянными.
💥 Иногда в иерархии полиморфных классов в конкретном наследнике вам не нужны все параметры виртуальной функции. Но для поддержания корректности переопределения виртуального интерфейса вы должны включить все параметры в сигнатуру метода. Опять же, неиспользуемые параметры можно пометить безымянными.
💥 Знаменитая перегрузка постфиксного оператора инкремента/декремента. Есть 2 вида этих операторов: префикстный и постфиксный. Проблема в том, что это все еще вызов функции operator++. Как различить реализации этих функций? Правильно, нужна перегрузка. Вот здесь и приходит на помощь безымянный параметр: в коде он не нужен, но влияет на выбор конкретной перегрузки. Выглядит это так:
В целом, эта фича нужна либо для соблюдения существующего апи, либо для того, чтобы при вызове функции гарантировано вызвалась правильная перегрузка.
Stay useful. Stay cool.
#cppcore #design
С++ позволяет не указывать имена параметров функций, если они не используются в коде.
void foo(int /no name here/);
void foo(int /no name here/)
{
std::cout << "foo" << std::endl;
}
foo(5);
Это можно делать и в объявлении функции, и в ее определении.
Важный момент, что отсутствие имени параметра не говорит о том, что параметра нет и его не нужно передавать. Для вызова такой функции вы должны передать в нее аргумент соответствующего типа. Даже если он ничего не делает полезного.
Но вот вопрос возникает тогда. Если параметр ничего не делает, нахрена он тогда вообще нужен?
На самом деле много кейсов, где неименованный параметр может пригодится.
💥 Допустим, у вас есть функция, которая используется в очень многих местах кода, может даже через какие-нибудь указатели на функцию. И в один момент времени часть функционала стала ненужной и один или несколько параметров стали ненужны. Править все вызовы этой функции было бы болью, особенно если туда вовлечены function поинтеры. Вместо этого вы можете сделать эти параметры безымянными, чтобы явно в коде показать, что этот параметр не используется. Его и нельзя даже будет использовать.
💥 Заглушки. Зачастую для тестирования функциональности применяют сущности-болванки, которые внешне ведут себя, как нормальные ребята, но на самом деле они лодыри и ничего путного не делают. Это нужно для мокания соседних модулей, чтобы протестировать только функциональность выбранного набора модулей. Такие заглушки должны выглядеть подобающе, то есть полностью повторять апи замоканой сущности, но могут не делать никакой полезной работы. Поэтому можно в этом апи сделать безымянные параметры, чтобы еще раз подчеркнуть, что они не используются.
💥 Иногда существующие сущности в коде требуют коллбэки определенного вида. И вам в своем коллбэке возможно не нужно использовать весь набор параметров. Но для соблюдения апи вы должны их указать в сигнатуре своего обратного вызова. В этом случае можно сделать эти параметры безымянными.
💥 Иногда в иерархии полиморфных классов в конкретном наследнике вам не нужны все параметры виртуальной функции. Но для поддержания корректности переопределения виртуального интерфейса вы должны включить все параметры в сигнатуру метода. Опять же, неиспользуемые параметры можно пометить безымянными.
💥 Знаменитая перегрузка постфиксного оператора инкремента/декремента. Есть 2 вида этих операторов: префикстный и постфиксный. Проблема в том, что это все еще вызов функции operator++. Как различить реализации этих функций? Правильно, нужна перегрузка. Вот здесь и приходит на помощь безымянный параметр: в коде он не нужен, но влияет на выбор конкретной перегрузки. Выглядит это так:
struct Digit
{
Digit(int digit=0) : m_digit{digit} {}
Digit& operator++(); // prefix has no parameter
Digit operator++(int); // postfix has an int parameter
private:
int m_digit{};
};
В целом, эта фича нужна либо для соблюдения существующего апи, либо для того, чтобы при вызове функции гарантировано вызвалась правильная перегрузка.
Stay useful. Stay cool.
#cppcore #design