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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​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
​​Висячие ссылки в лямбдах
#новичкам

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

В 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
Динамический полиморфизм. ОО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
Динамический полиморфизм: указатели на функции и 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
Динамический полиморфизм: разделяемые библиотеки
#опытным

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

Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь 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
1025👍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