Грокаем C++
9.46K 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
Динамический полиморфизм. ОО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
👍29🔥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🔥6❤‍🔥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🔥8❤‍🔥3
​​Третий аргумент main
#новичкам

Почти всегда вы пишите функцию main вот так:

int main() {
// some code
}


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

int main (int argc, char* argv[]) {
// argc - количество переданных аргументов
// argv - массив из переданных аргументов

// some parsing
}


Однако в стандарте описан еще один способ определения main:


int main(/ implementation-defined /) {body}


Стандарт разрешает компиляторам давать возможность пользователям как-то по-другому задавать аргументы для main. И хоть это будет не переносимо, нам не всегда нужна кроссплатформенность.

Самая часто встречающаяся нестандартная сигнатура main следующая:

int main(int argc, char*  argv[], char* envp[]) {}


Третий аргумент main - это массив строк переменных окружения в формате
"KEY=value". Массив завершается null pointer'ом.

Программа получает копию переменных окружения родительского процесса (например, терминала или скрипта). Только лишь независимую копию: изменение набора и значения переменных снаружи программы никак не влияет на то, что происходит внутри нее.

Вот минимальный примерчик:

#include <iostream>

int main(int argc, char* argv[], char* envp[]) {
std::cout << "Environment variables:\n";

for (char** env = envp; *env != nullptr; env++) {
std::cout << *env << "\n";
}

return 0;
}


Возможный вывод:

PATH=/usr/local/bin:/usr/bin:/bin
USER=grokaem_cpp
...


Вы не так часто можете увидеть этот формат сигнатуры main по уже очевидным для вас причинам:

- нестандарт
- а самое главное - это дело надо парсить. Засовывать в мапу какую-то и искать потом по ключу нужную переменную. А зачем, если есть std::getenv или его брат getenv из Сей.

Рассказываю про это, чтобы вы при встрече в таким форматом аргументов main не думали, что что-то базовое упустили при изучении плюсов. Ну или просто для расширения кругозора)

Expand your horizons. Stay cool.

#NONSTANDARD #goodoldc
👍4618🔥4😱4
​​environ
#опытным

В POSIX-совместимых системах (Linux, macOS, BSD) существует еще один альтернатива параметру envp в функции main() и функции std::getenv() для получения значений переменных окружения. Это глобальная переменная extern char** environ, которая предоставляет прямой доступ ко всему окружению процесса.

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

#include <stdio.h>

extern char** environ;

void foo() {
for(char** env = environ; *env != NULL; env++) {
printf("%s\n", *env);
}
}


В остальном, особенности работы такие же, как и у envp.

Но если мы уже зашли по колено в Posix, то там есть функция setenv, которая позволяет менять переменные окружения уже в ходе выполнения программы:

#include <stdlib.h>  

int setenv(const char *envname, const char *envval, int overwrite);


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

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

Интересно, как много в С|С++ методов получения окружения процесса. Но рекомендуется использовать конечно стандартный вариант std::getenv.

Be visiable. Stay cool.

#NONSTANDARD
2016👍8🔥8
Офер в Яндекс за 48 часов: ищем бэкендеров

В команду нужны опытные бэкенд-разработчики на C++, Python, Java и Go. Приглашаем на Мультитрек — онлайн-программу быстрой адаптации.

Всего за 2 дня вы можете получить офер:
• До 18 августа подать заявку и пройти предварительный отбор
• 23 августа решить задачи на технических секциях
• 24 августа пройти финальное собеседование и получить офер
После этого будет возможность поработать с тремя командами и выбрать проект по душе.

Создаём технологии, которые меняют мир. Присоединяйтесь! Оставляйте заявку на сайте.
👎138🔥4👍3😁3🗿2
Ревью
#новичкам

Пролистывал намедни один тг канал по С++, его название начинается на Senior и заканчивается на С++ Developer. Там обычно постится очень "качественный контент" и мне на глаза попался код, который я бы хотел закинуть вам на тряпкозакидательство.

В рамках #ревью мы приводим кусок кода, а вы в комментариях прожариваете его до полного well done: говорите, что вам не нравится, и как это исправить. Комментатора с самым большим количеством отмеченных проблем упомянем в завтрашнем посте с разбором.

struct Message {
int data;
};
std::queue<Message *> msgQueue;

void sender() {
for (int i = 0; i < 20; ++i) {
Message *msg = new Message();
msg->data = i;
msgQueue.push(msg);
std::cout << "Sent: " << msg->data << std::endl;
}
}

void receiver() {
while (true) {
if (msgQueue.empty()) {
break;
}
Message *msg = msgQueue.front();
msgQueue.pop();
std::cout << "Received: " << msg->data << std::endl;
delete msg;
}
}
int main() {
std::thread t1(sender);
std::thread t2(receiver);
t1.join();
t2.join();
return 0;
}


Вот такой код. Под оригинальным постом с этим кодом коллеги призвали руки рубить за такой код. Давайте сделаем так, чтобы он не вызывал испанского стыда, а только возвышенные чувства платонической любви к С++.

Раз, два, три, код в порядок приведи!

Critique your decisions. Stay cool.
124😱8🔥5👍4👎2😁2
​​Разбор ревью
#новичкам

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

Было непросто выбрать самый эффективный по критике комментарий, потому что некоторые люди предлагали странные решения. В итоге, мы выбрали @seweeex и вот его коммент. Давайте похлопаем ему 👏👏👏.

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

Поехали разбирать.

🔞 Зачем-то в очереди хранятся сырые указатели. Смысла в этом особого нет, кроме как подложить себе свинью на будущее и поджечь жёпы комментаторов. Тут даже умные указатели не нужны, зачем дополнительные аллокации? В очереди можно хранить сами объекты и никаких проблем с менеджментом памяти не будет.

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

🔞 Слон в посудной лавке здесь - это конечно отсутствие синхронизации на очереди. Это в принципе ub и дальше не о чем говорить. Нужна не стандартная, а потокобезопасная очередь.

Очередь должна быть блокирующей, чтобы не тратить активно ресурс CPU на ожидание нового сообщения. Это решается с помощью кондвара.

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

🔞 Бесконечный цикл в ресивере выглядит не очень. Если можно не писать бесконечных циклов и не вставлять брейки, то лучше этого не делать. break и continue усложняют понимание кода. Лучше перенести забирание элемента из очереди прям в шапку цикла.

🔞 Гонка на потоконебезопасном логировании. Нужен мьютекс, чтобы сообщения не интерферировали.

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

#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <optional>
#include <print>

struct Message {
int data;
};

template<typename T>
class ThreadSafeQueue {
public:
void push(T msg) {
{
std::lock_guard lock(mutex_);
queue_.push(std::move(msg));
}
cv_.notify_one();
}

std::optional<T> pop() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || stopped_; });

if (stopped_ && queue_.empty()) {
return std::nullopt;
}

auto msg = std::move(queue_.front());
queue_.pop();
return msg;
}

void stop_receive() {
stopped_ = true;
cv_.notify_all();
}

private:
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::atomic<bool> stopped_ = false;
};

void println(const std::string& str) {
static std::mutex mtx;
std::lock_guard lock(mtx);
std::cout << str << std::endl;
}

void sender(ThreadSafeQueue<Message>& msgQueue) {
for (int i = 0; i < 20; ++i) {
auto msg = Message(i);
println("Sent: " + std::to_string(msg.data));
msgQueue.push(std::move(msg));
}
msgQueue.stop_receive();
}

void receiver(ThreadSafeQueue<Message>& msgQueue) {
while (auto msg = msgQueue.pop()) {
println("Received: " + std::to_string(msg.value().data));
}
}

int main() {
ThreadSafeQueue<Message> msgQueue;
std::thread t1(sender, std::ref(msgQueue));
std::thread t2(receiver, std::ref(msgQueue));

t1.join();
t2.join();
}


Пишите свои дополнения, если что-то забыли.

Make things better. Stay cool.
1628👍9👏5🤨1
В России можно посещать IT-мероприятия хоть каждый день: как оффлайн, так и онлайн

Но где их находить? Как узнавать о них раньше, чем когда все начнут выкладывать фотографии оттуда?

Переходите на канал IT-Мероприятия России. В нём каждый день анонсируются мероприятия со всех городов России

📆 в канале размещаются как онлайн, так и оффлайн мероприятия;
👩‍💻 можно найти ивенты по любому стеку: программирование, frontend-backend разработка, кибербезопасность, дата-аналитика, osint, devops и другие;
🎙 разнообразные форматы мероприятий: митапы с коллегами по цеху, конференции и вебинары с известными опытными специалистами, форумы и олимпиады от важных представителей индустрии и многое другое

А чтобы не искать по разным форумам и чатам новости о предстоящих ивентах:

🚀 IT-мероприятия Россииподписывайся и будь в курсе всех предстоящих мероприятий!
😁54👍3🔥1😢1
​​or and not
#новичкам

В С/C++ всегда был не очень дружелюбный синтаксис операторов. Показать вот такой код человеку, который не в зуб ногой в программировании:

if (x > 0 && y < 10 || !z) {
// ...
}

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

Однако знали ли вы, что в С/C++ есть альтернативный синтаксис токенов? Символы операторов заменяются на короткие слова и код выше становится почти питонячьим:

if (x > 0 and y < 10 or not z) {
// ...
}


Выглядит свежо! Хотя было доступно еще с С++98.

Вот полный список альтернативных токенов:

&& - and
&= - and_eq
& - bitand
| - bitor
~ - compl
!= - not_eq
|| - or
|= - or_eq
^ - xor
^= - xor_eq
{ - <%
} - %>
[ - <:
] - :>
# - %:
## - %:%:


Последние токены для скобок - это конечно дичь. Но остальные - вполне интересные варианты.

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

А вы используете в продакшен коде альтернативные токены?

Evolve. Stay cool.

#fun #goodoldc
26👍9🔥6👎3😱3