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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Удобно превращаем enum в число
#опытным

В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.

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

Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:

enum class ColorMask : std::uint32_t
{
red = 0xFF,
green = (red << 8),
blue = (green << 8),
alpha = (blue << 8)
};

// std::cout << ColorMask::red << std::endl; // ERROR
std::cout << static_cast<int>(ColorMask::red) << std::endl;


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

Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.

С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:

enum e1 {};
enum class e2 {};
enum class e3 : unsigned {};
enum class e4 : int {};

constexpr bool e1_t = std::is_same_v<std::underlying_type_t<e1>, int>;
constexpr bool e2_t = std::is_same_v<std::underlying_type_t<e2>, int>;
constexpr bool e3_t = std::is_same_v<std::underlying_type_t<e3>, int>;
constexpr bool e4_t = std::is_same_v<std::underlying_type_t<e4>, int>;

std::cout
<< "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n';

// OUTPUT
// underlying type for 'e1' is non-int
// underlying type for 'e2' is int
// underlying type for 'e3' is non-int
// underlying type for 'e4' is int


Соответственно, для каста нужно сделать такую штуку:

auto num = static_cast<std::underlying_type_t<ColorMask>>(ColorMask::red);


Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:

auto num = std::to_underlying(ColorMask::red);


Красота!

Know your type. Stay cool.

#cpp11 #cpp23
👍21🔥169🥱1
​​magic_enum
#опытным

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

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

Такая задача требует рефлексии, которая в С++ только в 26 стандарте появится. Рефлексия - способность программы знать о том, как она написана на языке программирования. В скомпилированной программе перечислитель это всего лишь число, а чтобы получить имя, которое он имел в С++ коде, в программе должна эта информация где-то сохраниться.

Ну то есть, если у вас нет С++26, то нужно терпеть(но мы привыкли) и писать костыли. Что делается обычно:

enum class Color { RED, GREEN, BLUE };

std::unordered_map<Color, std::string> map = {{Color::RED, "RED"}, {Color::GREEN, "GREEN"}, {Color::BLUE, "BLUE"}};

std::cout << map[Color::RED] << std::endl;


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

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

Это хэдэр-онли С++17 библиотечка, которая предоставляет статическую рефлексию для перечислений и работает без макросни в интерфейсе и без бойлерплейта.


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

Color color = Color::RED;
std::string color_name = magic_enum::enum_name(color);
assert(color_name == "RED);


Как это работает?

magic_enum рассчитан на работу с небольшими enum'ами. По дефолту интервал underlying значений от -128 до 127.

Если сильно упрощенно, то magic_enum пытается инстанцировать специальную функцию для каждого значения из этого интервала. Так как шаблонный параметр будет NTTP, его значение будет вшито в саму сигнатуру функции. А ее можно получить с помощью __PRETTY_FUNCTION__ или __FUNCSIG__:

template <auto V>
constexpr std::string_view get_enum_name() {
return extract_name(PRETTY_FUNCTION);
}


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

Вот примеры сигнатуры функции для существующего и несуществующего перечислителя:

enum class Color { RED, GREEN, BLUE };

constexpr Color c = static_cast<Color>(4);
get_enum_name<c>(); // constexpr void get_enum_name() [with auto V = (Color)4]
get_enum_name<Color::GREEN>(); // constexpr void get_enum_name() [with auto V = Color::GREEN]


Чтобы это все реализовать и вместе собрать нужно еще много шаблонной магии, но суть такова.

magic_enum еще много чего умеет:

👉🏿 Обратный маппинг имен на значения перечисления

std::string color_name{"GREEN"};
auto color = magic_enum::enum_cast<Color>(color_name);
if (color.has_value()) {
// color.value() -> Color::GREEN
}


👉🏿 Доступ к элементам перечисления по индексу
std::size_t i = 0;
Color color = magic_enum::enum_value<Color>(i);


👉🏿 Можно получить количество элементов enum'а:
constexpr std::size_t color_count = magic_enum::enum_count<Color>();


👉🏿 Свои перегруженные io операторы:
using magic_enum::iostream_operators::operator<<; // out-of-the-box ostream operators for enums.
Color color = Color::BLUE;
std::cout << color << std::endl; // "BLUE"


и много чего еще.

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

Use magic. Stay cool.

#tools #template
1🔥29👍1310😱2
Мувать не всегда дешево
#новичкам

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

Но мир не такой уж солнечный и приветливый. Это очень опасное...

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

Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.

И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:

void Process(Data data);


Что выбрать?

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

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

Но это не так! Перемещение - это поверхностное копирование.

Возьмем простой пример:

struct Data {
int a;
double b;
};

Data obj1{3, 3.14};
Data obj2 = std::move(obj1);


Что будет происходить при перемещении obj1? Копирование a и b.

Чуть сложнее:

struct Data {
std::array<int, 5> arr;
};

Data obj1{.arr = {1, 2, 3, 4, 5}};
Data obj2 = std::move(obj1);


Что будет при перемещении obj1, а значит и arr? Тоже копирование! std::array - это массив, фиксированного размера, расположенный на стеке. Как вы собираетесь его перемещать в другой объект? Под другой объект уже выделена своя память на стеке, вы не можете один кусок стека переместить в другой. Вы можете только скопировать значения.

Можно еще занулить конечно, но это редко происходит из соображений перфоманса.

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

struct Data {
std::string * str;
// member functions for making it work properly
};

Data obj1{.str = new std::string("Hello, World!")};
Data obj2 = std::move(obj1);


obj2 теперь имеет такое же значение указателя str, как и obj1, но сама строка оказалась нетронутой.

Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.

Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.

Теперь снова актуализируем вопрос: мувать или копировать?

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

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

Think logically. Stay cool.

#cppcore #cpp11
3👍4116🔥7😎5
​​Передача владения
#новичкам

Захотелось совсем немного развить тему предыдущего поста.

В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.

void bar(std::vector<int>&& vec) {
// do nothing
}
void foo() {
std::vector<int> vec = {1, 2, 3};
bar(std::move(vec));
}


Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.

Другой пример:

std::vector<int> double_elements(std::vector<int> vec) {
for (auto& elem: vec) {
elem *= 2;
}
return vec;
}

void foo() {
std::vector<int> vec = {1, 2, 3};
{
auto doubled = double_elements(vec);
std::println("{}", doubled);
}
vec.push_back(4);
{
auto doubled = double_elements(std::move(vec));
std::println("{}", doubled);
}
}


Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.

Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.

Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.

Поэтому можно принять за правило, что, передав владение, вы больше физически не имеете права пользоваться объектом. Это как продав компанию, вы бы продолжили иметь то же влияние на нее. Нетушки. Либо крестик снимите, либо трусы наденьте. Либо передали владение и забыли, либо скопировали и дальше попользовались.

Give away what you don't need. Stay cool.

#cppcore #cpp11
18👍13🔥6
​​Оборачиваем вспять байты
#новичкам

Когда мы низкоуровнево работаем с сетью, то надо понимать, что в данных, полученных по сети, нужно реверсировать порядок байтов, чтобы правильно интерпретировать значения. Также реверсировать порядок нужно при отправке данных по сети. Это происходит из-за того, что в стеке протоколов TCP/IP принят порядок Big-endian - старший байт хранится по младшему адресу. А на большинстве хостов(десктопов и серверов) - Little-endian: младший байт хранится по младшему адресу.

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

### GCC/Clang

uint16_t swapped16 = __builtin_bswap16(value);
uint32_t swapped32 = __builtin_bswap32(value);
uint64_t swapped64 = __builtin_bswap64(value);

### MSVC:

uint16_t swapped16 = _byteswap_ushort(value);
uint32_t swapped32 = _byteswap_ulong(value);
uint64_t swapped64 = _byteswap_uint64(value);


Либо системное апи:

#include <arpa/inet.h>  // Linux/macOS
// или
#include <winsock2.h> // Windows

uint16_t network_to_host16 = ntohs(value);
uint16_t host_to_network16 = htons(value);

uint32_t network_to_host32 = ntohl(value);
uint32_t host_to_network32 = htonl(value);

uint64_t network_to_host64 = ntohll(value);
uint64_t host_to_network64 = htonll(value);


Либо какое-нибудь библиотечное решение:

#include <boost/endian/conversion.hpp>

uint32_t value = 0x12345678;
uint32_t swapped = boost::endian::endian_reverse(value);

uint32_t to_big = boost::endian::native_to_big(value);
uint32_t to_little = boost::endian::native_to_little(value);


Но в С++23 появилась стандартная функция для разворачивания порядка байтов!

template< class T >
constexpr T byteswap( T n ) noexcept;


Работает она только для интегральных типов и вот ее возможная реализация:

template<std::integral T>
constexpr T byteswap(T value) noexcept
{
static_assert(std::has_unique_object_representations_v<T>,
"T may not have padding bits");
auto value_representation = std::bit_cast<std::array<std::byte, sizeof(T)>>(value);
std::ranges::reverse(value_representation);
return std::bit_cast<T>(value_representation);
}


Результат у нее собственно ровно тот, который и ожидается:

template<std::integral T>
void dump(T v, char term = '\n')
{
std::cout << std::hex << std::uppercase << std::setfill('0')
<< std::setw(sizeof(T) * 2) << v << " : ";
for (std::size_t i{}; i != sizeof(T); ++i, v >>= 8)
std::cout << std::setw(2) << static_cast<unsigned>(T(0xFF) & v) << ' ';
std::cout << std::dec << term;
}

int main()
{
static_assert(std::byteswap('a') == 'a');

std::cout << "byteswap for U16:\n";
constexpr auto x = std::uint16_t(0xCAFE);
dump(x);
dump(std::byteswap(x));

std::cout << "\nbyteswap for U32:\n";
constexpr auto y = std::uint32_t(0xDEADBEEFu);
dump(y);
dump(std::byteswap(y));

std::cout << "\nbyteswap for U64:\n";
constexpr auto z = std::uint64_t{0x0123456789ABCDEFull};
dump(z);
dump(std::byteswap(z));
}

// OUTPUT
// byteswap for U16:
// CAFE : FE CA
// FECA : CA FE

// byteswap for U32:
// DEADBEEF : EF BE AD DE
// EFBEADDE : DE AD BE EF

// byteswap for U64:
// 0123456789ABCDEF : EF CD AB 89 67 45 23 01
// EFCDAB8967452301 : 01 23 45 67 89 AB CD EF


Как всегда стандарт запаздывает лет на 10-15-20, но хорошо, что все-таки завезли эту полезную функцию, которую можно кроссплатформенно использовать.

Use standard solutions. Stay cool.

#cpp23
28👍13😁8🔥5
И это все компилируется!
Сможете сказать, откуда каждая скобка взялась?)
1🤯4310😁10👀5🔥3🎄3🥱1
​​Атрибуты лямбды
#опытным

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

В С++11 у нас появилась возможность указывать атрибуты для функции. Например:

[[nodiscard]] int ComplicatedCompute() {
return 2*2;
}

ComplicatedCompute();
// warning: ignoring return value of 'int ComplicatedCompute()',
// declared with attribute nodiscard


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

Ну это функции. А как же лямбды? Хочется и для них указывать атрибуты.

И атрибуты для возвращаемого значения лямбды завезли в С++23. Выглядит это так:

auto complicated_compute = [] [[nodiscard]] () { return 2 * 2; };

complicated_compute();
// warning: ignoring return value of 'main()::<lambda()>',
// declared with attribute 'nodiscard'


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

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

Тут как бы все просто: не хотите - не используйте. У лямбды и так полно опциональных обвесок, одним больше, одним меньше. Можно определить шаблонную лямбду и обвесить ее всякими концептами с trailing return type. И это будет страшный зверь. Можно сделать отдельный пост, как может выглядеть ультимативная лямбда.

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

Don't ignore. Stay cool.

#cpp23
22👍10🔥8😁3
Атрибуты везде
#опытным

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

Есть на самом деле 3 легальных места для навешивания атрибутов на функцию.

1️⃣ Перед типом возвращаемого значения:

[[deprecated]] int foo() { return 42; }


Тогда он работает при непосредственном использовании функции.

foo();
// warning: 'int foo()' is deprecated


2️⃣ После имени функции:

int foo [[deprecated]] () { return 42; }


В таком виде атрибут тоже применяется к самой функции.

3️⃣ После параметров:

int foo() [[gnu::deprecated]] { return 42; }


Тогда атрибут применяется к типу функции, а не к самой функции. Разница вот в чем:

int foo() [[gnu::deprecated]] { return 42; }

int main() {
foo(); // no warnings
using FuncType = decltype(foo); // use of type is deprecated
}


Обычный вызов функции прекрасно компилируется. Но вот использование типа функции через decltype помечается как устаревшее.

Причем gcc и clang по-разному интерпретируют эту ситуацию. Clang говорит, что gnu::deprecated нельзя применять к типам и игнорирует атрибут. Вот ссылка на годболт для интересующихся.

Соответственно, в лямбде в тех же местах можно ставить атрибуты:

auto complicated_compute = [] [[nodiscard]] () [[gnu::deprecated]] {
return 2 * 2;
};


Признавайтесь, знали?)

Have your own opinion. Stay cool.

#cppcore
👍25🤯2010🔥6
​​Атрибуты параметров функции
#новичкам

Атрибуты можно также применять к параметрам функции. Это помогает чуть полнее в коде функции аннотировать некоторые свойства параметров.

class Interface {
public:
virtual void method(int param) = 0;
};

class Implementation : public Interface {
public:
void method(int param) override {
// this implementation doesn't use param so mark it
}
};


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

Если вы активно используете варнинги компилятора и прочие линтеры, при попытке собрать такой код вы скорее всего увидите предупреждение/ошибку компиляции. Чтобы стало все чётенько, стоит пометить param атрибутом maybe_unused, тем самым явно указав компилятору, что параметр не используется намеренно. И проблема исчезнет.

Однако из стандартных атрибутов по сути имеет смысл использовать только этот самый maybe_unused.

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

Например:

size_t safe_strcpy(
[[gnu::nonnull]] char* dest,
[[gnu::nonnull]] const char* src,
size_t dest_size
);


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

Annotate your code. Stay cool.

#cppcore
21🔥16👍9🤯1
​​Множество атрибутов
#опытным

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

1️⃣ Списочный. Внутри одних скобок перечисляете все атрибуты:

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]] int f();


2️⃣ Многоскобочный. Для больших любителей распиленных квадратов. Очень больших:

[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]] int f();


Больше квадратных скобок!

Также если вы используете несколько атрибутов из какого-то одного неймспейса, то можете использовать директиву using:

[[using gnu : always_inline, const, hot]] [[nodiscard]] int f();


Но тогда котлеты отдельно, мухи отдельно. Все атрибуты одного неймспейса нужно уносить в отдельные скобки. Это фича С++17.

Что интересно, вы можете написать полную чупуху:

[[rust, will, replace, cpp]] int f();


И это скомпилируется! Стандарт поддерживает любые implementation-defined атрибуты. Причем неизвестные атрибуты просто игнорируются. Правда игнор спровождается варнингами, которые тем не менее можно скрыть опциями, подобным -Wno-attributes.

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

Love squares. Stay cool.

#cppcore #cpp17
🔥2213👍10👎1
Парсим ужас
#новичкам

Вот мы и рассмотрели все необходимые компоненты, чтобы понять, что написано здесь:

[[]][[]]int main()[[]]{{[][][[]][[]]{{{}}}(main);}}


Если включить clang-format, то код преобразится во что-то такое:

/*1*/[[]][[]] int main()/*2*/[[]] {
{
[]/*3*/[[]](/*4*/[[]] /*5*/auto [])/*6*/[[]][[]] {
{
{}
}
}/*7*/(main);
}
}


Давайте посмотрим, откуда так много скобок:

1️⃣ Перед типом возвращаемого значения main определены 2 пустые области для указания атрибутов функции main.

2️⃣ Перед телом функции main определена пустая область для атрибутов, применяемых к типу функции main.

3️⃣ После блока захвата лямбды определена пустая область для атрибутов, применяемых к самой лямбде.

4️⃣ Внутри списка параметров лямбды определена пустая область для атрибутов, применяемых к единственному параметру лямбды.

5️⃣ Сама лямбда является generic и принимает массив неизвестного типа.

6️⃣ Перед телом лямбды определены 2 пустые области для атрибутов, применяемых к типу лямбды.

7️⃣ Вызываем лямбду с помощью указателя на функцию main.

8️⃣ Ну и разбавили это дело несколькими лишними скоупами по пути.

Не так уж и сложно оказалось)

Так, новичковая часть закончилась.
#опытным

Интересно, что этот код компилируется на gcc, но не на clang.

cppinsights показывает, что лямбда раскрывается во что-то такое:

class __lambda_5_17 {
public:
template <class type_parameter_0_0>
inline /*constexpr */ auto operator()(auto *) const {
{ {}; };
}

private:
template <class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(auto *__param0) {
return __lambda_5_17{}.operator()<type_parameter_0_0>(__param0);
}

public:
// /*constexpr */ __lambda_5_17() = default;
};


То есть по факту мы имеем шаблонный оператор с auto параметром.

Как интерпретировать эту штуку - дело нетривиальное и по ходу компиляторы это делают по-разному. Видимо gcc при попытке инстанцировать шаблон с параметром int() выводит auto как тот же самый тип функции int() и в итоге лямбда принимает указатель на функцию. А clang при попытке инстанцировать шаблон выводит тип параметра функции как массив функций int() и не может принять main в качестве такого параметра.

Пишите ваше мнение, кто прав, кто виноват)

Deal with horrible things step by step. Stay cool.

#cppcore #compiler
❤‍🔥24😁1311👍3🔥3
​​Мок собеседования
#новичкам #опытным

Представьте, что вы вкатун в АйТишку плюсовую. Прочитали несколько книжек, прошли кучу бесплатных курсов и дописали свой первый велосипед пет-проект.

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

Но как узнать, как проходить собесы, если никогда их не проходил?(оставим за скобками вопрос, как вообще добраться до собеса, это то еще шаманство)

Для этого существуют мок-собеседования. То есть дословно "имитация" собеседования.

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

Но если вы стеснительный волк-одиночка, к тому же еще и бедный(за занятия с ментором нужно платить), то и для вас есть вариант.

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

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

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

- Ambushed raccoon
- Владимир Балун

Ну а мы за вас собрали подборку всех(или почти всех) мок-собесов на русском языке по С++ и разбили их по уровням.

Junior:

- https://www.youtube.com/watch?v=_-EkLLZ5svk
- https://www.youtube.com/watch?v=H1mIHJxnm9E
- https://www.youtube.com/watch?v=PQ1C_0EAHFI
- https://www.youtube.com/watch?v=BCpHj698D8U
- https://youtu.be/7g8HufwNa0g?si=XVKRsuoHN20MJx3i
- https://youtu.be/a18qTcWn-II?si=OttfqKh0bHLjueOY
- https://www.youtube.com/live/rLOgkn6xVQA?si=lFqwGf_Wsr8IohHo
- https://youtu.be/VfoxaNLVtmQ?si=elt-OyZWB5tXp5hI


Middle:

- https://www.youtube.com/watch?v=nMdNehH8-Ss
- https://www.youtube.com/watch?v=Ed37R0FvkQ8
- https://www.youtube.com/watch?v=IDqMy4_xkb4
- https://www.youtube.com/watch?v=BOUEbS5L4-8
- https://www.youtube.com/watch?v=PwVMcxCBIkg
- https://www.youtube.com/watch?v=Np6UrKN6ZbA
- https://www.youtube.com/watch?v=1Ez3kbK_3bI
- https://www.youtube.com/watch?v=s6BXbEPaw5g
- https://www.youtube.com/watch?v=yfoFtu28n4o
- https://www.youtube.com/watch?v=cT3fonCyxJk
- https://www.youtube.com/watch?v=xwb2FAKxCUo
- https://www.youtube.com/watch?v=bOgz4K-ARzQ
- https://www.youtube.com/watch?v=wR4VRCp_BYo
- https://youtu.be/5enBKMwOST0?si=i-5mdyrxeeFiZKo1


Senior:

- https://www.youtube.com/watch?v=OwMEK_W8Ysw
- https://www.youtube.com/watch?v=dZpe58HKX-8

Просто вопросы и задачи с собесов:

- https://www.youtube.com/watch?v=boYk6gFg84E
- https://www.youtube.com/watch?v=ViHNB0_1j90
- https://www.youtube.com/watch?v=aYM7lksQ8yg
- https://www.youtube.com/watch?v=UdY_YMFx7SY
- https://www.youtube.com/watch?v=PStQ4jhhz08
- https://www.youtube.com/watch?v=wMYfg_iPqMQ


Просмотрев эти видосы(возможно по нескольку раз) и переписав ответы на все вопросы себе в тетрадочку или файлик, вы будете знать ответы на 95% устных вопросов, которые вам будут задавать в условной компании Х.

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

Ну а для любителем native english есть канал Кодингового Иисуса. Он постоянно у себя на стримах спрашивает у людей за плюсы. В основном люди валятся на простых вопросах, но иногда попадаются качественные собеседники. По крайней мере практика языка вам будет точно обеспечена.

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

Practice makes perfect. Stay cool.

#interview
55🔥20👍13❤‍🔥4🙏4
Как запустить cpp файл из консоли?

Да, да, именно запустить файл. Берете bash, берете файл. Как одно воткнуть в другое и получить результат работы программы?

Следите за пальцами:

#if 0
g++ test.cpp -o test && ./test
exit 0
#endif

#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
}


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

1️⃣ Ядро смотрит на первые 2 байта файла и пытается найти там шебанг - #!. Если нашла, то в этой строчке будет указан путь до нужного интерпретатора.

2️⃣ Если шебанга нет и файл не является исполняемым(как у нас), то файл считается shell-скриптом и исполняется с помощью текущего командного интерпретатора.

3️⃣ Для shell-скриптов символ # обозначает начало однострочного комментария, поэтому первая строчка игнорируется.

4️⃣ Интерпретатор встречает команду компиляции и выполняет ее.

5️⃣ Теперь мы пытаемся реально скомпилировать этот файл с помощью g++. На этапе препроцессинга ветка условия #if 0 выбросится из текста файла и компилироваться будет только реально С++ код.

6️⃣ После компиляции интерпретатор запускает исполняемый файл и завершает работу на инструкции exit 0. Остальной С++ код он уже не увидит.

Напишите в терминале

chmod +x test.cpp
./test.cpp


И увидите заветные слова.

Дожили! Превратили С++ в питон...

PS: Большое спасибо за идею и материалы Даниилу @dkay7.

Have a fun. Stay cool.

#fun
1058🔥21👍18😁14🤯12👀3🗿3🐳1
​​Bootstrap
#новичкам

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

Также возможно вы слышали, что компилятор java написан на java.

Если вдуматься в эти факты, то невольно задаешься вопросом: а как это вообще возможно? Нельзя же себя поднять за волосы из болота.

Вообще-то барон Мюнхгаузен смог и мы сможем!

Нужно лишь немного опоры. Чуть-чуть оттолкнуться.

Давайте пройдем весь процесс от и до.

1️⃣ Вы решили написать свой язык самый лучший язык во всей вселенной GOAT. Разработали синтаксис и правила языка, пару раз пооргазмировали от его крутости.

2️⃣ Дальше вам нужно уметь конвертировать код на новом языке в программу. Выхода нет: вы быстренько пишите простой компилятор или интерпретатор GOAT на уже существующем языке. Пусть для определенности вы написали компилятор на С. Компилируете его каким-нибудь gcc и у вас появляется первый компилятор языка GOAT. Это так называемый компилятор начальной загрузки или bootstrap компилятор.
Отлично! Вы можете писать программы на новом языке!

3️⃣ Но компилятор - это же тоже программа. Теперь вы можете на GOAT написать компилятор GOAT и скомпилировать его bootstrap компилятором. В итоге у вас получится рабочий компилятор GOAT, написанный на языке GOAT! Вот яйцо и родило яйцо.

4️⃣ В дальнейшем вы можете развивать компилятор, используя сам язык GOAT и перекомпилируя его самим собой.

Весь этот процесс называется bootstrapping.

Первый компилятор и jvm были написаны на С. Первый компилятор С был написан на ассемблере. Pascal'я - на Fortran.

Интересно, что clang изначально был написан на урезанной версии С++, которая компилировалась и на gcc, и на msvc. Поэтому clang можно использовать на винде.

Преимущества бутстраппинга:

👉🏿 Демонстрирует зрелость и самодостаточность языка. Можно развиваться самостоятельно и не зависеть от других технологий.

👉🏿 Разработчики компилятора могут использовать все возможности языка, для которого они пишут компилятор.

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

👉🏿 Упрощается разработка компилятора. Не нужно знать несколько языков.

А вот как бутстрапился С++ разберем в следующем посте.

Be self-sufficient. Stay cool.

#tools #compiler
148👍22🔥10🥱1
​​История компилятора С++
#новичкам

С++ начал свой путь в 1979 году, когда Бьерн Страуструп работал над своей диссертацией. Он назвал его "С с классами". Это была довольно примитивная версия С++, были просто добавлены классы, их методы, возможность наследования и другие более минорные фичи. Никакого полиморфизма и шаблонов. Для компиляции этой версии было достаточно написать препроцессор для С компилятора, который бы транслировал инструкции С++ в С-шный код. Этот препроцессор + С-компилятор назывался CPre.

Но Бьерн не забросил свою разработку и в 1982 году разработал улучшенную версию С с классами и назвал ее С++. Она уже включала виртуальные функции, перегрузку функций и операторов, ссылки, стандартные функции работы с памятью(new/delete).

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

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

По сути нужен был полноценный компилятор, преобразующий С++ код в С. И им стал Cfront.

Вот теперь пошли интересности.

Первая простейшая версия языка С++ и компилятора Cfront была написана на "С с классами" и скомпилирована CPre. В дальнейшем мощные фичи в язык добавлялись уже с помощью Сfront, который писался на предыдущей версии С++. CPre не вывозил возросшей сложности.

Ну хорошо, Бьерн у себя на домашнем компьютере с определенной архитектурой имеет свежайшую версию Cfront, которая написана на С++. С++ уже не может быть скомпилирован CPre. Как получить компилятор на другой архитектуре?

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

Таким образом распространялись версии компилятора под разные архитектуры.

Cfront успешно развивался еще несколько лет, но у него были объективные недостатки:

👉🏿 скорость компиляции в С/С++ и так не славится скорость, так тут еще и двухступенчатая компиляция была

👉🏿 при попытке компиляции ошибки показывались в сгенерированном C-коде, а не в исходном C++, что затрудняло отладку

👉🏿 некоторые возможности C++ было сложно или невозможно реализовать через трансляцию в C. С не всесилен и довольно лаконичен в используемых инструментах.

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

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

The end.

Know that everything has its limits. Stay cool.

#tools #compiler
43👍17🔥6🤷‍♂2🐳2👏1
​​Наследие Cfront. this
#новичкам

Cfront был первым компилятором С++. И это оказало большое влияние на то, какие подходы к компиляции C++ используют другие компиляторы. В следующих нескольких постах мы обсудим наследие, которое после себя оставил Cfront.

Главное, о чем надо помнить в этой серии - Cfront компилировал С++ код в С. То есть все концепции языка С++, которыми Cfront оперировал, можно было представить в С коде. Некоторые такие представления перетекли в стандарт, некоторые остались на уровне реализации.

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

Мы уже написали несколько постов о том, как можно вызывать методы классов с помощью указателя на функцию и объекта класса(или указателя на него). Тык, тык и тык. Но здесь повторим более наглядно чтоли.

Вот у Cfront на входе есть С++ код:

class Point {
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
int get_x() const { return x; }
int get_y() const { return y; }
void move(int dx, int dy) { x += dx; y += dy; }
};


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

struct Point {
int x;
int y;
};

void Point_ctor(struct Point* this, int x, int y) {
this->x = x;
this->y = y;
}

int Point_get_x(const struct Point* this) {
return this->x;
}

int Point_get_y(const struct Point* this) {
return this->y;
}

void Point_move(struct Point* this, int dx, int dy) {
this->x += dx;
this->y += dy;
}

void Point_dtor(struct Point* this) {}


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

Если есть класс, значит должно быть использование:

void foo() {
Point p = Point(1, 2);
p.move(2, 3);
printf("%d/n", p.get_x());
}


Этот код превращался в нечто подобное:

void foo() {
struct Point p;
Point_ctor(&p, 1, 2);
Point_move(&p, 2, 3);
printf("%d/n", Point_get_x(&p));
Point_dtor(&p);
}


Заметьте, что в конце любого скоупа, в котором был создан объект компилятором вставлялся вызов деструктора, тем самым обеспечивая идиому RAII.

Have a legacy. Stay cool.

#cppcore #goodoldc #compiler
437👍12🔥12
​​Наследие Cfront. Наследование
#новичкам

Сорри за тавталогию, но мимо этой темы мы не можем пройти(хотя я бы прошел, если бы не @shumilkinad, спасибо ему)

В раннем С++ уже были классы и наследование, но в С этого не было. Как же можно транслировать наследование классов в С'шный код?

Простая агрегация и никакого мошенничества.

Все классы в С++ - это простые С-структуры с отдельным набором функций, принимающих cv-специфицированный указатель на инстанс структуры(это мы выяснили в прошлом посте). Наследники же в начале объекта хранят все поля базового класса. Так давайте просто первым полем наследника сделаем инстанс базовой структуры. Таким образом мы обеспечим вложенность объектов с любым количеством наследований.

Вот есть пара родитель-наследник:

class Base {
public:
int x;
Base(int a) : x(a) {}
};

class Derived : public Base {
public:
int y;
int z;
Derived(int a, int b) : Base(a), y(b) {
z = x + y;
}
};


Примерно в такой код они транслировались:

struct Base {
int x;
};

struct Derived {
struct Base _base; // embedded base object
int y;
int z;
};

void Base_constructor(struct Base* this, int a) {
this->x = a;

// Constructor body (if presented)
}

void Derived_constructor(struct Derived* this, int a, int b) {
// Init base object
Base_constructor(&this->_base, a);

// Init fields of derived object
this->y = b;

// Constructor body
this->z = this->_base.x + this->y;
}


Первым полем структуры Derived - структура Base.

Конструкторы же - это отдельные функции. Но в С++ очень важен порядок вызовов конструкторов и деструкторов.

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

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

class Base {
public:
~Base() {
// Destructor Base body
}
};

class Derived : public Base {
public:
~Derived() {
// Destructor Derived body
}
}


Превращается в:

struct Base {
};

struct Derived {
struct Base _base;

};

void Base_destructor(struct Base* this) {
// Destructor Base body
}

void Derived_destructor(struct Derived* this) {
// Destructors execute in reverse order

// Destructor Derived body

Base_destructor(&this->_base);
}


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

Have a legacy. Stay cool.

#cppcore #goodoldc #compiler
5👍3313🔥12
Наследие Cfront. Полиморфизм
#новичкам

Часто мы говорим о каких-то вещах в С++ и можем сказать, как они работают под капотом. Но стандарт языка не говорит о том, как компиляторы должны реализовывать те или иные фичи. Он только задает требование. Тем не менее подходы к реализации некоторых фичей повторяются в разных компиляторах. И отчасти это так из-за влияния Cfront.

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

class Person {
protected:
std::string name;
int age;

public:
Person(const std::string &n, int a) : name(n), age(a) {}

virtual void describe() const {
std::cout << name << " (" << age << " years)";
}

virtual ~Person() = default;
};

class Employee : public Person {
std::string position;

public:
Employee(const std::string &n, int a, const std::string &pos)
: Person(n, a), position(pos) {}

void describe() const override {
std::cout << name << " (" << age << " years) - " << position;
}
~Employee() override = default;
};


Мы более подробно разбирали полиморфизм в одном из предыдущих постов. Сейчас мы поговорим, как примерно Cfront преобразовывал этот С++ код в С код.

Основные идеи:

1️⃣ Для каждого полиморфного класса формируется статическая таблица виртуальных функций. Это по сути массив указателей на виртуальные "методы" класса, которые Cfront представлял в виде обычных функций. Порядок методов в таблице определялся порядком их объявления в классе.

2️⃣ Нужно каким-то образом связать таблицу для класса с объектом этого класса. Для этого использовалось неявное дополнительное поле класса - указатель на таблицу виртуальных функций или vptr.

Вот как Cfront преобразовывал код выше(примерно, детали могут отличаться):

struct Person {
// pointer to vtable
void (**vptr)();
struct string name;
int age;
};

struct Employee {
struct Person base;
struct string position;
};


В самом первом базовом классе появлялся указатель vptr, который в конструкторе инициализируется правильным адресом нужной таблицы. void (**vptr)() - это тип указателя на указатель на функцию, возвращающую void и не принимающую аргументов.

Но погодите, виртуальные методы как минимум принимают один неявный аргумент this, а как максимум могут самые разнообразные сигнатуры иметь. Почему указатель имеет такой тип?

Ну а как вы еще засунете в один массив разные функции? Их кастили к одному общему типу void(*)(), только так это было возможно:

void (*Person_vtable[])() = {
(void(*)())Person_describe_impl,
(void(*)())Person_dtor
};

void (*Employee_vtable[])() = {
(void(*)())Employee_describe_impl,
(void(*)())Employee_dtor
};

void Person_describe_impl(const struct Person* this) {
printf("%s (%d years)", this->name, this->age);
}

void Person_dtor(struct Person* this) {
String_dtor(this->name);
}

void Employee_describe_impl(const struct Person* this) {
// cast to proper type
const struct Employee* emp = (const struct Employee*)this;
printf("%s (%d years) - %s", emp->base.name, emp->base.age, emp->position);
}

void Person_dtor(struct Person* this) {
String_dtor(this->position);
Person_dtor(this->base);
}


Cfront генерировал Сишные реализации методов и клал их в массив, попутно приводя к типу void(*)().

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

void print_info(const Person* person) {
person->describe();
}


void print_info(const struct Person* person) {
void (*describe_func)(const struct Person*) =
(void (*)(const struct Person*))person->vptr[0];
describe_func(person);
}


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

В наше время не нужна трансляция С++ в С, но тем не менее концепция таблиц виртуальных функций и указателей на них осталась.

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
👍23🔥159
Современные таблицы виртуальных функций
#опытным

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

Как устроены vtables сейчас, когда С++ напрямую компилируется в ассемблер?

Идея все та же:

👉🏿 На каждый полиморфный класс создается своя vtable

👉🏿 Она представляет собой просто массив адресов. В ассемблере адреса нетипизированные, поэтому они просто лежат там чиселками.

👉🏿 В каждом объекте хранится vptr - указатель на эту таблицу.

👉🏿 vtpr инициализируется в самом базовом классе его адресом его vtable и затем каждый конструктор наследника присваивает в него адрес своей таблицы

Реализации вносят свои особенности, но это ядро остается неизменным.

Вот примерчик(можете смотреть на годболте):

class Person {
protected:
std::string name;
int age;
public:
Person(const std::string &n, int a) : name(n), age(a) {}
virtual std::string getRole() const { return "Person"; }
virtual std::string getName() const { return name; }
virtual void describe() const {
std::cout << name << " (" << age << " years)";
}
virtual ~Person() = default;
};

class Employee : public Person {
std::string position;
public:
Employee(const std::string &n, int a, const std::string &pos)
: Person(n, a), position(pos) {}
std::string getRole() const override { return "Employee"; }
void describe() const override {
std::cout << name << " (" << age << " years) - " << position;
}
};

int main() {
Person * p = new Employee("Steven", 42, "CEO");
p->describe();
std::cout << p->getName() << std::endl;
}


Будем сейчас разбирать gcc-шный асм.

Вот так выглядят таблицы для обоих классов:

vtable for Person:
.quad 0
.quad typeinfo for Person
.quad Person::getRoleabi:cxx11 const
.quad Person::getNameabi:cxx11 const
.quad Person::describe() const
.quad Person::~Person()

vtable for Employee:
.quad 0
.quad typeinfo for Employee
.quad Employee::getRoleabi:cxx11 const
.quad Person::getNameabi:cxx11 const
.quad Employee::describe() const
.quad Employee::~Employee()


quad - это 64-битное число. Видно, что таблицы - это статические массивы.

Первым числом у них является offset to top, это число нужно для корректной работы множественного наследования, не будем вдаваться в детали.

Второй число - адрес расположения информации о динамическом типе(RTTI) объекта. Эта информация нужна, например, для dynamic_cast'а.

Дальше расположены адреса виртуальных функций нужных классов. Заметьте, что адреса расположены в порядке объявления виртуального метода в классе. Если в дочернем классе переопределяется метод, то в его vtable указатель родительского метода заменяется на переопределенный(методы getRole и describe). Если наследник не переопределяет метод, то в таблице остается указатель на родительский метод(getName).

В конструкторе Employee происходит такое:

; Сначала вызывается конструктор Person (устанавливает vptr Person)
call Person::Person(...) [base object constructor]

; Затем перезаписывается vptr на таблицу Employee
mov edx, OFFSET FLAT:vtable for Employee+16
mov rax, QWORD PTR [rbp-24]
mov QWORD PTR [rax], rdx


Вызывается конструктор базового класса и сразу же после этого переприсваивается vptr на таблицу класса Employee.

Ну а виртуальный вызов выглядит просто как call нужного адреса:

mov     rax, QWORD PTR [rbp-40]    ; Загружаем указатель p
mov rax, QWORD PTR [rax] ; Загружаем vptr (указывает на vtable+16)
add rax, 16 ; Смещаемся к 4-му слоту (describe)
mov rdx, QWORD PTR [rax] ; Загружаем адрес функции describe
mov rax, QWORD PTR [rbp-40] ; Загружаем this (указатель p)
mov rdi, rax ; Передаем this как первый параметр
call rdx ; Виртуальный вызов describe
❤‍🔥2211👍6🔥6😁1
​​Компилятору достаточно лишь правильно положить аргументы функции в правильные регистры, согласно calling conventions. Какие аргументы нужно подготовить компилятор знает заранее, так как сигнатура виртуальных методов одинакова для всех переопределенных вариантов. Остается лишь call'ьнуть нужный указатель и происходит виртуальный вызов.

Подкапотоное устройство полиморфизма часто спрашивают на собесах. Теперь вы знаете, как отвечать почти на полный спектр вопросов по этой теме.

Know whats under the hood. Stay cool.

#compiler #cppcore
24👍14🔥9
​​Наследие Cfront. Манглирование
#новичкам

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

Вместо

void print(int x);
void print(float x);


Там пишут:

void print_int(int x);
void print_float(float x);


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

А в С++ была перегрузка и надо было каким-то образом плюсовые перегруженные функции превращать в неперегруженные сишные при трансляции кода. Для этого было придумано декорирование имен или name mangling.

Самый простой способ - добавлять к конце функции ее параметры в закодированном виде: ИмяФункции_ТипыПараметров:

void draw(int x, int y);
void draw(double x, double y);

// Cfront mangling:
draw_i_i // draw(int, int)
draw_d_d // draw(double, double)


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

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

👉🏿 2 разных класса могут иметь методы с одинаковым названием. Так как при трансляции в С это были просто свободные функции, манглинг должен учитывать и имя класса:

void Circle::paint(Color c);
void Square::paint();
Rectangle::~Rectangle();
Shape::~Shape();

// transform into

void Circle_paint_Color(struct Circle* this, struct Color c);
void Square_paint(struct Square* this);
void Rectangle_dtor(struct Rectangle* this);
void Shape_dtor(struct Shape* this);


👉🏿 Есть же еще и пространства имен. Они помогают разграничить скоуп существования имен. И названия пространства имен тоже манглировались в имена функций:

namespace Graphics {
class Canvas {
void clear();
};
}

// transform into

void Graphics_Canvas_clear(struct Canvas* this)


👉🏿 В С++ когда-то появились шаблоны. Шаблон всегда инстанцируется с каким-то типом. И чтобы различать эти инстанциации, Cfront манглировал типы шаблонных параметров в полное имя типа:

template<typename T>
class Stack {
void push(T value);
};

Stack<int> stack;

// transforms into

Stack_int_push_i(int value);


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

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


void Circle::rotate(int);

// transforms into

_ZN6Circle6rotateEi

// Разбор:
_Z - префикс C++
N - вложенное имя
6Circle - длина=6, "Circle"
6rotate - длина=6, "rotate"
E - конец аргументов
i - тип int


Шаблоны, неймспейсы, noexcept и const квалификаторы - все вшивается в имя символа.

Вы также можете вручную управлять манглингом: включать и выключать его:

extern "C" void c_function();   // Без манглинга: _c_function
extern "C++" void cpp_function(); // С манглингом: _Z10cpp_functionv


Это нужно для совместимости ABI интерфейсов, предоставляемых библиотеками.

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

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
22👍21🔥13