
- Главная
- Каталог
- Интернет технологии
- Грокаем C++
Грокаем C++
Авторский канал о программировании на С++ и базе computer science. Простым и легким слогом рассказываем про сложные концепции С++. Самая активная и вовлеченная аудитория в тематике.
Статистика канала
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, 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
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{}
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
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
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Отзывы канала
всего 7 отзывов
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
Грокаем C++ — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 9.4K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 15.0, количество отзывов – 7, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 8391.6 ₽, а за 30 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий