
- Главная
- Каталог
- Интернет технологии
- Грокаем C++
Грокаем C++
Авторский канал о программировании на С++ и базе computer science. Простым и легким слогом рассказываем про сложные концепции С++. Самая активная и вовлеченная аудитория в тематике.
Статистика канала
struct Type {
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>();
}
private:
Type() = default;
};
int main()
{
auto obj = Type::Create();
}{}
Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.
Это можно обойти, просто использовав явный вызов new:
struct Type {
static std::unique_ptr<Type> Create() {
return std::unique_ptr<Type>(new Type);
}
private:
Type() = default;
};
int main()
{
auto obj = Type::Create();
}{}
Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!
Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:
class Type {
class PrivateKey { // private struct of Type
PrivateKey() = default;
friend Type;
};
public:
Type(PrivateKey) {}
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>(PrivateKey{});
}
};
int main() {
auto obj = Type::Create(); // OK
auto obj2 = Type(PrivateKey{}); // ERROR: PrivateKey is private
}{}
По сути, это та же идиома passkey из этого поста. Только здесь она раскрывается с еще одной стороны.
Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!
Know your enemies. Stay cool.
#cppcore #design #memory
std::mutex mtx;
std::map<std::string, int> cache;
void update_cache(const std::string& key, int value) {
std::lock_guard<std::mutex> lock(mtx);
cache[key] = value;
}{}
Это все конечно нужно обернуть в класс, но суть понятна и так.
Семафор же нужен для ограничения количество одновременно используемых ресурсов.
Например, ограниченная потокобезопасная очередь:
template<typename T, size_t N>
class BoundedQueue {
std::queue<T> queue_;
std::counting_semaphore<N> empty_slots_{N};
std::counting_semaphore<N> filled_slots_{0};
std::mutex mtx_;
public:
void push(T value) {
empty_slots_.acquire();
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(value));
}
filled_slots_.release();
}
T pop() {
filled_slots_.acquire();
T value;
{
std::lock_guard<std::mutex> lock(mtx_);
value = std::move(queue_.front());
queue_.pop();
}
empty_slots_.release();
return value;
}
};{}
Здесь семафоры нужны для того, чтобы в очередь не положили больше элементов, чем нужно, и не забрали больше, чем в очереди может потенциально быть. Обратите внимание на порядок инкремента и декремента.
Лайк, если понравилось. Да и если не понравилось, тоже ставьте.
Compare things. Stay cool.
#concurrency #cpp20 #cpp11
class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};{}
Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:
class OnlyStack {
OnlyStack() {
if (rand() % 2) {
std::cout << "ERROR" << std::endl;
}
}
public:
static std::optional<OnlyStack> Create() { return {}; }
};{}
"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"
Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:
class OnlyStack {
public:
OnlyStack() = default;
~OnlyStack() = default;
static void* operator new(std::size_t) = delete;
static void* operator new = delete;
};
OnlyStack obj; // OK
OnlyStack* p = new OnlyStack(); // ERROR{}
Вот так просто и без дополнительных приседаний.
Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:
class PooledObject {
static char pool[1024];
static size_t offset;
public:
void* operator new(size_t s) {
if (offset + s > 1024) throw std::bad_alloc();
void* ptr = pool + offset;
offset += s;
return ptr;
}
void operator delete(void*) noexcept {
// Complicated logic
// or just ignore freeing memory
}
};
PooledObject* obj = new PooledObject();{}
Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.
Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?
Тогда его расположение будет определяться тем, где находится объемлющий объект. То есть, если мы создаём объект Container на куче, то и все его поля, включая OnlyStack, окажутся на куче. Получается, что наш запрет на new OnlyStack не спасает от ситуации, когда OnlyStack становится членом другого класса, который кто-то создаёт через new.
class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};
struct Container {
Container() : obj{OnlyStack::Create()} {}
OnlyStack obj;
};
Container* p = new Container(); // OK{}
Пишите в комментах, если знаете, как обойти эту проблему)
Don't be so radical. Stay cool.
#cppcore #memory #design
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
private:
~MyClass() { std::cout << "MyClass destructor\n"; }
public:
static void destroy(MyClass* ptr) {
delete ptr;
}
static std::unique_ptr<MyClass, decltype(&destroy)> create() {
return std::unique_ptr<MyClass, decltype(&destroy)>(new MyClass(), destroy);
}
};{}
При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.
Публичный конструктор здесь не имеет большого смысла, но и так и так работает.
Но вообще говоря, зачем все эти запреты? Можно ли как-то добиться запрета при публичном конструкторе и деструкторе?
Можно(почти).
Давайте сделаем публичный деструктор и конструктор с параметром, который не может создать внешний код:
class Key {
private:
Key() = default;
friend class Factory;
};
class Object {
public:
explicit Object(const Key&) {} // требует ключ
};
class Factory {
public:
static std::unique_ptr<Object> create() {
return std::make_unique<Object>(Key());
}
};{}
Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.
Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.
Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.
И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.
// object.hpp
class Object;
std::unique_ptr<Object> createObject();
// object.cpp
#include "object.h"
class Object {
public:
Object() = default;
~Object() = default;
};
std::unique_ptr<Object> createObject() {
return std::make_unique<Object>();
}{}
В итоге да, мы запретили объекту создаваться, где угодно, кроме как через createObject. Но при этом пользоваться объектом мы никак не сможем. Только создать и удалить.
Explore exotic things. Stay cool.
#design #cppcore
class MyClass {
private:
MyClass() { std::cout << "MyClass created\n"; }
public:
~MyClass() { std::cout << "MyClass destroyed\n"; }
static std::unique_ptr<MyClass> create() {
return std::unique_ptr<MyClass>(new MyClass());
}
void hello() const {
std::cout << "Hello, MyClass!\n";
}
};
auto obj = MyClass::create();
obj->hello();{}
Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.
Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.
Такой подход может быть полезен, если:
👉🏿 размер объекта очень большой и просто не влезет в стек
👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.
👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr.
👉🏿 вы хотите какой-то особый контроль времени жизни.
Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:
class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void foo() {}
private:
Singleton() = default;
~Singleton() = default;
};{}
Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.
Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик.
Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз.
Limit wrong uses. Stay cool.
#design #cppcore
int i;
int main() {
std::cout << i << std::endl;
}
// OUTPUT is always
// 0{}
Все пошло от языка С. Там глобальные переменные занулялись, поэтому для совместимости с С С++ также перенял эту особенность.
"Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь."
Это был важный переход, который и приведет нас к ответу.
Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык.
Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями.
И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись.
Когда пришло время стандартизации С, то обнуление глобальных переменных органично перекочевало в стандарт языка.
Вот так обнуление стало стандартом С, а затем и С++.
Understand the root cause. Stay cool.
#cppcore #os
int main() {
int i;
std::cout << i << std::endl;
}
// POSSIBLE OUTPUT:
// 64{}
Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить?
На самом деле мусор - не значит какие-то рандомные числа, никто их не генерирует. И чтобы понять, откуда берется мусор, нужно знать о стеке вызовов.
Стек вызовов - пространство в памяти, которое содержит информацию об исполнении функций. Представим аналогию со стопкой книг. Каждая книга - небольшой кусочек памяти с информацией об отдельной функции(фрейм функции). Как только вы вызываете функцию, на стопку кладется новая книга(фрейм). Когда исполнение доходит до конца функции - сверху снимается книга. Это происходит автоматически на уровне машинных инструций.
Так вот локальные переменные как раз хранятся на стеке. Если точнее, то значения локальных переменных конкретной функции лежат внутри фрейма этой функции. И нас интересует процесс выделения памяти под локальные переменные.
При исполнении программы есть специальный указатель, который указывает на вершину стека. Выделение памяти под локальную переменную - это просто сдвиг этого указателя. И когда мы говорим:
int main() {
int i;
...
}{}
Мы говорим, что хотим иметь переменную i и надо выделить под нее память, то есть просто передвинуть указатель на стек с позиции X на позицию X+4. Само значение мы при этом не задаем.
Но значение-то все равно будет. И будет оно браться их тех битиков-байтиков, которые находились в куске памяти от X до X-4.
А кто-то из вас заранее знает, какие байтики там будут храниться?
Нет
На этом месте почти наверняка уже располагались данные какой-нибудь другой функции, которая уже выполнилась ранее и ее фрейм снялся со стека.
Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции.
Чтобы более наглядно представить себе процесс работы со стеком вызовов, можете воспользоваться этим визуализатором. Там примеры фиксированные. Если же хотите на собственных примерах посмотреть - можете поиграться тут.
"Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?"
На самом деле вызывается, вы просто не видите этого. Чтобы подготовить программу к запуску, надо немало потрудиться и эти труды обязательно будут отражены на стеке.
"Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?"
Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься.
"Очищение" памяти отнимает дополнительное драгоценное время исполнения программы. И никто его не хочет тратить на такую ненужную вещь, как обнуление памяти. Инициализируйте свои переменные и проблем с вывозом мусора не будет.
Understand the root cause. Stay cool.
#cppcore #osGROKAEMCPP
Купить билет
Реклама. ООО «Джуг Ру Груп». ИНН 7801341446Отзывы канала
всего 7 отзывов
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
Грокаем C++ — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 9.3K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 16.9, количество отзывов – 7, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 8391.6 ₽, а за 32 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий