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

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Баланс — залог успеха

Многие программисты пилят свои пет-проекты, но немногие запускают их в условный "прод". Для определенности возьмем сайт, на который вы загружаете pdf-ку и вам возвращается распознанный текст. С момента запуска такой проект претерпевает стандартный набор метаморфоз на пути к успеху:

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

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

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

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

А как он это решает — отдельный разговор. Пару дней назад у Вани Ходора, бэкенд-разработчика Лавки, вышел пост со всесторонним обзором методов балансировки трафика. Он там описал алгоритмы балансировки (round-robin, consistent hashing и другие), да и вообще подробно рассказал про тонкости работы с балансировщиками.

Если вы интересуетесь дизайн-секциями и хотите получше в этой теме разобраться, то будет интересно.
👍13❤‍🔥84👎1🔥1👏1
​​WAT
#новичкам

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Дан простой кусочек кода:
const char* s1 = "First";
constexpr char* s2 = "Second";
constexpr const char* s3 = "Third";

static_assert(std::is_const_v<decltype(s1)>);
static_assert(std::is_const_v<decltype(s2)>);
static_assert(std::is_const_v<decltype(s3)>);


Все просто. Тип 3-х переменных проверяется на константность.

Вопрос: сможете сказать без компилятора какой из трех вариантов нормально соберется, какой выдаст assert, а какой выдаст warning?

Возьмите паузу на подумать.

Ответ будет такой:

static_assert(std::is_const_v<decltype(s1)>); // выдаст ассерт
static_assert(std::is_const_v<decltype(s2)>); // выдаст варнинг
static_assert(std::is_const_v<decltype(s3)>); // нормально скомилится


ЧЗХ? Там же все константное?

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

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

Так вот ассерты проверяют является ли сам указатель константным. s1 - это неконстантный указатель на константу, поэтому срабатывает ассерт.

Теперь с constexpr разбираемся. Этот спецификатор подразумевает const. И так как его нельзя применять более одного раза при объявлении переменной, то он применяется к самой "верхушке" типа. То есть s2 и s3 становятся константными указателями. И для них ассерты не срабатывают.

Для s2 выдается варнинг, потому что мы пытаемся присвоить строковый литерал, который имеет тип const char[], то есть массив константных символов, к указателю на неконстанту. В нормальной ситуации это бы вызвало ошибку компиляции, но такие преобразования возможны в С. И С++ сохраняет здесь совместимость, хоть и стремается этого и генерирует предупреждение о такой опасной ситуации.

Все примеры довольно просто объяснимы, хотя на первый взгляд лицо деформируется в вопросительный знак. Помните об особенностях константности указателей и будет вам счастье.

Be amazed. Stay cool.

#cppcore
29👍17🤯11🔥3❤‍🔥2😁2
​​constexpr vs consteval функции
#опытным

В С++20 добавили новый спецификатор - consteval. Отдельно его интро не особо интересно разбирать, поэтому попробуем в сравнении с constexpr.

consteval может быть применен только к функциям и это ключевое слово заставляет функцию выполняться во время компиляции и возвращать константное выражение. Отсюда и название спецификатора "константное вычисление"(constant evaluation).

Вот пара простых примеров:

consteval int sum_consteval(int a, int b) {
return a + b;
}

constexpr int sum_constexpr(int a, int b) {
return a + b;
}

int main() {
constexpr auto c = sum_consteval(100, 100);
static_assert(c == 200);

constexpr auto c1 = sum_constexpr(100, 100);
static_assert(c1 == 200);

constexpr auto val = 10;
static_assert(sum_consteval(val, val) == 2*val);

int a = 10;
int res = sum_constexpr(a, 10); // fine with constexpr function
int res1 = sum_consteval(10, 10);

// int res2 = sum_consteval(a, 10); // error!
// the value of 'a' is not usable in a constant expression
}


Мы можем использовать оба спецификатора constexpr и consteval для того, чтобы инициализировать compile-time константы и обычные переменные.

Однако consteval функции могут вызываться только с constant expression в качестве аргументов. При попытке передачи в них неконстантного выражения будет ошибка компиляции.

Итого:

👉🏿 спецификатор consteval может быть применен только для функций

👉🏿 constexpr может быть применен и для переменных

👉🏿 consteval заставляет компилятор вычислять выражение на этапе компиляции. Если хотя бы один аргумент такой функции не является константным выражением, то будет ошибка компиляции

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

Don't be confused. Stay cool.

#cpp20
👍26🔥54🗿2😁1
constexpr vs constinit
#опытным

constinit - еще один спецификатор, который появился в С++20. Им помечаются глобальные или thread-local переменные для того, чтобы удостоверится в их статической инициализации. Это либо нулевая инициализация, либо константная инициализация. Собственно, это и отображено в самом названии спецификатора.

Тут суть в Static Initialization Order Fiasco. Инициализация глобальных переменных в рантайме зависит от фазы луны и половой активности жаб в Центральной Америке. Мы нем можем гарантировать порядок инициализации глобальных переменных в рантайме и если одна переменная зависит от значения другой, то может произойти много неприятных неожиданностей.

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

Интересность ситуёвины состоит в том, что constinit не подразумевает константность объекта! Действительно, мы же можем проинициализировать константным выражение ь неконстантную переменную и это будет валидный код:

static int i = 42;


constinit - это только про гарантии инициализации в компайл тайме и все! Например:

// init at compile time
constexpr int compute(int v) { return vvv; }
constinit int global = compute(10); // compute is invoked at compile-time

// won't work:
// constinit int another = global; // global is a runtime value

int main() {
// but allow to change later...
global = 100;

// global is not constant expression!
// std::array<int, global> arr;
}


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

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

Итого:

👉🏿 constinit переменные в своей базе мутабельные, constexpr - немутабельные.

👉🏿 constinit применяется только к static и thread storage duration объектам. Проще говоря, к разного рода глобальным переменным. constexpr может применяться к локальным переменным.

👉🏿 Оба спецификатора обеспечивают инициализацию глобальных переменных в compile-time и защищают от SIOF.

👉🏿 Эти спецификаторы нельзя использовать в одном выражении.

Don't be confused. Stay cool.

#cpp20
👍1910🔥9❤‍🔥1
​​Висячие ссылки в лямбдах
#новичкам

Все знают, что возврат ссылки на локальный объект функции приводит к неопределенному поведению. Однако не всегда так просто можно распознать такие ситуации.

В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.

Лямбда, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой её создали. Как только лямбда покинула скоуп - можно начинать молиться:

auto make_add_n(int n) {
return [&](int x) {
return x + n; // n will become dangling reference!
};
}

auto add5 = make_add_n(5);
std::cout << add5(5) << std::endl; // UB!


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

Еще более интересная ситуация с объектами и методами.

struct Task {
int id;

std::function<void()> GetNotifier() {
return [=]{
std::cout << "notify " << id << std::endl;
};
}
};

int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}


Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.

На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать this в списке захвата:

