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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Unity build
#опытным

Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.

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

Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали std::vector<int> - компилируем эту инстанциацию. В другом написали std::vector<int> - заново скомпилировали эту инстанциацию. То есть большая проблема в компиляции одного и того же кучу раз.

Но помимо компиляции вообще-то есть линковка. И чем больше единиц трансляции, библиотек и все прочего, тем больше времени нужно линковщику на соединение все этого добра в одно целое.

Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)

Условно. Есть у вас 2 цппшника и один хэдэр:

// header.hpp
#pragma once
void foo();

// source1.cpp
#include "header.hpp"
void foo() {
std::cout << "You are the best!" << std::endl;
}

// source2.cpp
#include "header.hpp"
int main() {
foo();
}


Вы все цппшники подключаете в один файл unity_build.cpp:

#include "source1.cpp"
#include "source2.cpp"


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

Или нет?

У этой техники есть ряд недостатков:

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

Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.

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

У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.

Solve the problem. Stay cool.

#cppcore #compiler #tools
122🔥7👍6😁2🗿1
​​Ответ
#новичкам

Многие из вас подумали, что будет ошибка компиляции. В целом, логичная цепочка мыслей: ну как же можно мувнуть данные из константной ссылки? Она же неизменяема.

Но она все же неверная. Правильный ответ: на консоль выведется "copy ctor".

Копия? Мы же муваем!

Сейчас разберемся. Но для начало вспомним сам пример:

#include <iostream>

struct Test {
Test() = default;
Test(const Test &other) {
std::cout << "copy ctor" << std::endl;
}
Test(Test &&other) {
std::cout << "move ctor" << std::endl;
}
Test &operator=(const Test &other) = default;
Test &operator=(Test &&other) = default;
~Test() = default;
};

int main() {
Test test;
const Test &ref = test;
(void)std::move(ref);
auto emigma = std::move(ref);
}


На самом деле проблема в нейминге. Все вопросы к комитету. Это они имена всему плюсовому раздают.

std::move ничего не мувает. Она делает всего лишь static_cast. Но не просто каст к правой ссылке, это не совсем корректно. Посмотрим на реализацию std::move:

template <class T>  
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}


Обратите внимание, что от типа отрезается любая ссылочность и только затем добавляется правоссылочность. Но константность-то никуда не уходит. По сути результирующий тип выражения std::move({константная левая ссылка}) это константная правая ссылка.

Чтобы это проверить, перейдем на cppinsights:

Test test;
const Test &ref = test;
using ExprType = decltype(std::move(ref));

// под капотом ExprType вот чему равен

using ExprType = const Test &&;


Так как мы просто кастуем к валидному типу, мув успешно отрабатывает, но строчка (void)std::move(ref); не дает в консоли никакого вывода, потому что никаких новых объектов мы не создаем.

Теперь про копирование. Вспомним правила приведения типов. const T&& может приводится только к const T&. То есть единственный конструктор, который может вызваться - это копирующий конструктор.

Интересная ситуация, конечно, что "перемещение" может приводить к копированию в плюсах. Но имеем, что имеем. Терпим и продолжаем грызть гранит С++.

Give a proper name. Stay cool.

#cppcore #template
34👍14🔥7😁4🥱2💯1
​​Гарантии безопасности исключений
#новичкам

Программа на С++ - очень интересное явление. Вроде мощный, подкаченный, умный и скоростной парень. Но вот проблема. Ходить не умеет нормально. То в ноги себе стреляет, то падает периодически. В общем, беда у него с ходьбой. У этого могут быть разные причины. Все из них даже трудно в голове удержать. Но сегодня обсудим, какие есть гипсы, лангеты и костыли, которые помогут этому парню нормально ходить при работе с исключениями.

Даже не зная, что вы работаете с исключениями - вы уже работаете с ними. Даже обычный, казалось бы, безобидный new может кинуть std::bad_alloc. И это core языка. Стандартная библиотека пронизана исключениями.

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

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

Итак, существует 3 гарантии безопасности исключений:

Базовая гарантия. Формулировка разнится, но более общая звучит так: "после возникновения исключения и его обработки программа должна остаться в согласованном состоянии". Теперь на рабоче-крестьянском: не должно быть утечек ресурсов и должны сохраняться все инварианты классов. С утечками, думаю, все понятно. Инвариант - некое логически согласованное состояние системы. Если класс владеет массивом и содержит поле для его размера, то инвариантом этого класса будет совпадение реального размера массива со значением поля-размера. Для класса "легковая машина" инвариантом будет количество колес - 4. Обычная машина без одного или нескольких колес просто не едет. И для того, чтобы машина корректно работала, количество колес должно быть одинаково - 4. Или например, нельзя, чтобы дата создания чего-то была больше текущего дня, ну никак. Вот такие штуки должны сохраняться.

Строгая гарантия. Если при выполнении операции возникает исключение, операция не должна оказать на систему никакого влияния. То есть пан или пропал. Либо вся операция выполняется успешно и ее результат применяется, либо система откатывается в состояние до выполнения операции. Это свойство программы называется транзакционностью.

Гарантия отсутствия исключений. Ни при каких обстоятельствах не будет брошено исключение. Легко сказать, но тяжело сделать. С++ и его стандартная библиотека разрабатывались с учетом использования исключений. И это накладывает свои ограничения на отсутствие исключений. Мы все-таки не на Go пишем.

Ну и есть еще одна гарантия - отсутствие каких-либо гарантий. Мама - анархия, во всей красе.

Тема важная, будем потихоньку ее разбирать с примерами по каждой гарантии.

Be a guarantor. Stay cool.

#cppcore
23👍14🔥7👎2
​​Базовая гарантия исключений
#новичкам

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

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

class IntArray {
int *array;
std::size_t nElems;

public:
// ...

~IntArray() { delete[] array; }

IntArray(const IntArray &that); // nontrivial copy constructor
IntArray &operator=(const IntArray &rhs) {
if (this != &rhs) {
delete[] array;
array = nullptr;
nElems = rhs.nElems;
if (nElems) {
array = new int[nElems];
std::memcpy(array, rhs.array, nElems * sizeof(*array));
}
}
return *this;
}

// ...
};


Внутренний инвариант класса IntArray - член array является валидным (возможно, нулевым) указателем, а член nElems хранит количество элементов в массиве. В операторе присваивания освобождается память текущего array'я и присваивается значение счётчику элементов nElems до выделения нового блока памяти для копии. В результате, если из new вылетит исключение, то array будет нулевым, а размер массива нет. Это нарушение инварианта и таким объектом просто небезопасно пользоваться. Метод size потенциально вернет ненулевой размер, а закономерное использование следом оператора[] приведет к неопределенному поведению.

Код, который избегает подобных нежелательных эффектов, называется exception safe. То есть предоставление базовой гарантии уже говорит о том, что ваш код "exception safe".

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

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

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

Здесь кстати можно провести параллель с мувнутыми объектами: ими тоже особо не попользуешься и по хорошему их надо просто удалить.

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

auto db = std::make_shared<DBConnection>(credentials);
try {
auto result = db->Execute("UPDATE ...");
process(result);
} catch (std::exception& ex) {
std::cout << "We can cannot rely on table state and must retry with that in mind" << std::endl;
// retry
}


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

Provide guarantees. Stay cool.

#cppcore
👍119🔥3😁3👎2
​​Строгая гарантия исключений
#новичкам

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

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

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

Давайте перепишем оператор присваивания класс IntArray из предыдущего поста так, чтобы он предоставлял строгую гарантию:

class IntArray {
int *array;
std::size_t nElems;

public:
// ...

~IntArray() { delete[] array; }

IntArray(const IntArray &that); // nontrivial copy constructor

IntArray &operator=(const IntArray &rhs) {
int *tmp = nullptr;
if (rhs.nElems) {
tmp = new int[rhs.nElems];
std::memcpy(tmp, rhs.array, rhs.nElems * sizeof(*array));
}
delete[] array;
array = tmp;
nElems = rhs.nElems;
return *this;
}

// ...
};


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

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

template <typename T>
class vector {
private:
T *data = nullptr;
size_t size = 0;
size_t capacity = 0;

void reallocate(size_t new_capacity) {
// allocate memory
T *new_data =
static_cast<T *>(::operator new(new_capacity * sizeof(T)));
size_t new_size = 0;

try {
// Move or copy elements
for (size_t i = 0; i < size; ++i) {
new (new_data + new_size) T(std::move_if_noexcept(data[i]));
++new_size;
}
} catch (...) {
// Rollback in case of exception
for (size_t i = 0; i < new_size; ++i) {
new_data[i].~T();
}
::operator delete(new_data);
throw;
}

// cleanup
// ...
}

public:
void push_back(const T &value) {
if (size >= capacity) {
size_t new_capacity = capacity == 0 ? 1 : capacity * 2;

// save for rollback
T *old_data = data;
size_t old_size = size;
size_t old_capacity = capacity;

try {
reallocate(new_capacity);
} catch (...) {
// restore
data = old_data;
size = old_size;
capacity = old_capacity;
throw;
}
}

// actually insert element
// ...
}

};


в хэлпере reallocate используется std::move_if_noexcept, который условно кастит в rvalue ссылке, если мув конструктор noexcept. И только в этом случае можно предоставить строгую гарантию: если вы уже повредили один из исходных объектов, его уже никак не восстановить. А безопасное перемещение элементов гарантирует готовый к использованию новый расширенный буфер.

Be strong. Stay cool.

#cppcore
114🔥8👍7👎3
​​Гарантия отсутствия исключений
#новичкам

Переходим к самой сильной гарантии - отсутствие исключений.

В сам язык С++(new, dynamic_cast), и в его стандартную библиотеку в базе встроены исключения. Поэтому писать код без исключений в использованием стандартных инструментов практически невозможно. Вы конечно можете использовать nothrow new и написать свой вариант стандартной библиотеки и других сторонних решений. И кто-то наверняка так делал. Но в этом случае разработка как минимум затянется, а как максимум вы бросите это гиблое дело.

Поэтому повсеместно предоставлять nothow гарантии с использованием стандартных инструментов не всегда реалистично.

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

Но для начала проясним термины.

Под гарантией отсутствия исключений подразумевается обычно 2 понятия: nothrow и nofail.

nothrow подразумевает отсутствие исключений, но не отсутствие ошибок. Говорится, что ошибки репортятся другими средствами(в основном через глобальное состояние, потому что деструктор ничего не возвращает) или полностью скрываются и игнорируются.

Примером сущностей с nothrow гарантией является деструкторы. С С++11 они по-умолчанию помечены noexcept. В основном это сделано для того, чтобы при раскрутке стека не получить double exception.
Но деструкторы могу фейлиться. Просто никаких средств, кроме глобальных переменных для репорта ошибок невозможно использовать. Они ведь ничего не возвращают, а исполняются скрытно от нас(если вы используете RAII конечно).

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

Например в swap-идиоме std::swap и мув-конструкторы используются для определения небросающего оператора присваивания.

nofail гарантиями также должны обладать функторы-коллбэки модифицирующих алгоритмов. std::sort не предоставляет никаких гарантий на состояние системы, если компаратор бросит эксепшн.

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

Provide guarantees. Stay cool.

#cppcore #cpp11
15👍8🔥5❤‍🔥3😁3👎1
​​WAT
#опытным

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

"Век живи - век учись" - сказал Луций Сенека.

"Век живи - век учи С++" - реалии нашей жизни.

Просто посмотрите на следующий код:

struct Foo
{
void Bar();
};

void Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Foo::Bar()
{
printf("Foofoo!");
}

int main()
{
Foo f;
f.Bar();
return 0;
}


И он компилируется.

WAT?

Это называется injected class name. Имя класса доступно из скоупа этого же класса. Так сделано для того, чтобы поиск имени X внутри класса X всегда разрешался именно в этот класс.

Такое поведение может быть полезно в таком сценарии:

void X() { }
class X {
public:
static X Сreate() { return X(); }
};


injected class name гарантирует, что из метода Сreate будет возвращен именно инстанс класса Х, а не результат вызова функции Х.

Это также полезно внутри шаблонов классов, где имя класса можно использовать без списка аргументов шаблона, например, используя просто Foo вместо полного идентификатора шаблона Foo<blah, blah, blah>.

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

Так что это не у вас в глазах двоится, это плюсы такие шебутные)

Find yourself within. Stay cool.

#cppcore
10🔥36🤯119😁64👍4
​​data race
#новичкам

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

Начнем с data race. Это по сути единственная категория, которая четко определена в стандарте С++.

Скажем, что два обращения к памяти конфликтуют, если:

- они обращаются к одной и той же ячейке памяти.
- по крайней мере одно из обращений - запись.

Так вот гонкой данных называется 2 конфликтующих обращения к неатомарной переменной, между которыми не возникло отношение порядка "Произошло-Раньше".

Если не вдаваться в семантику отношений порядков, то отсутствие синхронизации с помощью примитивов(мьютексов и атомиков) при доступе к неатомикам карается гонкой данных и неопределененным поведением.

Простой пример:

int a = 0;

void thread_1() {
for (int i = 0; i < 10000; ++i) {
++a;
}
}

void thread_2() {
for (int i = 0; i < 10000; ++i) {
++a;
}
}

std::jthread thr1{thread_1};
std::jthread thr1{thread_2};
std::cout << a << std::endl;


В двух потоках пытаемся инкрементировать a. Проблема в том, что при выводе на консоль a не будет равна 20000, а скорее всего чуть меньшему числу. Инкремент инта - это неатомарная операция над неатомиком, поэтому 2 потока за счет отсутствия синхронизации кэшей будут читать и записывать неактуальные данные.

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

Have an order. Stay cool.

#cppcore #concurrency
32😁15👍10🔥3👎1
​​Самая надежная гарантия отсутствия исключений
#опытным

Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать try-catch блоки. Исключения - это не zero-cost абстракция. throw требуют динамические аллокации, catch - RTTI, а в машинном коде компилятор обязан генерировать инструкции на случай вылета исключений. Плюс обработка исключений сама по себе медленная.

Поэтому некоторые и стараются минимизировать использование исключений и максимально использовать noexcept код.

Но можно решить проблему накорню. Так сказать отрезать ее корешок под самый корешок.

Есть такой флаг компиляции -fno-exceptions. Он запрещает использование исключений в программе. Но что значит запрет на использование исключений?

👉🏿 Ошибка компиляции при выбросе исключения. А я говорил, что под корень рубим. Вы просто не соберете программу, которая кидает исключения.

int main() {
throw 1; // even this doesn't compile
}


👉🏿 Ошибка компиляции при попытке обработать исключение. Ну а че, если вы живете в мире без исключений, зачем вам их обрабатывать?

int main() {
// even this doesn't compile
try {
} catch(...) {
}

}


👉🏿 Можно конечно сколько угодно жить в розовом мире без исключений, но рано или поздно придется использовать чужой код. Что будет, если он выкинет исключение?

std::map<int, int> map;
std::cout << map.at(1) << std::endl;


Моментальное завершение работы
. Оно как бы и понятно. Метод мапы at() кидает std::out_of_range исключение, если ключа нет в мапе. Обрабатывать исключение нельзя, поэтому чего вола доить, сразу терминируемся. И никакой вам раскрутки стека и graceful shutdown. Просто ложимся и умираем, скрестив ручки.

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

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

Как тогда код писать? А об этом через пару постов.

Handle errors. Stay cool.

#cppcore #compiler
👍2511🔥6😁3❤‍🔥2🤔1
​​Что не так с модулями?
#опытным

Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.

Модули - это новая фундаментальная единица организации кода, которая должна дополнить и в идеале(в мечтах комитета) заменить старую концепцию заголовочных файлов.

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

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

Короткий пример:

// math.cppm - файл модуля
export module math; // Объявление модуля

import <vector>; // Импорт, а не включение

// Макросы НЕ экспортируются!
#define PI 3.14159

// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);

// Внутренние функции скрыты
void internal_helper();


и его использование:

// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода

// Используем экспортированную функцию
double area = calculate_circle_area(10);

// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются


Модули призваны решать следующие проблемы:

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

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

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

На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...

А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?

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

😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.

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

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

😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.

😡 Нужны новейшие версии систем сборки, компиляторов и других инструментов, чтобы заработали модули.

😡 Пока популярные библиотеки не начнут распространяться через модули, существующие проекты не будут иметь большое желание переезжать на модули, потому что получится частичное внедрение.

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

Use new features. Stay cool.

#cppcore #compiler #tools
16👍6🔥6
​​pointer to data member
#опытным

В этом посте мы рассказывали о том, что с помощью ranges и и параметра проекции можно кастомизировать алгоритмы с соответствии с определенным полем класса. Например, чтобы найти в коллекции элемент с максимальным определенным полем, то можно сделать так:

struct Payment {
double amount;
std::string category;
};
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);


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

В последней строчке используется &Payment::amount - указатель на поле amount в классе Payment. Но если это параметр функции, то это значение какого-то типа. Но какой тип у этого указателя?

Если про указатели на конкретные мемберы знают не только лишь все, то это совсем дебри плюсов.

Явный тип указателя на поле класса используется так:

struct Payment {
double amount;
std::string category;
};

double Payment::*ptr = &Payment::amount; // Here!

Payment payment{3.14, "Groceries"};
std::cout << payment.*ptr << std::endl;
// OUTPUT:
// 3.14


double Payment::*       ptr         = &Payment::amount;
// тип указателя имя указателя инициализатор


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

Мы обязательно должны указать, на какой тип полей этот указатель может указывать. Таким образом указатель ptr может указывать на любое поле класса Payment, имеющее тип double. То есть:

struct Type {
int a;
int b;
float c;
};

int Type::*p = nullptr;

p = &Type::a; // OK, a is int
p = &Type::b; // OK, b is int

p = &Type::c; // ERROR! c is float


Указателю на интовое поле нельзя присвоить указатель на флотовое. И наоборот, указатель p работает с любыми полями типа int.

Если вы подумали, что очень узкоспециализированная вещь, то вы правы. Чуть больше универсализации здесь могут дать шаблоны:

// Takes pointer to any data member for any type
template<typename T, typename FieldType>
void print_field(const T& obj, FieldType T::*field) {
std::cout << obj.*field << std::endl;
}

Payment payment{3.14, "Groceries"};
Type t(42, 69, 3.14);

print_field(payment, &Payment::amount);
print_field(payment, &Payment::category);

print_field(t, &Type::a);
print_field(t, &Type::b);
print_field(t, &Type::c);
// OUTPUT
// 3.14
// Groceries
// 42
// 69
// 3.14


print_field может печатать значение любого поля любого класса по его указателю. Обратите внимание на шаблонную сигнатуру.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
322🔥14👍10
​​WAT
#опытным

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

Всегда ли nullptr указатель равен нулю?

Казалось бы в названии дан ответ:

int * p = nullptr;
std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
// OUTPUT
// true
// 0


Но в общем случае это неправда! Смотрим на пример:

struct A {
int i;
};

int main() {
int A::* p = 0;

std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
std::cout << std::boolalpha << (std::bit_cast<std::uintptr_t>(p) == 0xffffffffffffffff) << "\n";
}
// OUTPUT:
// true
// ffffffffffffffff
// true


nullptr указатель равен совсем не нулю, как декларировалось в начале main.

WAT? Что за фокусы с пропажей нуля?

Во вчерашнем посте мы рассказывали об особом типе указателя - pointer to data member. Этот указатель, которым и является p из примера, по сути хранит информацию о том, как в объекте найти нужное поле класса.

И в большинстве случаев эта информация представляет собой просто смещение поля относительно начала объекта в байтах.

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

Вместо этого обычно используется число -1, которое в байтовом представлении как раз выглядит как все единички:

std::cout << std::hex << static_cast<long long int>(-1) << "\n";
// OUTPUT:
// ffffffffffffffff


С помощью указателей на поля класса можно кстати наглядно изучать выравнивание и упаковку полей с объект:

struct Type {
double a;
char b;
float c;
long long d;
short e;
unsigned f;
};

std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::a) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::b) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::c) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::d) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::e) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::f) << "\n";

// OUTPUT:
// 0
// 8
// 12
// 16
// 24
// 28


Опять же, интересный уголок плюсов.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
20👍11🔥8❤‍🔥2🤯2
​​Сколько весит объект полиморфного класса?
#новичкам

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

У нас уже был пост про размер объекта пустого класса.

А что если это будет класс с виртуальными методами? Сколько тогда будет весить этот класс?

struct SomeClass {

virtual ~SomeClass() = default;

virtual void Process() {
std::cout << "Process" << std::endl;
}
};


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

Таблица одна на все объекты заданного класса. И им каким-то образом нужно получить доступ к этой таблице.

Учитывайте, что нельзя захардкодить эту информацию по статическому типу объекта(например SomeClass& или SomeClass*), потому что под его личиной может скрываться наследник.

Значит надо ее класть в каждый объект. И самое простое - положить в них указатель на свою таблицу виртуальных функций. Так и делают на самом деле. Этот указатель называют vptr.

Соответственно размер класса зависит от битности системы. Для 64-бит указатель имеет размер 8 байт(64 бита) поэтому и размер класса SomeClass будет 8 байт.

std::cout << sizeof(SomeClass) << std::endl;
// OUTPUT
// 8


Пустые наследники SomeClass кстати тоже будут иметь размер 8 из-за того, что им нужно лишь другое значения указателя.

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

Be lightweight. Stay cool.

#cppcore #interview
119👍14🔥7😁6🆒1