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
Раз есть проблема - должно быть и решение. Сегодня поговорим о паре-тройке вариантов. Пост вдохновлен этим комментом нашего подписчика Антона.
Очевидно, что в комментах немного поразгоняли эту тему. Поэтому вот…
Раз есть проблема - должно быть и решение. Сегодня поговорим о паре-тройке вариантов. Пост вдохновлен этим комментом нашего подписчика Антона.
Очевидно, что в комментах немного поразгоняли эту тему. Поэтому вот…
❤🔥12🔥8⚡4👍3❤1🤩1
Проблема 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
👍20❤7🔥6🤔2😁1
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
👍40🔥11❤5⚡1👎1🤩1
Неименованные параметры функций
С++ позволяет не указывать имена параметров функций, если они не используются в коде.
Это можно делать и в объявлении функции, и в ее определении.
Важный момент, что отсутствие имени параметра не говорит о том, что параметра нет и его не нужно передавать. Для вызова такой функции вы должны передать в нее аргумент соответствующего типа. Даже если он ничего не делает полезного.
Но вот вопрос возникает тогда. Если параметр ничего не делает, нахрена он тогда вообще нужен?
На самом деле много кейсов, где неименованный параметр может пригодится.
💥 Допустим, у вас есть функция, которая используется в очень многих местах кода, может даже через какие-нибудь указатели на функцию. И в один момент времени часть функционала стала ненужной и один или несколько параметров стали ненужны. Править все вызовы этой функции было бы болью, особенно если туда вовлечены 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
4❤🔥15❤8🔥7😁6👍5
Почему приватные методы находятся в описании класса?
#опытным
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать?
Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
На самом деле, в С++ мы имеем прямой доступ к памяти, в значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол такого дизайн решения?
Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
#опытным
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать?
Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
На самом деле, в С++ мы имеем прямой доступ к памяти, в значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол такого дизайн решения?
Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
👍21🔥7❤4❤🔥2
Наследование? От лямбды? Ч3
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
template<typename ... Lambdas>
struct DerivedFromLambdas : Lambdas...
{
DerivedFromLambdas(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{
}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
int main(){
std::vector<var_t> vec = {10, 1.5, "hello"};
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
10 1.500000 "hello"
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
❤22👍15🔥9🤯3⚡2👏1
Как header only либы обходят ODR
#новичкам
В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.
Действительно, если будут 2 функции с одинаковыми названиями, но разной реализацией, то непонятно, какую из них выбрать для линковки с использующим функцию кодом.
Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.
В чем подвох?
Подвоха нет. Даже так, чисто заголовочная природа библиотеки это не совсем цель, а возможно простое следствие. Следствие того, что часто библиотеки напичканы шаблонами по самые гланды. А шаблоны просто вынуждены находиться в хэдэрах, ничего уж тут не поделаешь. У нас даже целый пост про это есть.
Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.
А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .
В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.
Bypass the rules. Stay cool.
#compiler #design
#новичкам
В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.
Действительно, если будут 2 функции с одинаковыми названиями, но разной реализацией, то непонятно, какую из них выбрать для линковки с использующим функцию кодом.
Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.
В чем подвох?
Подвоха нет. Даже так, чисто заголовочная природа библиотеки это не совсем цель, а возможно простое следствие. Следствие того, что часто библиотеки напичканы шаблонами по самые гланды. А шаблоны просто вынуждены находиться в хэдэрах, ничего уж тут не поделаешь. У нас даже целый пост про это есть.
Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.
А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .
В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.
Bypass the rules. Stay cool.
#compiler #design
🔥19👍10❤4👏1
Потокобезопасный интерфейс
#новичкам
Не для всех очевидная новость: не всегда можно превратить класс из небезопасного в потокобезопасный, просто по уши обложившись лок гардами. Да, вызов конкретного метода будет безопасен. Но это не значит, что классом безопасно пользоваться.
Возьмем максимально простую реализацию самой простой очереди:
Она конечно потокоНЕбезопасная. То есть ей можно адекватно пользоваться только в рамках одного потока.
Как может выглядеть код простого консьюмера этой очереди?
И вот мы захотели разделить обязанности производителя чисел и их потребителя между разными потокам. Значит, нам надо как-то защищать очередь от многопоточных неприятностей.
Бабахаем везде лок гард на один мьютекс и дело в шляпе!
Все доступы к очереди защищены. Но спасло ли реально это нас?
Вернемся к коду консюмера:
А вдруг у нас появится еще один консюмер? Тогда в первом из них мы можем войти условие, а в это время второй достанет последний элемент. Получается, что мы получим доступ к неинициализированной памяти в методе front.
То есть по факту в многопоточном приложении полученный стейт сущности сразу же утрачивает свою актуальность.
Что делать? Не только сами методы класса должны быть потокобезопасными. Но еще и комбинации использования этих методов тоже должны обладать таким свойством. И с данным интерфейсом это сделать просто невозможно.
Если стейт утрачивает актуальность, то мы вообще не должны давать возможность приложению получать стейт очереди. Нам нужны только команды управления. То есть push и pop.
Внутри метода
Теперь консюмер выглядит так:
Можно конечно было использовать кондвары и прочее. Но я хотел сфокусироваться именно на интерфейсе. Теперь реализация просто не позволяет получать пользователю потенциально неактульные данные.
Stay safe. Stay cool.
#concurrency #design #goodpractice
#новичкам
Не для всех очевидная новость: не всегда можно превратить класс из небезопасного в потокобезопасный, просто по уши обложившись лок гардами. Да, вызов конкретного метода будет безопасен. Но это не значит, что классом безопасно пользоваться.
Возьмем максимально простую реализацию самой простой очереди:
struct Queue {
void push(int value) {
storage.push_back(value);
}
void pop() {
storage.pop_front();
}
bool empty() {
return storage.empty();
}
int& front() {
return storage.front();
}
private:
std::deque<int> storage;
};Она конечно потокоНЕбезопасная. То есть ей можно адекватно пользоваться только в рамках одного потока.
Как может выглядеть код простого консьюмера этой очереди?
while(condition)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}
И вот мы захотели разделить обязанности производителя чисел и их потребителя между разными потокам. Значит, нам надо как-то защищать очередь от многопоточных неприятностей.
Бабахаем везде лок гард на один мьютекс и дело в шляпе!
struct Queue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
void pop() {
std::lock_guard lg{m};
storage.pop_front();
}
bool empty() {
std::lock_guard lg{m};
return storage.empty();
}
int& front() {
std::lock_guard lg{m};
return storage.front();
}
private:
std::deque<int> storage;
std::mutex m;
};Все доступы к очереди защищены. Но спасло ли реально это нас?
Вернемся к коду консюмера:
while(true)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}
А вдруг у нас появится еще один консюмер? Тогда в первом из них мы можем войти условие, а в это время второй достанет последний элемент. Получается, что мы получим доступ к неинициализированной памяти в методе front.
То есть по факту в многопоточном приложении полученный стейт сущности сразу же утрачивает свою актуальность.
Что делать? Не только сами методы класса должны быть потокобезопасными. Но еще и комбинации использования этих методов тоже должны обладать таким свойством. И с данным интерфейсом это сделать просто невозможно.
Если стейт утрачивает актуальность, то мы вообще не должны давать возможность приложению получать стейт очереди. Нам нужны только команды управления. То есть push и pop.
struct ThreadSafeQueue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
std::optional<int> pop() {
std::lock_guard lg{m};
if (!storage.empty()) {
int elem = storage.front();
storage.pop_front();
return elem;
}
return nullopt;
}
private:
std::deque<int> storage;
std::mutex m;
};Внутри метода
pop мы можем использовать проверять и получать стейт очереди, так как мы оградились локом. Возвращаем из него std::optional, который будет хранить фронтальный элемент, если очередь была непуста. В обратном случае он будет пуст.Теперь консюмер выглядит так:
while(true) {
auto elem = queue.pop();
if (elem)
process_elem(elem.value());
}Можно конечно было использовать кондвары и прочее. Но я хотел сфокусироваться именно на интерфейсе. Теперь реализация просто не позволяет получать пользователю потенциально неактульные данные.
Stay safe. Stay cool.
#concurrency #design #goodpractice
10👍42🔥11❤7😁2🤔1
Почему приватные методы находятся в описании класса?
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников.
Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект.
Ну а приватные-то методы зачем раскрывать? Зачем их помещать внутрь класса и делать видимыми для клиентского кода?
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
В С++ мы имеем прямой доступ к памяти, а значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол того, что приватные методы участвуют в разрешении перегрузок?
Давайте снова представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но им этой одной было достаточно.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты.
Я могу удалить приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Не знаю, как оценивать все перечисленные причины. Напишите свое мнение в комментариях.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников.
Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект.
Ну а приватные-то методы зачем раскрывать? Зачем их помещать внутрь класса и делать видимыми для клиентского кода?
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
В С++ мы имеем прямой доступ к памяти, а значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол того, что приватные методы участвуют в разрешении перегрузок?
Давайте снова представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но им этой одной было достаточно.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты.
struct DumbClass {
void DumbFunction(int param) {
// i do nothing because i’m dumb
}
private:
void DumbFunction(double param) = delete;
};
int main() {
DumbClass{}.DumbFunction(1.0);
}
// error: use of deleted function ‘void DumbClass::DumbFunction(double)’Я могу удалить приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Не знаю, как оценивать все перечисленные причины. Напишите свое мнение в комментариях.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
😁25❤9👍6🔥3👎1
ref-qualified методы
#опытным
В С++ можно довольно интересными способами перегружать методы класса. Один из самых малоизвестных и малоиспользуемых - помечать методы квалификатором ссылочности.
Чтобы было понятнее. Примерно все знают, что бывают константные и неконстантные методы.
Константные объекты могут вызывать только константные методы. Поэтому мы можем перегрузить метод класса, чтобы он мог работать с константными объектами.
В примере видно что у константного объекта вызывается константная перегрузка.
По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.
Обратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода.
Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном.
Зачем это придумано?
Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению или по константной ссылке.
Также и с ссылочностью. В каких-то случаях оптимально или просто необходимо использовать для правых ссылок иную логику метода.
Подробнее об этом чуде-юде будем разбираться в следующих постах.
Stay flexible. Stay cool.
#cpp11 #design
#опытным
В С++ можно довольно интересными способами перегружать методы класса. Один из самых малоизвестных и малоиспользуемых - помечать методы квалификатором ссылочности.
Чтобы было понятнее. Примерно все знают, что бывают константные и неконстантные методы.
struct SomeClass {
void foo() {std::cout << "Non-const member function" << std::endl;}
void foo() const {std::cout << "Const member function" << std::endl;}
};
SomeClass nonconst_obj;
const SomeClass const_obj;
nonconst_obj.foo();
const_obj.foo();
// OUTPUT
// Non-const member function
// Const member functionКонстантные объекты могут вызывать только константные методы. Поэтому мы можем перегрузить метод класса, чтобы он мог работать с константными объектами.
В примере видно что у константного объекта вызывается константная перегрузка.
По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.
struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;}
void foo() && {std::cout << "Call on rvalue reference" << std::endl;}
};
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
// OUTPUT
// Call on lvalue reference
// Call on rvalue referenceОбратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода.
Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном.
Зачем это придумано?
Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению или по константной ссылке.
Также и с ссылочностью. В каких-то случаях оптимально или просто необходимо использовать для правых ссылок иную логику метода.
Подробнее об этом чуде-юде будем разбираться в следующих постах.
Stay flexible. Stay cool.
#cpp11 #design
🔥29👍12❤4🤯4
Modern pimpl идиома
#новичкам
В одном из давнишних постов мы уже обсуждали базовую формулировку и реализацию идиомы pimpl. Это сырой указатель на forward-объявленную структуру.
В современных плюсах, естественно, никто уже так не делает. Сырые указатели уходят в прошлое и старые подходы пересматриваются с применением умных указателей.
Допустим, вы используете какой-то нестандартный формат сериализации данных, например apache avro. Конечно вы хотите написать свою библиотеку сериализации, которая позволить конвертировать данные в/из avro формата. Как должна выглядеть эта библиотека?
Ну скорее всего вы хотите использовать только доменные сущности в интерфейсе библиотеки. В этом случае только эта библиотека зависит от стороннего решения и вы в любой момент смогли, например, изменить формат сериализации, если у вас изменились бизнес требования. Также приятно иметь стабильный интерфейс и линковать стороннее решение только с этой библиотекой.
С такими симптомами ваш терапевт прописал вам однократный прием pimpl идиомы. Выглядеть это может так:
Основная идея - все упоминания и детали реализации avro находятся в cpp и не торчат наружу. Это достигается за счет использования forward-declared класса в хэдэре и определение этого класса в сорцах. За счет этого мы имеем стабильный интерфейс, стабильный ABI и сокращенное время линковки. А за счет использования std::unique_ptr у нас тривиальные реализации всех специальных методов.
Примерно так это и выглядит в современных плюсах. Но это еще не конец для pimpl. Есть решение, которое поможем вам избежать динамических аллокаций. Но об этом в другой раз.
Hide details. Stay cool.
#design
#новичкам
В одном из давнишних постов мы уже обсуждали базовую формулировку и реализацию идиомы pimpl. Это сырой указатель на forward-объявленную структуру.
В современных плюсах, естественно, никто уже так не делает. Сырые указатели уходят в прошлое и старые подходы пересматриваются с применением умных указателей.
Допустим, вы используете какой-то нестандартный формат сериализации данных, например apache avro. Конечно вы хотите написать свою библиотеку сериализации, которая позволить конвертировать данные в/из avro формата. Как должна выглядеть эта библиотека?
Ну скорее всего вы хотите использовать только доменные сущности в интерфейсе библиотеки. В этом случае только эта библиотека зависит от стороннего решения и вы в любой момент смогли, например, изменить формат сериализации, если у вас изменились бизнес требования. Также приятно иметь стабильный интерфейс и линковать стороннее решение только с этой библиотекой.
С такими симптомами ваш терапевт прописал вам однократный прием pimpl идиомы. Выглядеть это может так:
// serializer.hpp
class Serializer {
public:
Serializer();
~Serializer();
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;
std::vector<uint8_t> serializeOrder(const domain::Order& person);
domain::Order deserializeOrder(const std::vector<uint8_t>& data);
private:
std::unique_ptr<struct Impl> pimpl_; // Smart pointer on forward-declared struct
};
// serializer.cpp
struct Serializer::Impl {
avro::GenericRecord convertToAvro(const domain::Order&);
domain::Order convertFromAvro(const avro::GenericRecord&);
};
Serializer::Serializer()
: pimpl_(std::make_unique<Impl>()) {
}
Serializer::~Serializer() = default;
Serializer::Serializer(Serializer&&) = default;
Serializer& Serializer::operator=(Serializer&&) = default;
std::vector<uint8_t> Serializer::serializeOrder(const domain::Person& person) {
auto record = pimpl_->convertToAvro(person);
return {...};
}
// deserializeOrder
Основная идея - все упоминания и детали реализации avro находятся в cpp и не торчат наружу. Это достигается за счет использования forward-declared класса в хэдэре и определение этого класса в сорцах. За счет этого мы имеем стабильный интерфейс, стабильный ABI и сокращенное время линковки. А за счет использования std::unique_ptr у нас тривиальные реализации всех специальных методов.
Примерно так это и выглядит в современных плюсах. Но это еще не конец для pimpl. Есть решение, которое поможем вам избежать динамических аллокаций. Но об этом в другой раз.
Hide details. Stay cool.
#design
❤21🔥13👍6
Зачем в pimpl определять специальные методы в cpp?
#опытным
В прошлом посте вы могли заметить, что все специальные методы класса Serializer только объявляются в хэдере, а реализуются в файле исходников. Зачем так делать?
Мы ведь привыкли либо вообще не определять никакие специальные методы по правилу нуля, либо просто помечать их default и все. Почему в этот раз так нельзя сделать?
Все дело в нашем особенном указателе pimpl_. Это умный указатель на forward-объявленную структуру. Это значит, что мы не знаем примерно ничего об этой структуре, кроме ее имени. И в этом загвоздка.
Когда мы явно помечаем специальные методы класса как default, мы явно просим компилятор за нас сгенерировать определение этих методов.
То есть мы запрашиваем определение метода в теле класса. На основе чего компилятор будет генерировать их? На основе того, что есть в теле класса.
Но вот беда, в теле класса нет никакой информации о том, что из себя представляет класс Impl. Возьмем тот же деструтор. У std::unique_ptr тип делитера зашит в шаблонный параметр. По дефолту это default_deleter, который вызывает просто
С перемещающими операциями похожая история. Когда мы используем операции std::unique_ptr мы инстанцируем этот шаблон с конкретными шаблонными параметрами. Значит должен инстанцироваться и default_deleter, который не может работать с неполными типами, как уже было сказано выше.
Так что определяйте специальные методы в реализации при использовании pimpl и будет вам счастье.
Don't give impossible tasks. Stay cool.
#cppcore #design
#опытным
В прошлом посте вы могли заметить, что все специальные методы класса Serializer только объявляются в хэдере, а реализуются в файле исходников. Зачем так делать?
class Serializer {
public:
Serializer();
~Serializer();
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;
//...
private:
std::unique_ptr<struct Impl> pimpl_;
};Мы ведь привыкли либо вообще не определять никакие специальные методы по правилу нуля, либо просто помечать их default и все. Почему в этот раз так нельзя сделать?
Все дело в нашем особенном указателе pimpl_. Это умный указатель на forward-объявленную структуру. Это значит, что мы не знаем примерно ничего об этой структуре, кроме ее имени. И в этом загвоздка.
Когда мы явно помечаем специальные методы класса как default, мы явно просим компилятор за нас сгенерировать определение этих методов.
class Serializer {
public:
Serializer();
~Serializer() = default; // Here
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept = default; // Here
Serializer& operator=(Serializer&&) noexcept = default; // Here
//...
private:
std::unique_ptr<struct Impl> pimpl_;
};То есть мы запрашиваем определение метода в теле класса. На основе чего компилятор будет генерировать их? На основе того, что есть в теле класса.
Но вот беда, в теле класса нет никакой информации о том, что из себя представляет класс Impl. Возьмем тот же деструтор. У std::unique_ptr тип делитера зашит в шаблонный параметр. По дефолту это default_deleter, который вызывает просто
delete ptr . А у delete expression есть пометка, что при удалении указателя на неполный тип программа становится ill-formed. Поэтому компиляторы на случай, если вы захотите сгенерировать деструктор std::unique_ptr с неполным типом, ставят такой ассерт времени компиляции: static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type");. А компиляция последнего примера крашнется с соотвествующей ошибкой.С перемещающими операциями похожая история. Когда мы используем операции std::unique_ptr мы инстанцируем этот шаблон с конкретными шаблонными параметрами. Значит должен инстанцироваться и default_deleter, который не может работать с неполными типами, как уже было сказано выше.
Так что определяйте специальные методы в реализации при использовании pimpl и будет вам счастье.
Don't give impossible tasks. Stay cool.
#cppcore #design
🔥21❤8👍7
🔞 Если принимаете сырой указатель, как параметр - не забудьте проверить его на nullptr. Меньше будете голову ломать при будущем дебаге.
🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.
🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.
Но если очень хочется оставить compareData, то лучше все равно определить оператор, не делать compareData другом класса, и пусть она использует нативный оператор внутри себя без нарушения инкапсуляции.
🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.
🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.
Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.
Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.
Фух, на этом вроде все.
Вот что вышло по итогу исправлений:
Пишите, если что забыл.
Critique your solution. Stay cool.
#cppcore #OOP #goodpractice #design
🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.
🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.
Но если очень хочется оставить compareData, то лучше все равно определить оператор, не делать compareData другом класса, и пусть она использует нативный оператор внутри себя без нарушения инкапсуляции.
🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.
🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.
Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.
Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.
Фух, на этом вроде все.
Вот что вышло по итогу исправлений:
class Data final {
private:
std::unique_ptr<char[]> buffer;
size_t buf_size = 0;
public:
Data(const char* input, size_t size)
: buf_size(size) {
if (input == nullptr && size != 0) {
throw std::invalid_argument("Input cannot be null for non-zero size");
}
if (size > 0) {
buffer = std::make_unique<char[]>(size);
std::copy(input, input + size, buffer.get());
}
}
template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}
// Копирующий конструктор
Data(const Data& other)
: Data{other.buffer.get(), other.buf_size} {
}
// Универсальное присваивание
Data& operator=(Data other) {
swap(*this, other);
return *this;
}
// Перемещающий конструктор
Data(Data&& other) noexcept {
swap(*this, other);
}
~Data() = default;
// Оператор сравнения
bool operator==(const Data& rhs) const noexcept {
return std::string_view{buffer.get(), buf_size} == std:;string_view{rhs.buffer.get(), buf_size()};
}
// Вспомогательная функция для обмена
friend void swap(Data& first, Data& second) noexcept {
using std::swap;
swap(first.buffer, second.buffer);
swap(first.buf_size, second.buf_size);
}
};
[[nodiscard]] bool compareData(const Data& data1, const Data& data2) noexcept {
return data1 == data2;
}Пишите, если что забыл.
Critique your solution. Stay cool.
#cppcore #OOP #goodpractice #design
❤36🔥12👍8❤🔥1👎1
Inline виртуальные методы
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?
Ну то есть такая иерархия:
Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?
Посмотрим на такой код:
Конечно же метод будет встраиваться в вызывающий код. Компилятор на этапе компиляции четко знает тип, с которым он работает, и может проводить оптимизации.
Но вообще говоря, это далеко не основной кейс использования виртуальных методов. Давайте ближе к реальной задаче:
Здесь все понятно. Компилятор реально не знает, какие типы находятся внутри my_vec в функции vec_base. Поэтому все, что он может сделать - это использовать указатель на таблицу виртуальных функций и пусть рантайме уже решается, какой конкретно метод вызвать.
Теперь посмотрим такой код:
И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.
Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.
И здесь в игру вступает ключевое слово final и девиртуализация вызовов, о которой мы говорили тут и тут. Пометив класс как final, мы можем ожидать, что компилятор поймет, что никаких наследников у него нет. Поэтому он может встраивать вызов метода foo для Derived класса в последнем примере.
Вот код на годболте с последними примерами и final девиртуализацией. Немного упростил его, чтобы легче было ассемблер читать.
Правды ради стоит сказать, что для девиртуализации нужны очень особенные условия, которых сложно достичь, используя стандартные практики программирования и паттерны ООП.
Учитывая, что
1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)
2️⃣ операции над непосредственно типами наследниками не распространены
3️⃣ а для девиртуализации нужно танцевать с бубнами
от определения чуть сложных виртуальных методов внутри класса могут быть только проблемы. Поэтому например в Chromium гайдлайнах четко прописано, что запрещены инлайн определения виртуальных методов. Все уносим в цпп.
Be useful. Stay cool.
#cppcore #OOP #design
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?
Ну то есть такая иерархия:
class Base {
public:
virtual int foo() = 0;
protected:
int x = 2;
};
class Derived : public Base {
public:
int foo() override {
x *= 2;
return x;
}
};Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?
Посмотрим на такой код:
void simple() {
Derived d;
std::cout << d.foo();
}Конечно же метод будет встраиваться в вызывающий код. Компилятор на этапе компиляции четко знает тип, с которым он работает, и может проводить оптимизации.
Но вообще говоря, это далеко не основной кейс использования виртуальных методов. Давайте ближе к реальной задаче:
void vec_base(std::vector<std::unique_ptr<Base>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Base>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);Здесь все понятно. Компилятор реально не знает, какие типы находятся внутри my_vec в функции vec_base. Поэтому все, что он может сделать - это использовать указатель на таблицу виртуальных функций и пусть рантайме уже решается, какой конкретно метод вызвать.
Теперь посмотрим такой код:
void vec_derived(std::vector<std::unique_ptr<Derived>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Derived>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.
Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.
И здесь в игру вступает ключевое слово final и девиртуализация вызовов, о которой мы говорили тут и тут. Пометив класс как final, мы можем ожидать, что компилятор поймет, что никаких наследников у него нет. Поэтому он может встраивать вызов метода foo для Derived класса в последнем примере.
Вот код на годболте с последними примерами и final девиртуализацией. Немного упростил его, чтобы легче было ассемблер читать.
Правды ради стоит сказать, что для девиртуализации нужны очень особенные условия, которых сложно достичь, используя стандартные практики программирования и паттерны ООП.
Учитывая, что
1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)
2️⃣ операции над непосредственно типами наследниками не распространены
3️⃣ а для девиртуализации нужно танцевать с бубнами
от определения чуть сложных виртуальных методов внутри класса могут быть только проблемы. Поэтому например в Chromium гайдлайнах четко прописано, что запрещены инлайн определения виртуальных методов. Все уносим в цпп.
Be useful. Stay cool.
#cppcore #OOP #design
1❤14👍7🔥6❤🔥2
race condition
#новичкам
Теперь состояние гонки. Это более общее понятие, чем гонка данных. Это ситуация в программе, когда поведение системы зависит от относительного порядка выполнения операций в потоках.
Внимание: состояние гонки есть даже в правильно синхронизированных программах. В однопоточной программе можно четко предсказать порядок обработки элементов. А вот если много потоков будут разгребать одну кучу задач - вы не сможете сказать заранее, какой выхлоп в следующий раз произведет конкретный поток. Потому что это зависит от шедулинга потоков.
Но нам и не важно это предсказание, потому что имеет значение поведение всей программы целиком.
Проблемы возникают, когда такие спорадические эффекты приводят к некорректным результатам. И именно эти ситуации обычно называют состоянием гонки. Мне кажется важным проговорить, что потоки всегда в состоянии гонки за чем-то и в этом отражение недетерминированности многопоточной среды. Но далее буду употреблять "состояние гонки" в негативном контексте.
Проблемы из-за состояния гонки могут происходить даже в программах без гонки данных.
Например:
Может так произойти, что поток 1 выполнится в промежутке между условием и выводом
Состояние гонки - это в основном ошибка проектирования в условиях многопоточности. Знаменитая проблема наличия метода size() у многопоточной очереди - состояние гонки:
Если между успешной и потокобезопасной проверкой, что очередь непустая, придет другой поток и заберет последний элемент из очереди, вы получите ub в попытке увидеть фронтальный элемент.
Основные черты состояния гонки:
🙈 Наличие логической ошибки при проектировании системы
🙈 Зависимость от планирования потоков
🙈 Зависимость от времени выполнения операции. Вчера в чате скинули мем, иллюстрирующий эту зависимость.
Многие путают или не понимают разницы между race condition и data race. Это даже частый вопрос на собеседованиях, на который 50% кандидатов отвечают что-то вообще невнятное. Но теперь вы подготовлены и вооружены правильным словарным аппаратом.
Be independent of other's schedule. Stay cool.
#design #concurrency #interview
#новичкам
Теперь состояние гонки. Это более общее понятие, чем гонка данных. Это ситуация в программе, когда поведение системы зависит от относительного порядка выполнения операций в потоках.
Внимание: состояние гонки есть даже в правильно синхронизированных программах. В однопоточной программе можно четко предсказать порядок обработки элементов. А вот если много потоков будут разгребать одну кучу задач - вы не сможете сказать заранее, какой выхлоп в следующий раз произведет конкретный поток. Потому что это зависит от шедулинга потоков.
Но нам и не важно это предсказание, потому что имеет значение поведение всей программы целиком.
Проблемы возникают, когда такие спорадические эффекты приводят к некорректным результатам. И именно эти ситуации обычно называют состоянием гонки. Мне кажется важным проговорить, что потоки всегда в состоянии гонки за чем-то и в этом отражение недетерминированности многопоточной среды. Но далее буду употреблять "состояние гонки" в негативном контексте.
Проблемы из-за состояния гонки могут происходить даже в программах без гонки данных.
Например:
std::atomic<int> x = 2;
void thread_1() {
x = 3;
}
void thread_2() {
if (x % 2 == 0) {
std::cout << x << std::endl;
}
}
Может так произойти, что поток 1 выполнится в промежутке между условием и выводом
x на консоль. Это очень маловероятная ситуация, однако на консоль может вывестись нечетное число 3 с учетом того, что перед выводом мы проверили на четность. Как минимум удивительный результат, хотя с программе нет гонки данных.Состояние гонки - это в основном ошибка проектирования в условиях многопоточности. Знаменитая проблема наличия метода size() у многопоточной очереди - состояние гонки:
template <typename T>
class ThreadSafeQueue {
...
size_t size() {
std::lock_guard lg{mtx_};
return queue_.size();
}
private:
std::deque<T> queue_;
...
};
ThreadSafeQueue<int> queue;
...
if (queue.size() > 0) {
auto item = std::move(queue.front());
queue.pop();
// process item
}
Если между успешной и потокобезопасной проверкой, что очередь непустая, придет другой поток и заберет последний элемент из очереди, вы получите ub в попытке увидеть фронтальный элемент.
Основные черты состояния гонки:
🙈 Наличие логической ошибки при проектировании системы
🙈 Зависимость от планирования потоков
🙈 Зависимость от времени выполнения операции. Вчера в чате скинули мем, иллюстрирующий эту зависимость.
Многие путают или не понимают разницы между race condition и data race. Это даже частый вопрос на собеседованиях, на который 50% кандидатов отвечают что-то вообще невнятное. Но теперь вы подготовлены и вооружены правильным словарным аппаратом.
Be independent of other's schedule. Stay cool.
#design #concurrency #interview
1❤22👍11🔥6👎2😁2🤔1
Output параметры
#новичкам
Если у вас нет std::variant, std::expected, std::optional, вам лень засовывать ошибки в объекты, то у вас остается не так много вариантов. Один из них - output параметры функции.
Идея донельзя простая. Не хочешь использовать сложные типы? Добавь дополнительный параметр функции!
Но конфигурация возвращаемого значения и параметров функции может быть разная:
👉🏿 С-style. Возвращаем int, который каким-то образом кодирует ошибку + потенциально какую-то полезную информацию, а результат работы функции записывается в выходной параметр. Например системный вызов read:
Возвращает количество прочитанных байт, либо -1, если произошла ошибка. Конкретная ошибка передается через errno. Сами данные записываются в buf.
Так как это не С++ подход, то он содержит все недостатки отсутствия ООП.
👉🏿 Код результата 1. Возвращаем enum успешности операции, а сам результат возвращаем в одном или нескольких выходных параметрах.
В этом случае приходится объявлять переменную результата заранее.
👉🏿 Код результата 2. Наоборот, возвращаем результат, а в выходные параметры передаем потенциальную ошибку:
Такая сигнатура никак не форсит человека проверить код ошибки, можно случайно забыть проверить воспользоваться невалидным результатом.
Этот подход используется в том числе в стандартной библиотеке:
Теперь уже заранее надо код результата инициализировать.
👉🏿 Универсальный. Можно ввести и универсальный кодстайл: возвращаем bool в качестве индикатора успешности выполнения функции, а результаты и ошибки возвращаем в output параметрах.
Здесь сигнатура явно форсит проверить результат(особенно с nodiscard). Но еще больше данных нужно объявлять до вызова функции и код становится все менее декларативным.
Наверное когда-то это был популярный подход к обработке ошибок. Но в современном С++ существуют хорошие альтернативы, которые сделают ваш код проще, понятнее и консистентнее.
Prevent misuse. Stay cool.
#design
#новичкам
Если у вас нет std::variant, std::expected, std::optional, вам лень засовывать ошибки в объекты, то у вас остается не так много вариантов. Один из них - output параметры функции.
Идея донельзя простая. Не хочешь использовать сложные типы? Добавь дополнительный параметр функции!
Но конфигурация возвращаемого значения и параметров функции может быть разная:
👉🏿 С-style. Возвращаем int, который каким-то образом кодирует ошибку + потенциально какую-то полезную информацию, а результат работы функции записывается в выходной параметр. Например системный вызов read:
ssize_t read(size_t count, int fd, void buf[count], size_t count);
Возвращает количество прочитанных байт, либо -1, если произошла ошибка. Конкретная ошибка передается через errno. Сами данные записываются в buf.
Так как это не С++ подход, то он содержит все недостатки отсутствия ООП.
👉🏿 Код результата 1. Возвращаем enum успешности операции, а сам результат возвращаем в одном или нескольких выходных параметрах.
enum Code {
Success,
DivisionByZero,
NegativeNumber,
Overflow
};
Code safe_sqrt(double x, double& result) {
if (x < 0) {
return Code::NegativeNumber;
}
result = std::sqrt(x);
return Code::Success;
}
double result = 0.;
auto code = safe_sqrt(-4.0, result);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}В этом случае приходится объявлять переменную результата заранее.
👉🏿 Код результата 2. Наоборот, возвращаем результат, а в выходные параметры передаем потенциальную ошибку:
double safe_sqrt(double x, Code& code) {
if (x < 0) {
code = Code::NegativeNumber;
return {};
}
code = Code::Success;
return std::sqrt(x);
}
Code code = Code::Success;
auto result = safe_sqrt(-4.0, code);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}Такая сигнатура никак не форсит человека проверить код ошибки, можно случайно забыть проверить воспользоваться невалидным результатом.
Этот подход используется в том числе в стандартной библиотеке:
bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;
Теперь уже заранее надо код результата инициализировать.
👉🏿 Универсальный. Можно ввести и универсальный кодстайл: возвращаем bool в качестве индикатора успешности выполнения функции, а результаты и ошибки возвращаем в output параметрах.
bool parse_coordinates(const std::string& input,
double& x, double& y, double& z,
std::string& error_message);
double x, y, z;
std::string error_msg;
if (parse_coordinates("10.5,20.3,30.7", x, y, z, error_msg)) {
std::cout << "Coordinates: " << x << ", " << y << ", " << z << std::endl;
} else {
std::cout << "Error: " << error_msg << std::endl;
}
Здесь сигнатура явно форсит проверить результат(особенно с nodiscard). Но еще больше данных нужно объявлять до вызова функции и код становится все менее декларативным.
Наверное когда-то это был популярный подход к обработке ошибок. Но в современном С++ существуют хорошие альтернативы, которые сделают ваш код проще, понятнее и консистентнее.
Prevent misuse. Stay cool.
#design
4👍21❤13🔥7
Как применять switch?
#новичкам
Мы вспомнили про switch и некоторые его особенности. Теперь посмотрим, в каких сценариях их адекватно применять и как это делать.
Для начала повторим ограничения. switch может работать только с целочисленными значениями и перечислениями. В основном, конечно, с перечислениями как с именованными числами. В коде не должно быть магических чисел, каждая ветка должна иметь наглядную семантику, а перечисления помогают "называть" числа без неконтролируемого размножения локальных или статических переменных.
В прошлом посте мы свичались по http статус кодам. Тогда они были просто числами, так было проще вспомнить механику этой инструкции. Давайте перепишем в нормальный вид:
Теперь магических чисел нет.
Плюс к этому все кейсы должны быть известны на этапе компиляции. Динамически менять ничего не получится.
Эти особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).
Дальше идем.
Чем вообще может быть плох switch?
👉🏿 Его использование смешивает логику выбора и обработки. Это может быть нормой при небольшом количестве веток. Все находится рядышком, не нужно прыгать по коду, чтобы понять полную картину.
Но это совершенно точно становится проблемой, когда количество кейсов больше 10-20. Свитч целиком перестает помещаться на экран и вместо того, чтобы при чтении кода понимать логику работы кода, вы разбираетесь в том, какой обработчик для каждого кейса вызывается.
👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.
👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.
Из этих проблем можно сделать несколько выводов:
1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.
2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.
3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.
Когда не надо применять switch?
🔞 Пользовательский тип под условием.
🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)
🔞 Если нужен какой-то дикий полиморфизм, когда обработчик полиморфно выбирается на основе значения кейса.
В этих случаях обычно используют статическую std::unordered_map, сопоставляя значения кейсов их обработчикам.
Use the right tool. Stay cool.
#cppcore #goodpractice #design
#новичкам
Мы вспомнили про switch и некоторые его особенности. Теперь посмотрим, в каких сценариях их адекватно применять и как это делать.
Для начала повторим ограничения. switch может работать только с целочисленными значениями и перечислениями. В основном, конечно, с перечислениями как с именованными числами. В коде не должно быть магических чисел, каждая ветка должна иметь наглядную семантику, а перечисления помогают "называть" числа без неконтролируемого размножения локальных или статических переменных.
В прошлом посте мы свичались по http статус кодам. Тогда они были просто числами, так было проще вспомнить механику этой инструкции. Давайте перепишем в нормальный вид:
switch(status_code) {
case HTTPStatus::OK:
processSuccess();
break;
case HTTPStatus::BadRequest:
case HTTPStatus::Unauthorized:
case HTTPStatus::Forbidden:
case HTTPStatus::NotFound:
logClientError(status_code);
break;
case HTTPStatus::InternalServerError:
case HTTPStatus::BadGateway:
case HTTPStatus::ServiceUnavailable:
return logServerError(status_code);
default:
// ...
}Теперь магических чисел нет.
Плюс к этому все кейсы должны быть известны на этапе компиляции. Динамически менять ничего не получится.
Эти особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).
Дальше идем.
Чем вообще может быть плох switch?
👉🏿 Его использование смешивает логику выбора и обработки. Это может быть нормой при небольшом количестве веток. Все находится рядышком, не нужно прыгать по коду, чтобы понять полную картину.
Но это совершенно точно становится проблемой, когда количество кейсов больше 10-20. Свитч целиком перестает помещаться на экран и вместо того, чтобы при чтении кода понимать логику работы кода, вы разбираетесь в том, какой обработчик для каждого кейса вызывается.
👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.
👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.
Из этих проблем можно сделать несколько выводов:
1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.
2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.
3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.
void dispatch(Editor &editor, Command cmd) {
switch (cmd.type) {
case Command::NewFile:
executeNewFile(editor);
return;
case Command::OpenFile:
executeOpenFile(editor);
return;
// ...
}
}
void processUserInput(Editor &editor, UserInput input) {
if (auto cmd = interpretInput(input)) {
dispatch(editor, *cmd);
}
}Когда не надо применять switch?
🔞 Пользовательский тип под условием.
🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)
🔞 Если нужен какой-то дикий полиморфизм, когда обработчик полиморфно выбирается на основе значения кейса.
В этих случаях обычно используют статическую std::unordered_map, сопоставляя значения кейсов их обработчикам.
Use the right tool. Stay cool.
#cppcore #goodpractice #design
❤18👍10🔥5🤣5😁1