
⚡️ Заказывайте в AI-каталоге — получайте скидку!
5% скидка на размещения в каналах, которые подобрал AI. Промокод: TELEGA-AI
Подробнее

РегистрацияВойтиВойти
Скидка 3,5% на первые три заказа
Получите скидку на первые три заказа!
Зарегистрируйтесь и получите скидку 3,5% на первые рекламные кампании — промокод активен 7 дней.
30.1

Грокаем C++
5.0
7
Интернет технологии
627
11
Авторский канал о программировании на С++ и базе computer science. Простым и легким слогом рассказываем про сложные концепции С++. Самая активная и вовлеченная аудитория в тематике.
Поделиться
В избранное
Купить рекламу в этом канале
Формат:
keyboard_arrow_down
- 1/24
- 2/48
- 3/72
- Нативный
- 7 дней
- Репост
1 час в топе / 24 часа в ленте
Количество:
%keyboard_arrow_down
- 1
- 2
- 3
- 4
- 5
- 8
- 10
- 15
Стоимость публикации:
local_activity
8 391.60₽8 391.60₽local_mall
0.0%
Осталось по этой цене:0
Последние посты канала
Динамический полиморфизм. ОО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 #cppcore2834
12:00
23.07.2025
Динамический полиморфизм. 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.
#cpp112876
12:00
24.07.2025
Динамический полиморфизм: указатели на функции и 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 #goodoldc3084
16:39
25.07.2025
play_circleВидео недоступно для предпросмотра
2 августа Яндекс проведет конференцию C++ Zero Cost Conf для разработчиков
Конференция пройдет в двух странах и трех городах: в Москве (офлайн + онлайн), Белграде (офлайн + онлайн) и Санкт-Петербурге (только офлайн).
В программе:
— «i, j, k и шаблоны: вспоминаем линейную алгебру», Ваня Ходор, Яндекс Лавка.
— «Hardening: текущий статус и перспективы развития», Роман Русяев и Юрий Грибов, Huawei.
— «Алиасинг памяти в компиляторе и в вашей программе», Константин Владимиров и Владислав Белов, Syntacore.
Полную программу выступлений по городам можно посмотреть на сайте.
Для участников в Москве пройдёт воркшоп по системе непрерывного профилирования Perforator с демонстрацией работы и локального использования, кейс-лаб по надёжности сервисов, а также код-гольф с решением задача на C++. Во всех городах проведут код-ревью с поиском и исправлением ошибок в коде.
Регистрация
2797
13:39
25.07.2025
imageИзображение не доступно для предпросмотра
Что может помочь плюсисту сделать свой код чище и лучше? Только вредные советы, только хардкор! 🔥
Если вы ищете книгу, где собраны нетривиальные кейсы по плюсам, да еще и с юмором и мемами, то «Вредные советы для С++ программистов» — для вас.
Автор — Андрей Карпов, сооснователь компании PVS-Studio по разработке статического анализатора кода. В книге программист с двадцатилетним опытом рассказывает, как точно не стоит писать код. Каждый совет сопровождается подробным разбором с объяснением сложных и неочевидных аспектов C++.
«Вредные советы для С++ программистов» подойдут новичкам для изучения реальных примеров с пояснениями. А профессионалы и опытные разработчики смогут погрузиться в тонкости C++ и оценить иронию автора. Прокачивайте навыки, учитесь избегать распространенных ловушек и посмотрите на программирование под новым углом!
Хорошие новости: полную версию книги можно получить в PDF-файле одним из двух способов:
🔵подпишитесь на дайджест статей от PVS-Studio;
🔵подпишитесь на Telegram-бот.
Все подробности по ссылке, там же вы сможете скачать демоверсию «Вредных советов».
А еще больше полезной информации о буднях разработчика и нестандартных кейсах вы сможете узнать из авторского канала Андрея Карпова «Бестиарий программирования». Читайте и подписывайтесь!
Хорошего дня и чистого кода!❤
2551
10:01
28.07.2025
Динамический полиморфизм: 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 #template2358
13:00
28.07.2025
Динамический полиморфизм: разделяемые библиотеки
#опытным
В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.
Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь 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 #compiler2021
12:00
29.07.2025
imageИзображение не доступно для предпросмотра
Нужно ли использовать нейронки в обучении программированию? На каких этапах?
2858
14:00
27.07.2025
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.
#cppcore1459
12:00
31.07.2025
close
С этим каналом часто покупают
Отзывы канала
keyboard_arrow_down
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
5.0
0 отзыва за 6 мес.
m
**cromarketing@****.ru
на сервисе с августа 2023
24.01.202513:24
5
Высокая конверсия
Показать еще
Лучшие в тематике
Новинки в тематике
Статистика канала
Рейтинг
30.1
Оценка отзывов
5.0
Выполнено заявок
29
Подписчики:
9.4K
Просмотры на пост:
lock_outline
ER:
20.3%
Публикаций в день:
1.0
CPV
lock_outlineВыбрано
0
каналов на сумму:0.00₽
Подписчики:
0
Просмотры:
lock_outline
Перейти в корзинуКупить за:0.00₽
Комментарий