struct Task {
int id;

std::function<void()> GetNotifier() {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


Так уже чуть проще отловить проблему.

Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.

В случае с захватом this профилактикой может быть синтаксическое ограничение использование методов, возвращающий лямбду, с помощью ref-квалификаторов методов:

struct Task {
int id;
std::function<void()> GetNotifier() && = delete; // forbit call on temporaries

std::function<void()> GetNotifier() & {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


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

Конечно это вряд ли поможет в многопоточке, но это уже что-то.

Refer to actual things. Stay cool.

#cppcore #cpp11 #cpp20
2🔥27👍1711
История capture this
#опытным

Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.

С++11

Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается &(*this) то есть указатель на текущий объект:

struct Foo {
int m_x = 0;

void func() {
int x = 0;

//Explicit capture 'this'
[this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[&]() { /*access m_x and x*/ }();

//Redundant 'this'
[&, this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[=]() { /*access m_x and x*/ }();

//Error
[=, this]() { }();
}
};


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

С++14

Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:

struct Foo {
int m_x = 0;

void func() {
[copy=*this]() mutable {
copy.m_x++;
}();
}
};


В остальном все осталось также.

С++17

Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:

struct Processor {
//Some state data..

std::future<void> process(/*args*/) {
//Pre-process...
//Do the data processing asynchronously
return
std::async(std::launch::async,
[=](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

auto caller() {
Processor p;
return p.process(/*args*/);
}


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

struct Processor {
std::future<void> process(/*args*/) {
return
std::async(std::launch::async,
[*this](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.

С++20

С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.

Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.

Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:

struct Bagel {
int x = 0;
void func() {
//OK until C++20. Warning in C++20.
[=]() { std::cout << x; }();

//Error/warning until C++20. OK in C++20.
[=, this]() { std::cout << x; }();
}
};


Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.

Know the history. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥14👍108🤯4
Правильно захватываем по ссылке объект в лямбду
#опытным

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

struct Task {
int id;

std::function<void()> GetNotifier() {
return [&]{
std::cout << "notify " << id << std::endl;
};
}
};

int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}


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

Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:

struct Task {
int id;

std::function<void()> GetNotifier() {
return [curr = std::shared_ptr<Task>(this)]{
std::cout << "notify " << curr->id << std::endl;
};
}
};

int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}


Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект! curr будет думать, что только он и его копии будут владеть объектом, а оригинальный объект просто уничтожится и curr будет ссылаться на невалидный объект. Получили то же самое UB.

Что делать?

Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:

struct Task : std::enable_shared_from_this<Task> {
int id;

std::function<void()> GetNotifier() {
// Захватываем shared_ptr на текущий объект
return [self = shared_from_this()] {
std::cout << "notify " << self->id << std::endl;
};
}
};

int main() {
auto notify = std::make_shared<Task>(5)->GetNotifier();
notify(); // Теперь безопасно - объект не будет уничтожен
}


В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.

2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.

3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.

Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.

Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.

Watch your lifetime. Stay cool.

#template #cpp11
2👍9319🔥14🤯1
Динамический полиморфизм. ООP-style
#новичкам

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

Но раз уж заговорили про об ООП, давайте для начала поговорим понятие про полиморфизм в рамках ООП.

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

Для работы работы динамического полиморфизма нужен: базовый класс, пара наследников и виртуальные методы:

struct ITask {
virtual void Execute() = 0;
virtual ~ITask() = default;
};

struct FileDeleteTask : public ITask {
std::string path_;

FileDeleteTask(const std::string &path) : path_(path) {}

void Execute() override {
std::filesystem::remove(path_);
std::cout << "Deleted: " << path_ << std::endl;
}
};

struct S3FileUploadTask : public ITask {
std::string bucket_;
std::string path_;
std::shared_ptr<S3Client> client_;

S3FileUploadTask(const std::string &bucket, const std::string &path, const std::shared_ptr<S3Client> &client)
: bucket_{bucket}, path_{path}, client_{client} {}

void Execute() override {
client_->Upload(bucket_, path_);
std::cout << "Uploaded: " << bucket_ << ", pathL " << path_ << std::endl;
}
};


У нас есть интерфейс ITask и виртуальный метод Execute. Два других класса наследуются от ITask и переопределяют метод Execute. В задаче FileDeleteTask удаляется файл по заданному пути из файловой системы. В задаче S3FileUploadTask файл загружается в удаленное хранилище S3.

Заметим, у этих задач общий интерфейс(их можно выполнить), но они совершают разные действия.

Теперь мы можем использовать эти задачи:

void Producer1(const std::string &bucket, const std::vector<std::string> &paths,
const std::shared_ptr<S3Client> &client, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<S3FileUploadTask>(bucket, path, client));
}

void Producer2(const std::vector<std::string> &paths, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<FileDeleteTask>(path));
}

void Worker(std::deque<std::unique_ptr<ITask>> &tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
task.pop_front();
task->Execute();
}
}


У нас есть 2 продюсера, которые кладут задачи в очередь, и воркер, который выполнятся задачи из очереди.

В очереди хранятся уникальные указатели на базовый класс ITask. Это значит, что она может хранить объекты любых наследников интерфейса ITask.

Теперь самое важное: воркеру не нужно знать, какую конкретно задачу он сейчас достанет из очереди и какой конкретно продюсер ее туда положил. Единственное, что важно - общий интерфейс. Он позволяет единообразно выполнить задачи разных типов, даже не зная их исходный тип.

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

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

Extract common traits. Stay cool.

#OOP #cppcore
127👍9🔥81
​​Динамический полиморфизм. std::function
#новичкам

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

Есть другой прекрасный инструмент - std::function. Это обертка над всеми callable объектами, которая позволяет их единообразоно вызывать. Никаких иерархий, только функциональные объекты.

void Worker(std::deque<std::function<void()>>& tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
tasks.pop_front();
task(); // call callable
}
}

void Producer1(const std::string& bucket, const std::vector<std::string>& paths,
const std::shared_ptr<S3Client>& client, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
client_->Upload(bucket, path);
std::cout << "Uploaded: " << bucket << ", path " << path << std::endl;
});
}

void Producer2(const std::vector<std::string>& paths, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
std::remove(path.c_str());
std::cout << "Deleted: " << path << std::endl;
});
}


Теперь в очереди хранятся какие-то вызываемые объекты. Воркеру не важно, что это за объекты. Главное, что продюсеры могут разные функциональные объекты положить в один и тот же контейнер, попутно обернув их в std::function и тем самым полностью обезличив их. А легитимность такого мува достигается за счет того, что эти объекты имеют единый интерфейс - их можно вызвать без аргументов и не получить никакого возвращаемого значения.

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

std::function довольно интересно внутри устроен. После верхнеуровневого разговора про все полиморфизмы, вернемся к нему.

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

Extract common traits. Stay cool.

#cpp11
25👍14🔥6
This media is not supported in your browser
VIEW IN TELEGRAM
2 августа Яндекс проведет конференцию C++ Zero Cost Conf для разработчиков

Конференция пройдет в двух странах и трех городах: в Москве (офлайн + онлайн), Белграде (офлайн + онлайн) и Санкт-Петербурге (только офлайн).

В программе:
— «i, j, k и шаблоны: вспоминаем линейную алгебру», Ваня Ходор, Яндекс Лавка.
— «Hardening: текущий статус и перспективы развития», Роман Русяев и Юрий Грибов, Huawei.
— «Алиасинг памяти в компиляторе и в вашей программе», Константин Владимиров и Владислав Белов, Syntacore.

Полную программу выступлений по городам можно посмотреть на сайте.

Для участников в Москве пройдёт воркшоп по системе непрерывного профилирования Perforator с демонстрацией работы и локального использования, кейс-лаб по надёжности сервисов, а также код-гольф с решением задача на C++. Во всех городах проведут код-ревью с поиском и исправлением ошибок в коде.

Регистрация
🔥189👍6👎2
Динамический полиморфизм: указатели на функции и void указатели
#новичкам

C++ - разжиревший отпрыск С, поэтому в нем имеется возможность для динамического полиморфизма пользоваться сишными инструментами.

И два основных сишных инструмента дин полиморфизма - указатели на функции и void указатели.

Функции работают с аргументами и каждое имя функции при компиляции соответствует адресу этой функции в памяти. Даже если 2 функции имеют разные адреса, но одинаковый набор и порядок аргументов, в низкоуровневом коде они вызываются абсолютно единообразно. Поэтому есть такая сущность, как указатели на функции. Они могут хранить адреса любых функций с наперед заданной сигнатурой:

int x2(int i) {
return i * 2;
}
int square(int i) {
return i * i;
}
using IntFuncPtr = int (*)(int);

IntFuncPtr func_ptr;

// Вызываем x2 через указатель
func_ptr = x2;
std::cout << "x2(5) = " << func_ptr(5) << std::endl;

// Вызываем square через указатель
func_ptr = square;
std::cout << "square(5) = " << func_ptr(5) << std::endl;


В коде выше с помощью одного указателя вызываются 2 разные функции. Полиморфизм? Вполне! Только вот примерчик давайте по-серьезнее возьмем:

void *bsearch(const void *key, const void *ptr, std::size_t count,
std::size_t size, /* c-compare-pred */ *comp);

void *bsearch(const void *key, const void *ptr, std::size_t count,
std::size_t size, /* compare-pred */ *comp);

extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);


std::bsearch - функция, которая выполняет алгоритм бинарного поиска и возвращает либо найденный элемент, либо нулевой указатель, если элемента не было в массиве. Причем он может проводить поиск в массивах разных типов по разным правилам!

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

Единственная сложность - нужен дополнительный параметр size, с помощью которого задается байтовый размер типа элемента массива.

Ну и давайте все это применим:

int compare_doubles(const void *a, const void *b) {
static constexpr double EPSILON = 1e-9;
double diff = *(double *)b - *(double *)a;
if (std::fabs(diff) < EPSILON) {
return 0;
}
return (diff > 0) ? 1 : -1;
}

int compare_ints(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}

double double_arr[] = {5.5, 4.4, 3.3, 2.2, 1.1};
size_t double_size = sizeof(double_arr) / sizeof(double_arr[0]);

int int_arr[] = {10, 20, 30, 40, 50};
size_t int_size = sizeof(int_arr) / sizeof(int_arr[0]);



// Поиск в массиве double
double double_key = 3.30000000001; // Почти 3.3
double *double_res = (double *)std::bsearch(
&double_key, double_arr, double_size,
sizeof(double), compare_doubles);
// тут надо проверить на nullptr, но опустим это
std::cout << "Found double: " << *double_res << std::endl;

// Поиск в массиве int
int int_key = 30;
int *int_res =
(int *)std::bsearch(&int_key, int_arr, int_size,
sizeof(int), compare_ints);

std::cout << "Found int: " << *int_res << std::endl;


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

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

Act independently of input. Stay cool.

#cppcore #goodoldc
19👍10🔥4🤩2💯2
Нужно ли использовать нейронки в обучении программированию? На каких этапах?
🤣62😁155👎2😈2
Динамический полиморфизм: std::variant + std::visit
#опытным

Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же std::function - сочетание виртуальных функций и шаблонов. Но о подробностях реализации в другом посте.

Другой пример - std::variant + std::visit. std::variant - шаблонный класс, который может хранить в себе объект любого типа, который есть среди его шаблонных параметров. Эдакий типобезопасный union, без UB и прочей грязи.

std::variant<int, float, std::string> value;
value = 3.14f; // valid
value = 42; // also valid
value = std::string{"You are the best!"}; // again valid
value = 3.14; // ERROR: 3.14 is double and double is not in template parameter list


Вариант позволяет складывать фиксированный набор типов в один контейнер:
std::vector<std::variant<int, float, std::string>> vec;
vec.push_back(3.14f);
vec.push_back(42);


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

Через std::visit, конечно. Эта функция, которая принимает функциональный объект, который можно вызвать для любого типа, потенциально хранящегося в варианте, к самому объекту варианта. Объект std::variant на самом деле знает, какой тип в нем хранится, просто нам он об этом не рассказывает. А std::visit'у рассказывает:

struct PrintVisitor {
void operator()(int x) { cout << "int: " << x; }
void operator()(float x) { cout << "float: " << x; }
void operator()(string s) { cout << "string: " << s; }
};

std::variant<int, float, string> value;
value = 3.14f;

std::visit(PrintVisitor{}, value); // Prints "float: 3.14"


По-настоящему мощным это сочетание ставится при применении паттерна overload:

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>;

void Worker(const std::vector<var_t>& vec){
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);
});
}


Опять же, на этапе компиляции воркер понятия не имеет, какой тип реально хранится в варианте. Решение, какой обработчик вызвать, принимается в рантайме. Поэтому пара variant+visit реализует динамический полиморфизм, хоть и не без шаблонной магии.

Visit your closest. Stay cool.

#cpp17 #template
👍28🔥1712❤‍🔥2🤯1
Динамический полиморфизм: разделяемые библиотеки
#опытным

В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.

Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь libc и libstdc++ например неявно подгружаются на старте). Основную часть таких библиотек мы прописываем в опциях линковки.

Однако динамические библиотеки можно неявно подгружать прямо из кода! Для этого на разных системах существует разное системное апи, но для юниксов это dlopen+dlsym.

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

Тут пример будет довольно длинный, поэтому начнем с начала.

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

class PluginInterface {
public:
virtual int method() = 0;
};

extern "C" PluginInterface* create_plugin();

extern "C" void destroy_plugin(PluginInterface* obj);


Эта команда берет и реализует этот интерфейс:

#include "PluginInterface.hpp"
#include <iostream>

class MyPlugin : public PluginInterface {
public:
virtual void method() override;
};

int MyPlugin::method() {
std::cout << "Method is called\n";
return 42;
}

extern "C" PluginInterface* create_plugin() {
return new MyPlugin();
}

extern "C" void destroy_plugin(PluginInterface* obj) {
delete obj;
}


Также вы договорились, что каждая реализация интерфейса предоставляет 2 функции: создания и уничтожения наследников.

Функции create_plugin и destroy_plugin обязаны иметь сишную линковку, чтобы достать указатели на них по их имени из библиотеки с помощью dlsym:

#include "PluginInterface.hpp"
#include <dlfcn.h>
#include <iostream>

typedef PluginInterface *(*creatorFunction)();
typedef void (*destroyerFunction)(PluginInterface *);

int main() {
void *handle = dlopen("myplugin.so", RTLD_LAZY);
if (!handle) {
std::println("dlopen failure: {}", dlerror());
return 1;
}
creatorFunction create = reinterpret_cast<creatorFunction>(dlsym(handle, "create_plugin"));
destroyerFunction destroy = reinterpret_cast<destroyerFunction>(dlsym(handle, "destroy_plugin"));

PluginInterface *plugin = (*create)();
std::println("{}", plugin->method());
(*destroy)(plugin);
dlclose(handle);
}


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

Разве по имени функции можно получить указатель на нее? Похоже на какую-то рефлексию с первого взгляда.

Тут дело в именах функций и отображении их в символы бинарного файла при компиляции. В С нет никакого манглинга имен, поэтому в готовом бинарном файле можно найти символ, соответствующий названию функции, и связанный с ним адрес этой фукнции. Именно поэтому create_plugin и destroy_plugin помечены extern "C", чтобы их имена обрабатывались по правилам С.

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

Choose the right name. Stay cool.

#cppcore #OS #compiler
1026👍9🔥8
​​WAT
#новичкам

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Дан простой кусок кода:

#include <array>
#include <cstring>
#include <iostream>

int main(int argc, char *argv[]) {
const char *string{nullptr};
std::size_t length{0};
if (const bool thisIsFalse = argc > 100000;
thisIsFalse) {
string = "ABC";
length = 3;
}

std::array<char, 128> buffer;
std::memcpy(buffer.data(), string, length);

if (string == nullptr) {
std::cout
<< "String is null, so cancel the launch.\n";
} else {
std::cout << "String is not null, so launch the "
"missiles!\n";
}
}


Единственный вопрос: что выведется на экран при запуске программы без аргументов?

Подумайте несколько секунд.

"Да все очевидно же. string не меняется, поэтому сообщение об этом и выведется на экран".

Но мы же на плюсах пишем, тут невозможное становится возможным.

Например, при компиляции на gcc на О3 оптимизациях выводится String is not null, so launch the missiles!

"WAT? Где пруфы?"

А вот они.

Виновато конечно во всем ненавистное UB. Все грязные тряпки кидайте в него.

По стандарту, если в memcpy передать нулевой указатель, то поведение неопределено. Может случиться все, что угодно.

Это может произойти, только если количество аргументов запуска программы меньше 100000. То есть одна ветка приводит к UB, а вторая нет. И на основании этого gcc делает вывод, что порченная ветвь кода никогда не должна выполняться (так как UB означает, что поведение программы не определено, то компилятор может предполагать, что UB не должно происходить) и просто выкидывает эту ветку из ассемблера.

Уберите условие, либо memcpy, то вывод будет ожидаемым. Либо UB не будет, либо эвристики компилятора по-другому заработают.

Пишите качественный и безопасный код, чтобы не было таких неожиданностей.

Be safe. Stay cool.

#cppcore
23🔥9👍6❤‍🔥3
​​starts_with, ends_with
#новичкам

До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.

В нем появляются стандартные методы std::string/std::string_view .starts_with() и .ends_with():

constexpr bool starts_with(string_view sv) const noexcept;
constexpr bool starts_with(CharT c ) const noexcept;
constexpr bool starts_with(const CharT* s ) const;

constexpr bool ends_with(string_view sv )const noexcept;
constexpr bool ends_with(CharT c ) const noexcept;
constexpr bool ends_with(const CharT* s ) const;


Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.

const std::string url { "https://isocpp.org" };

// string literals
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";

// a single char:
if (url.starts_with('h') && url.ends_with('g'))
std::cout << "letters matched!\n";


Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.

Примерчик:

const std::vector<std::string> tokens { 
"<header>",
"<h1>",
"Hello World",
"</h1>",
"<p>",
"This is my super cool new web site.",
"</p>",
"<p>",
"Have a look and try!",
"</p>",
"</header>"
};

auto text = tokens |
std::views::filter([](const std::string& s) {
if (s.starts_with("<") || s.ends_with(">"))
return false;

return true;
});

for (const auto& str : text)
std::cout << str << std::endl;

// OUTPUT:
// Hello World
// This is my super cool new web site.
// Have a look and try!


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

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

Be expressive. Stay cool.

#cpp20 #STL
13🔥2811👍7❤‍🔥3🐳2
​​std::type_identity
#опытным

Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:

double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}


В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:

template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );


Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:

auto bounded = std::clamp(42, 3.14, 69.f); // ERROR!


Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.

Можно было сделать 3 отдельных параметра:

template<class T1, class T2, class T3>
constexpr const T& clamp( const T1& v, const T2& lo, const T3& hi );


Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.

Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:

template<class T>
struct type_identity { using type = T; };

template< class T >
using type_identity_t = type_identity<T>::type;


Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:

template <class T>
auto bound(T num, typename std::type_identity<T>::type low, typename std::type_identity<T>::type high) {
return std::clamp(num, low, high);
}

auto bounded = bound(25.5, 20, 25);


При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.

В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.

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

template<class T>
void foo(typename std::type_identity<T>::type arg) {}

foo<int>(42); // T жёстко задаётся как int
// foo(42); // Ошибка: вывод T невозможен!


Тоже самое для вариадиков:

template <typename... Ts>
void process(typename std::type_identity<std::tuple<Ts...>>::type data) {
}

process<int, double>(std::tuple{1, 2.0}); // OK
process(std::tuple{1, 2.0}); // ERROR, не указаны типы шаблонных параметров


Прикольный инструмент для тонкой настройки вашего шаблонного кода.

Спасибо @d7d1cd за идею для поста)

Turn off deduction when it is not needed. Stay cool.

#template #cpp20
926👍15🔥9🤯1
Качественных авторских каналов по плюсам не так много на просторах телеги. С++ такой сложный, что новички просто не смогут писать качественный контент, а большие сеньоры-помидоры вкачали весь опыт в хард-скиллы или не могут выделять достаточно времени на написание контента.

Благо есть свет в конце тоннеля, а точнее несколько звездочек на темном небе:

👉🏿 Канал Ильи Шишкова. ex-Яндекс, в настоящем СберТеховец, создатель, пожалуй, самых качественных курсов по С++ - "Пояса С++". На канале он рассказывает интересные кейсы с работы, как разрабатывает СУБД Pangolin, и как устроится в компанию мечты за толстую котлету.

👉🏿 Канал Ивана Ходора "this-notes". Работает в Яндекс Лавке, а на канале собирает дайджесты интересных экспертных статей по плюсам, хайлоаду и не только, да еще и с кратким обзором. Если лень самому искать инфу, то просто читайте статьи из его обзоров и уже преисполнитесь силушкой ITшной.

👉🏿 Ну а если хочется чуть больше углубиться в плюсы, получать подборки хороших лекции по С++, то вам в канал "Библиотека С++ разработчика".

Мы тут немного коллабимся и собрали все наши каналы в одной папке - https://t.me/addlist/jEIgjFluVUI0YjM6.

Так что если вас спросят, че почитать по плюсам - вы знаете, где взять ответ. Шарьте папку, качественный контент достоин внимания!

Make quality things. Stay cool.
👍169🔥5❤‍🔥4
​​Конфигурация и переменные окружения
#опытным

Любой серьезный сервис нуждается в конфигурации. Файлы конфигурации (JSON, YAML, INI) — популярный способ хранения настроек приложений. Так параметры можно хранить в репозитории, версионировать, да и просто удобно, когда все можно менять в одном месте и никак не менять команду запуска.

Однако не одними конфигами едины. Не всегда они подходят для решения определенных задач.

Возьмем например ключи шифрования. Не всегда они генерируются новые, для интеграции двух партнеров могут использоваться ключи, которые обновляются раз в год или раз в полгода. Безопасно ли ключ шифрования выставлять в конфиге?

Не совсем. Что если какой-нибудь умник после тестирования приложения случайно закоммитит ключ в репозиторий? Это серьезная опасность: репозиторий вашей команды скорее всего может читать любой сотрудник, у которого есть доступ к вашей системе совместной разработки. А если у вас еще сторонние лица имеют доступ к репе... Не завидую вам. Безопасники будут радостно потирать ладоши, когда будут вам пистоны вставлять за эту ошибку. Потом еще ключ перевыпускать скомпрометированный, долго и мучительно заменять его... Сам наступал на эти грабли, приятного мало.

{
"data_key": "qwerty123" // Утечка при публикации кода!
}


Да и хранить ключ в открытом виде в файле на сервере такое себе. А если кто-нибудь подглядит?

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

# config.yml (попадает в Git)
db:
host: db.example.com
username: admin
password: "P@ssw0rd123!" # Утечка при публикации кода!


А как с докерами и кубернетисами вашими работать? Иметь 100500 образов с разными настройками кредов и множить их постоянно? Выглядит, как не очень расширяемое решение.

Конечно же никто не хранит в конфигах чувствительные данные и специфичные для конкретного инстанса переменные. Вместо этого используют переменные окружения.

Переменные окружения можно установить видимыми только для конкретного запущенного docker контейнера:

docker run -e MY_VAR=value my_image


В k8s можно брать переменые окружения из отдельно развернутого и защищенного Vault. В этом случае вообще отсутсвует явное указание секрета:

env:
- name: MY_VAR
- value: vault:my_group/my_service#my_var


Переменные окружения не попадают в репозиторий -> нет компрометации секретов.

Можно без изменения конфига на одном и том же сервере тестировать приложение в разных контурах:

# Local
export DB_HOST=localhost

# Dev
export DB_HOST=dev-db.example.com


В общем, переменные окружения в приложении - полезная вещь, не стоит ими принебрегать.

К чему это я и причем здесь С++?

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

Protect your secrets. Stay cool.

#goodpractice #tools
22👍2214🔥7💯4❤‍🔥2
​​std::getenv
#новичкам

Переменные окружения - это пары "ключ-значение", которые хранятся в операционной системе и доступны всем процессам. Они часто используются для:

- Конфигурации приложений
- Хранения чувствительных данных (паролей, ключей API)
- Управления поведением программ

Самый банальный пример - PATH. В этой переменной окружения находится список путей для поиска исполняемых файлов системой. Добавив путь к своей утилите в эту переменную, вы сможете ее запускать без указания полного пути.

Или например, более приближеный к плюсам пример, LD_LIBRARY_PATH. Это список путей, в котором линкер ищет указанные при линковке библиотеки.

И мы можем прочитать из плюсового кода переменные окружения с помощью С++11 функции std::getenv:

#include <cstdlib>

char* std::getenv(const char* name);


Это скоммунизженная из Сей функция, которая принимает имя переменной окружения и возвращает ее содержимое. Если искомой переменной не существует, возвращается nullptr.

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

При запуске программы ОС копирует переменные окружения, видимые родительскому процессу, внутрь программы и таким образом они окружение программы перестает зависеть от внешнего мира.

Допустим, пишите вы какой-нибудь свой клиент-сервер на Boost.Asio. Хочется конфигурировать клиента адресом и портом сервера извне, чтобы иметь возможность по-разному запускать клиента локально и, допустим, через docker-compose. Конфиг и его парсилку писать довольно муторно, а адекватную парсилку аргументов командной строки - еще сложнее. Даже если использовать готовые решения в виде json парсера и boost.program_options.

Вместо этих решений можно передавать креды подключения к серверу через переменную окружения:

#include <boost/asio.hpp>
#include <iostream>
#include <cstdlib>

using boost::asio::ip::tcp;

int main() {
try {
// HERE
const char* host = std::getenv("SERVER_HOST");
const char* port = std::getenv("SERVER_PORT");

if (!host || !port) {
std::cerr << "Please set SERVER_HOST and SERVER_PORT environment variables\n";
return 1;
}

boost::asio::io_context io_context;

// Создаем и соединяем сокет
tcp::socket socket(io_context);
tcp::resolver resolver(io_context);
boost::asio::connect(socket, resolver.resolve(host, port));

// Отправляем тестовое сообщение
std::string message = "Hello from Boost.Asio client!\n";
boost::asio::write(socket, boost::asio::buffer(message));

// Читаем ответ (до символа новой строки)
boost::asio::streambuf response;
boost::asio::read_until(socket, response, '\n');

// Выводим ответ
std::istream is(&response);
std::string reply;
std::getline(is, reply);
std::cout << "Server replied: " << reply << std::endl;

} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}

return 0;
}


Всего две строчки и никакой мороки! Очень удобная и полезная функция.

Explore your enviroment. Stay cool.

#cpp11
27👍3410🔥7❤‍🔥3