Как запустить cpp файл из консоли?
Да, да, именно запустить файл. Берете bash, берете файл. Как одно воткнуть в другое и получить результат работы программы?
Следите за пальцами:
Есть ли мы попытаемся запустить файл с этим содержимым через терминал, то будет выполняться следующая последовательность шагов:
1️⃣ Ядро смотрит на первые 2 байта файла и пытается найти там шебанг -
2️⃣ Если шебанга нет и файл не является исполняемым(как у нас), то файл считается shell-скриптом и исполняется с помощью текущего командного интерпретатора.
3️⃣ Для shell-скриптов символ # обозначает начало однострочного комментария, поэтому первая строчка игнорируется.
4️⃣ Интерпретатор встречает команду компиляции и выполняет ее.
5️⃣ Теперь мы пытаемся реально скомпилировать этот файл с помощью g++. На этапе препроцессинга ветка условия
6️⃣ После компиляции интерпретатор запускает исполняемый файл и завершает работу на инструкции
Напишите в терминале
И увидите заветные слова.
Дожили! Превратили С++ в питон...
PS: Большое спасибо за идею и материалы Даниилу @dkay7.
Have a fun. Stay cool.
#fun
Да, да, именно запустить файл. Берете 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
10❤58🔥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
#новичкам
Если вы когда-нибудь заходили в исходники своего компилятора, то могли заметить, что его исходный код написан на С++.
Также возможно вы слышали, что компилятор 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
1❤48👍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
#новичкам
С++ начал свой путь в 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 на входе есть С++ код:
У каждого класса есть конструктор, деструктор и набор методов. Все это компилировалось в обычные функции, которые первым аргументом принимали неявный указатель this на экземпляр сишной структуры:
Константность метода регулировалась константностью указателя this.
Если есть класс, значит должно быть использование:
Этот код превращался в нечто подобное:
Заметьте, что в конце любого скоупа, в котором был создан объект компилятором вставлялся вызов деструктора, тем самым обеспечивая идиому RAII.
Have a legacy. Stay cool.
#cppcore #goodoldc #compiler
#новичкам
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
4❤37👍12🔥12
Наследие Cfront. Наследование
#новичкам
Сорри за тавталогию, но мимо этой темы мы не можем пройти(хотя я бы прошел, если бы не @shumilkinad, спасибо ему)
В раннем С++ уже были классы и наследование, но в С этого не было. Как же можно транслировать наследование классов в С'шный код?
Простая агрегация и никакого мошенничества.
Все классы в С++ - это простые С-структуры с отдельным набором функций, принимающих cv-специфицированный указатель на инстанс структуры(это мы выяснили в прошлом посте). Наследники же в начале объекта хранят все поля базового класса. Так давайте просто первым полем наследника сделаем инстанс базовой структуры. Таким образом мы обеспечим вложенность объектов с любым количеством наследований.
Вот есть пара родитель-наследник:
Примерно в такой код они транслировались:
Первым полем структуры Derived - структура Base.
Конструкторы же - это отдельные функции. Но в С++ очень важен порядок вызовов конструкторов и деструкторов.
Поэтому конструктор самого базового класса только инициализирует свои поля и дальше выполняет инструкции из тела конструктора. При создании же наследников в начале вызывается конструктор базового класса и лишь потом инициализация полей и выполнение тела.
Разрушение же объекта выполняется в обратном порядке. Трусы снимаются только после штанов, раньше не получится.
Превращается в:
Новичкам особенно будет полезно понимать, как простой код С++ может быть написан на С, чтобы досканально понимать все абстракции языка и какие действия скрыты от наших глаз.
Have a legacy. Stay cool.
#cppcore #goodoldc #compiler
#новичкам
Сорри за тавталогию, но мимо этой темы мы не можем пройти(хотя я бы прошел, если бы не @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👍33❤13🔥12
Наследие Cfront. Полиморфизм
#новичкам
Часто мы говорим о каких-то вещах в С++ и можем сказать, как они работают под капотом. Но стандарт языка не говорит о том, как компиляторы должны реализовывать те или иные фичи. Он только задает требование. Тем не менее подходы к реализации некоторых фичей повторяются в разных компиляторах. И отчасти это так из-за влияния Cfront.
В С++ классический динамический полиморфизм подтипов синтаксически реализуется через виртуальные функции.
Мы более подробно разбирали полиморфизм в одном из предыдущих постов. Сейчас мы поговорим, как примерно Cfront преобразовывал этот С++ код в С код.
Основные идеи:
1️⃣ Для каждого полиморфного класса формируется статическая таблица виртуальных функций. Это по сути массив указателей на виртуальные "методы" класса, которые Cfront представлял в виде обычных функций. Порядок методов в таблице определялся порядком их объявления в классе.
2️⃣ Нужно каким-то образом связать таблицу для класса с объектом этого класса. Для этого использовалось неявное дополнительное поле класса - указатель на таблицу виртуальных функций или vptr.
Вот как Cfront преобразовывал код выше(примерно, детали могут отличаться):
В самом первом базовом классе появлялся указатель vptr, который в конструкторе инициализируется правильным адресом нужной таблицы. void (**vptr)() - это тип указателя на указатель на функцию, возвращающую void и не принимающую аргументов.
Но погодите, виртуальные методы как минимум принимают один неявный аргумент this, а как максимум могут самые разнообразные сигнатуры иметь. Почему указатель имеет такой тип?
Ну а как вы еще засунете в один массив разные функции? Их кастили к одному общему типу void(*)(), только так это было возможно:
Cfront генерировал Сишные реализации методов и клал их в массив, попутно приводя к типу void(*)().
Ну а в месте вызова метода компилятору ничего не мешает сделать каст к нужному типу, так как он знает сигнатуру вызываемого метода:
В те времена С не был стандартизирован и правила преобразования типов были несколько мягче, поэтому такая магия с приведениями работала.
В наше время не нужна трансляция С++ в С, но тем не менее концепция таблиц виртуальных функций и указателей на них осталась.
Have a legacy. Stay cool
#cppcore #goodoldc #compiler
#новичкам
Часто мы говорим о каких-то вещах в С++ и можем сказать, как они работают под капотом. Но стандарт языка не говорит о том, как компиляторы должны реализовывать те или иные фичи. Он только задает требование. Тем не менее подходы к реализации некоторых фичей повторяются в разных компиляторах. И отчасти это так из-за влияния 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🔥15❤9
Современные таблицы виртуальных функций
#опытным
С++ давно ушел от компиляции в С. Но таблицы виртуальных функций, как средство реализации динамического полиморфизма, остались в компиляторах.
Как устроены vtables сейчас, когда С++ напрямую компилируется в ассемблер?
Идея все та же:
👉🏿 На каждый полиморфный класс создается своя vtable
👉🏿 Она представляет собой просто массив адресов. В ассемблере адреса нетипизированные, поэтому они просто лежат там чиселками.
👉🏿 В каждом объекте хранится vptr - указатель на эту таблицу.
👉🏿 vtpr инициализируется в самом базовом классе его адресом его vtable и затем каждый конструктор наследника присваивает в него адрес своей таблицы
Реализации вносят свои особенности, но это ядро остается неизменным.
Вот примерчик(можете смотреть на годболте):
Будем сейчас разбирать gcc-шный асм.
Вот так выглядят таблицы для обоих классов:
quad - это 64-битное число. Видно, что таблицы - это статические массивы.
Первым числом у них является offset to top, это число нужно для корректной работы множественного наследования, не будем вдаваться в детали.
Второй число - адрес расположения информации о динамическом типе(RTTI) объекта. Эта информация нужна, например, для dynamic_cast'а.
Дальше расположены адреса виртуальных функций нужных классов. Заметьте, что адреса расположены в порядке объявления виртуального метода в классе. Если в дочернем классе переопределяется метод, то в его vtable указатель родительского метода заменяется на переопределенный(методы
В конструкторе Employee происходит такое:
Вызывается конструктор базового класса и сразу же после этого переприсваивается vptr на таблицу класса Employee.
Ну а виртуальный вызов выглядит просто как call нужного адреса:
#опытным
С++ давно ушел от компиляции в С. Но таблицы виртуальных функций, как средство реализации динамического полиморфизма, остались в компиляторах.
Как устроены 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
❤🔥22❤11👍6🔥6😁1
Компилятору достаточно лишь правильно положить аргументы функции в правильные регистры, согласно calling conventions. Какие аргументы нужно подготовить компилятор знает заранее, так как сигнатура виртуальных методов одинакова для всех переопределенных вариантов. Остается лишь call'ьнуть нужный указатель и происходит виртуальный вызов.
Подкапотоное устройство полиморфизма часто спрашивают на собесах. Теперь вы знаете, как отвечать почти на полный спектр вопросов по этой теме.
Know whats under the hood. Stay cool.
#compiler #cppcore
Подкапотоное устройство полиморфизма часто спрашивают на собесах. Теперь вы знаете, как отвечать почти на полный спектр вопросов по этой теме.
Know whats under the hood. Stay cool.
#compiler #cppcore
❤24👍14🔥9
Наследие Cfront. Манглирование
#новичкам
В С нет перегрузки функций, поэтому там люди вынуждены каким-то образом руками разделять имена функций.
Вместо
Там пишут:
Плюс линкеры работают с именами символов. А имя функции в С не включает в себя параметры. Поэтому во всей программе не может быть двух функций с одинаковыми именами. Линкер их банально не различит и выдаст ошибку множественного определения.
А в С++ была перегрузка и надо было каким-то образом плюсовые перегруженные функции превращать в неперегруженные сишные при трансляции кода. Для этого было придумано декорирование имен или name mangling.
Самый простой способ - добавлять к конце функции ее параметры в закодированном виде: ИмяФункции_ТипыПараметров:
Так как у нас не может быть двух перегрузок с разными возвращаемыми значениями, то кодирование типа возврата не нужно.
Но это самое базовое представление о декорировании имен. Давайте посмотрим, что еще может влиять на итоговое имя функции:
👉🏿 2 разных класса могут иметь методы с одинаковым названием. Так как при трансляции в С это были просто свободные функции, манглинг должен учитывать и имя класса:
👉🏿 Есть же еще и пространства имен. Они помогают разграничить скоуп существования имен. И названия пространства имен тоже манглировались в имена функций:
👉🏿 В С++ когда-то появились шаблоны. Шаблон всегда инстанцируется с каким-то типом. И чтобы различать эти инстанциации, Cfront манглировал типы шаблонных параметров в полное имя типа:
Конкретные преобразованные имена из поста могут быть не такими, какими их генерировал Cfront, но главное уловить идею.
В современных компиляторах тоже делается манглинг имен, чтобы линкер не ругался на одинаковые символы:
Шаблоны, неймспейсы, noexcept и const квалификаторы - все вшивается в имя символа.
Вы также можете вручную управлять манглингом: включать и выключать его:
Это нужно для совместимости ABI интерфейсов, предоставляемых библиотеками.
Поэтому декорирование имен живее всех живых и повсеместно используется в современных компиляторах.
Have a legacy. Stay cool
#cppcore #goodoldc #compiler
#новичкам
В С нет перегрузки функций, поэтому там люди вынуждены каким-то образом руками разделять имена функций.
Вместо
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
Новый год
Вот и подходит к своему завершению 2025 год.
Самое время подвести его итоги. Но я не буду перечислять список полезных или популярных постов. Кто захочет, то найдет. Я хочу поделиться состоянием(вайбом для зумеров) от завершающего свой последний аккорд года.
⚡️ Меня радует, что качество постов наконец-то меня удовлетворяет. Не по обертке(всегда бывают косяки), а по сути, структурированности и целостности. Можете ради интереса вернуться к первым постам и проследить эволюцию стиля. Количество юморесок и степень свободы изложения заметно сократились в угоду системности. Это логичная проф деформация, когда каждый раз при написании постов делаешь инъекцию 3-х кубиков стандарта внутривенно. Это несомненно позитивные изменения. Но теперь хочется, чтобы маятник вновь качнулся в сторону развлекательного аспекта. Все-таки мы здесь покекать пришли, а не плюсы учить. Или у вас не так?)
⚡️ Канал свел меня с прекрасными людьми, с которыми у нас началось сотрудничество по части плюсов и не только. Я очень вырос благодаря этим связям и не могу наудивляться, насколько у нас здесь клевые ребята собрались.
⚡️ Очень радует, что подписчики все чаще предлагают какие-то идеи для постов, особенно лично. Благодаря некоторым появились даже целые рубрики. Видно, что наше коммьюнити живое и активно развивается. У нас можно и поболтать за жизнь, и похоливарить на тему языков, и узнать что-то новенькое от крутых спецов. Есть более профессиональные чаты для гурманов и заядлых сомелье С++, но мне кажется, что определенную нишу мы заняли.
У меня это был эпический год по всем направлениям в жизни. Накопилась большая усталось, но при этом на лице улыбка.
И хочу вам сказать большое спасибо. Всем и каждому. Именно благодаря вам я расту и развиваюсь. Благодаря вам куча людей в принципе может регулярно получать небольшую порцию любимого С++ к обеду. Надеюсь, что канал вам нравится и вы находите его полезным. Без вас ничего этого не было бы.
Желаю вам в будущем году плотно и потно трудиться. Легко не будет, особенно начинающим, но у вас все получится. Идите к своим целям на всех парах. Верю в вас всех и каждого.
С Новым годом, друзья! С новым счастьем!☃️🎊🎉🎄
Вот и подходит к своему завершению 2025 год.
Самое время подвести его итоги. Но я не буду перечислять список полезных или популярных постов. Кто захочет, то найдет. Я хочу поделиться состоянием(вайбом для зумеров) от завершающего свой последний аккорд года.
⚡️ Меня радует, что качество постов наконец-то меня удовлетворяет. Не по обертке(всегда бывают косяки), а по сути, структурированности и целостности. Можете ради интереса вернуться к первым постам и проследить эволюцию стиля. Количество юморесок и степень свободы изложения заметно сократились в угоду системности. Это логичная проф деформация, когда каждый раз при написании постов делаешь инъекцию 3-х кубиков стандарта внутривенно. Это несомненно позитивные изменения. Но теперь хочется, чтобы маятник вновь качнулся в сторону развлекательного аспекта. Все-таки мы здесь покекать пришли, а не плюсы учить. Или у вас не так?)
⚡️ Канал свел меня с прекрасными людьми, с которыми у нас началось сотрудничество по части плюсов и не только. Я очень вырос благодаря этим связям и не могу наудивляться, насколько у нас здесь клевые ребята собрались.
⚡️ Очень радует, что подписчики все чаще предлагают какие-то идеи для постов, особенно лично. Благодаря некоторым появились даже целые рубрики. Видно, что наше коммьюнити живое и активно развивается. У нас можно и поболтать за жизнь, и похоливарить на тему языков, и узнать что-то новенькое от крутых спецов. Есть более профессиональные чаты для гурманов и заядлых сомелье С++, но мне кажется, что определенную нишу мы заняли.
У меня это был эпический год по всем направлениям в жизни. Накопилась большая усталось, но при этом на лице улыбка.
И хочу вам сказать большое спасибо. Всем и каждому. Именно благодаря вам я расту и развиваюсь. Благодаря вам куча людей в принципе может регулярно получать небольшую порцию любимого С++ к обеду. Надеюсь, что канал вам нравится и вы находите его полезным. Без вас ничего этого не было бы.
Желаю вам в будущем году плотно и потно трудиться. Легко не будет, особенно начинающим, но у вас все получится. Идите к своим целям на всех парах. Верю в вас всех и каждого.
С Новым годом, друзья! С новым счастьем!☃️🎊🎉🎄
23❤92🎄22👍16☃13❤🔥3👎1🗿1
Праздник Баг к нам приходит
Врываемся в новый год с поучительных историй.
На дворе 31 декабря 2к21 года. В России все нарезают салаты, готовятся веселиться и еще не знают, какой год их ждет впереди. На западе тоже все еще празднуют, хотя Рождество уже позади.
В первые минуты января сверкает салют, хлопаются хлопушки, шампанское переливается через края бокалов и перестает доставляться вся электронная почта, за обработку и доставку которой отвечал Microsoft Exchange Server.
Вот так в одну секунду по всему миру куча организаций перестала получать свою почту. В журнале событий сервера можно было найти такую запись: «Процесс сканирования FIP-FS Scan Process не прошел инициализацию. Ошибка: 0x8004005. Подробности ошибки: «Неопределенная ошибка» или «Код ошибки: 0x80004005». Описание ошибки: «Не удается преобразовать "2201010001" в длинное число».
Оказывается, что Exchange сохранял в журнале проверок антивируса даты в виде int32. Максимальным значением может быть 2147483647. У числа может быть всего 10 десятичных разрядов: первые 2 числа - последние 2 цифры года, дальше month/day/time. Для 2021 все было ок. Но чтобы представить первую секунду 2022 года нужно число 2201010001. А это уже перебор. Конвертация навернулась и полетели ошибки.
Эту проблему тогда обозвали Y2K22 bug, по аналогии с багом 2000 года(Y2K). В 20-м веке люди тоже часто обрабатывали только последние 2 цифры даты и в момент миллениума года сбрасывались просто до нуля.
Стоит ли говорить, что малварьный модель FIP-FS был скорее всего написан на С/C++?
Но не в этом дело. Если какой-то программист решил применить свою уникальную оптимизацию и не подумал чуть наперед, то здесь любым языком можно пакостей.
Вообще, ошибки связаные с переходом на новый год, не так уж и редки. У меня на проекте тоже был такой баг. Так что будьте внимательны при работе со временем. Хардкод и обрезание дат играют злую шутку.
А вы сталкивались с подобными ошибками? Расскажите в комментах.
Celebrate the holiday. Stay cool.
#fun
Врываемся в новый год с поучительных историй.
На дворе 31 декабря 2к21 года. В России все нарезают салаты, готовятся веселиться и еще не знают, какой год их ждет впереди. На западе тоже все еще празднуют, хотя Рождество уже позади.
В первые минуты января сверкает салют, хлопаются хлопушки, шампанское переливается через края бокалов и перестает доставляться вся электронная почта, за обработку и доставку которой отвечал Microsoft Exchange Server.
Вот так в одну секунду по всему миру куча организаций перестала получать свою почту. В журнале событий сервера можно было найти такую запись: «Процесс сканирования FIP-FS Scan Process не прошел инициализацию. Ошибка: 0x8004005. Подробности ошибки: «Неопределенная ошибка» или «Код ошибки: 0x80004005». Описание ошибки: «Не удается преобразовать "2201010001" в длинное число».
Оказывается, что Exchange сохранял в журнале проверок антивируса даты в виде int32. Максимальным значением может быть 2147483647. У числа может быть всего 10 десятичных разрядов: первые 2 числа - последние 2 цифры года, дальше month/day/time. Для 2021 все было ок. Но чтобы представить первую секунду 2022 года нужно число 2201010001. А это уже перебор. Конвертация навернулась и полетели ошибки.
Эту проблему тогда обозвали Y2K22 bug, по аналогии с багом 2000 года(Y2K). В 20-м веке люди тоже часто обрабатывали только последние 2 цифры даты и в момент миллениума года сбрасывались просто до нуля.
Стоит ли говорить, что малварьный модель FIP-FS был скорее всего написан на С/C++?
Но не в этом дело. Если какой-то программист решил применить свою уникальную оптимизацию и не подумал чуть наперед, то здесь любым языком можно пакостей.
Вообще, ошибки связаные с переходом на новый год, не так уж и редки. У меня на проекте тоже был такой баг. Так что будьте внимательны при работе со временем. Хардкод и обрезание дат играют злую шутку.
А вы сталкивались с подобными ошибками? Расскажите в комментах.
Celebrate the holiday. Stay cool.
#fun
👍23❤10🔥6😁4🫡3
Наследие Cfront. Компоновка
#новичкам
Продолжаем серию постов с прошлого года. Для тех, кто не в теме: пост про Cfront тут, начало серии тут.
С++ оказывал влияние не только на компиляторы, но и на линковщики.
Если в первых версиях языка и компилятора Cfront Бьерн сфокусировался на ООП и полиморфизме, то дальше были введены шаблоны и inline функции.
Шаблоны и inline функции просто исходя из своей механики работы предполагают то, что конкретные инстанциации и определения inline функций могут находиться в нескольких единицах трансляции в рамках одной программы.
Но в С был и есть One Definition Rule, который запрещал иметь более одного определения сущности в рамках одной программы.
Поначалу для решения этой проблемы использовались разные хаки: от макросов и магии с именами до ручной инстанциации шаблонов в одном cpp файле.
Это конечно было неудобно, но благо С++ становился все более популярным и влиятельным. Поэтому команда Cfront начала активно взаимодействовать с разработчиками линковщиков для того, чтобы ввести так поддержку слабых символов. Их в программе может быть сколько угодно, линковщик выберет один любой из них и будет ссылать на этот символ все заглушки. Главное, чтобы все определения символа были одинаковыми, иначе UB.
Также в С++ появились глобальные объекты. А глобальные объекты требуют своей инициализации (то есть выполнения кода) до main. В сижке такого нет, там исполнение пользовательского кода начинается с вызова main. Кстати поэтому в С нет SIOF.
Поэтому приходилось извращаться. Формировать массив конструкторов глобальных объектов и вызывать его первой инструкцией main. Разрушение происходило в конце main с помощью массива деструкторов.
Но это костыль и нужно было нормальное решение. Результатом совместной работы Cfront и линковщиков стали секции .ctors/.dtors в объектных и бинарных файлах. Там находятся информация о том, какие глобальные пользовательские объекты есть в коде. Код конструкторов и деструкторов объектов из этих секций выполняется до и после main соответственно.
Таким было наследие Cfront. The end.
Have a legacy. Stay cool.
#compiler
#новичкам
Продолжаем серию постов с прошлого года. Для тех, кто не в теме: пост про Cfront тут, начало серии тут.
С++ оказывал влияние не только на компиляторы, но и на линковщики.
Если в первых версиях языка и компилятора Cfront Бьерн сфокусировался на ООП и полиморфизме, то дальше были введены шаблоны и inline функции.
Шаблоны и inline функции просто исходя из своей механики работы предполагают то, что конкретные инстанциации и определения inline функций могут находиться в нескольких единицах трансляции в рамках одной программы.
Но в С был и есть One Definition Rule, который запрещал иметь более одного определения сущности в рамках одной программы.
Поначалу для решения этой проблемы использовались разные хаки: от макросов и магии с именами до ручной инстанциации шаблонов в одном cpp файле.
Это конечно было неудобно, но благо С++ становился все более популярным и влиятельным. Поэтому команда Cfront начала активно взаимодействовать с разработчиками линковщиков для того, чтобы ввести так поддержку слабых символов. Их в программе может быть сколько угодно, линковщик выберет один любой из них и будет ссылать на этот символ все заглушки. Главное, чтобы все определения символа были одинаковыми, иначе UB.
Также в С++ появились глобальные объекты. А глобальные объекты требуют своей инициализации (то есть выполнения кода) до main. В сижке такого нет, там исполнение пользовательского кода начинается с вызова main. Кстати поэтому в С нет SIOF.
Поэтому приходилось извращаться. Формировать массив конструкторов глобальных объектов и вызывать его первой инструкцией main. Разрушение происходило в конце main с помощью массива деструкторов.
Но это костыль и нужно было нормальное решение. Результатом совместной работы Cfront и линковщиков стали секции .ctors/.dtors в объектных и бинарных файлах. Там находятся информация о том, какие глобальные пользовательские объекты есть в коде. Код конструкторов и деструкторов объектов из этих секций выполняется до и после main соответственно.
Таким было наследие Cfront. The end.
Have a legacy. Stay cool.
#compiler
👍27🔥11❤8
Cppfront
#опытным
Есть интересный проект у Герба Саттера - компилятор Cppfront. По аналогии с Cfront, который компилировал С++ в С, Cppfront компилирует экспериментальный синтаксис Cpp2 в наш привычный C++.
Cpp2 - это можно сказать очень сахарный и слегка преобразованный С++. Герб говорит, что это не наследник и не соперник С++. Он помогает С++ эволюционировать, проверяя на нем функциональность, которую предлагают внести в С++.
Если просто: Герб и другие активные деятели С++ из комитета придумали Cpp2 и написали препроцессор, который преобразует новый синтаксис в наш привычный С++.
Как обычно завозятся фичи в стандарт: пишется огромный документ, где словами объясняется как должна работать фича. Но проверить на практике механизмы ее работы работы нельзя, можно лишь мысленные эксперименты проводить.
С появлением Cppfront, если кто-то хочет завести в стандарт С++ какую-то новую функциональность(например pattern matching), то ее можно относительно быстро реализовать в Сpp2 и поиграться с ней на практике. Коммьюнити может почелленджить решение и его использование, прочекать граничные и сложные случаи и тд.
То есть не нужно трогать существующие компиляторы, стандартные библиотеки, разбираться в синтаксическом анализе, копаться в кишках реализации и тд. Просто пишешь парсер и фичу уже можно потрогать руками.
И это будет работать на любой платформе, где есть С++ компилятор!
Простой пример кода на Cpp2:
Видно, что добавлены всякие аннотации, универсальные инициализации переменных и функций. Внутреннее наполнение функций примерно такое же, но под другой оберткой.
И очень прикольно, что вы сами можете поиграться с Cpp2 в годболте и посмотреть какой будет итоговый С++ код.
Вот ссылочка на гитхаб и на доку.
Help to evolve. Stay cool.
#compiler
#опытным
Есть интересный проект у Герба Саттера - компилятор Cppfront. По аналогии с Cfront, который компилировал С++ в С, Cppfront компилирует экспериментальный синтаксис Cpp2 в наш привычный C++.
Cpp2 - это можно сказать очень сахарный и слегка преобразованный С++. Герб говорит, что это не наследник и не соперник С++. Он помогает С++ эволюционировать, проверяя на нем функциональность, которую предлагают внести в С++.
Если просто: Герб и другие активные деятели С++ из комитета придумали Cpp2 и написали препроцессор, который преобразует новый синтаксис в наш привычный С++.
Как обычно завозятся фичи в стандарт: пишется огромный документ, где словами объясняется как должна работать фича. Но проверить на практике механизмы ее работы работы нельзя, можно лишь мысленные эксперименты проводить.
С появлением Cppfront, если кто-то хочет завести в стандарт С++ какую-то новую функциональность(например pattern matching), то ее можно относительно быстро реализовать в Сpp2 и поиграться с ней на практике. Коммьюнити может почелленджить решение и его использование, прочекать граничные и сложные случаи и тд.
То есть не нужно трогать существующие компиляторы, стандартные библиотеки, разбираться в синтаксическом анализе, копаться в кишках реализации и тд. Просто пишешь парсер и фичу уже можно потрогать руками.
И это будет работать на любой платформе, где есть С++ компилятор!
Простой пример кода на Cpp2:
#include <iostream>
#include <string>
name: () -> std::string = {
s: std::string = "world";
decorate(s);
return s;
}
decorate: (inout s: std::string) = {
s = "[" + s + "]";
}
auto main() -> int {
std::cout << "Hello " << name() << "\n";
}
Видно, что добавлены всякие аннотации, универсальные инициализации переменных и функций. Внутреннее наполнение функций примерно такое же, но под другой оберткой.
И очень прикольно, что вы сами можете поиграться с Cpp2 в годболте и посмотреть какой будет итоговый С++ код.
Вот ссылочка на гитхаб и на доку.
Help to evolve. Stay cool.
#compiler
🔥31👍15❤11
Время жизни и range-based for
#новичкам
Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:
Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?
Перед ответом экскурс в стандарт. Есть у вас range-based for:
Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:
Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.
В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.
А вот во что преобразуется цикл из самого первого примера поста:
range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.
Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:
Другие решения рассмотрим в следующих постах.
Avoid dangling references. Stay cool.
#cppcore #cpp11
#новичкам
Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?
Перед ответом экскурс в стандарт. Есть у вас range-based for:
for(const auto& item: <range>) {
process(item);
}Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:
auto&& range = <range>;
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
process(item);
}
Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.
std::vector<int> generate() {
return {1, 2, 3, 4, 5};
}
for (const auto& item: generate()) {
...
}
// range-based for transforms into
auto&& range = generate();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
...
}В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.
А вот во что преобразуется цикл из самого первого примера поста:
auto&& range = generateData().items();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
int x = *it;
process(x);
}
range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.
Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
auto data = generateData();
for (int x : data.items()) {
process(x);
}Другие решения рассмотрим в следующих постах.
Avoid dangling references. Stay cool.
#cppcore #cpp11
❤33🔥23👍8😁5
Инициализация внутри range-based for
#новичкам
Решение из предыдущего поста не совсем идеальное. Посмотрите сами:
Переменная data больше нигде кроме цикла не нужна. Но она засоряет собой и своим именем скоуп, требует отдельной инструкции, да и хотелось бы сразу избавляться от ненужного.
Было бы идеально инициализировать data прям внутри скоупа цикла, как это делается в условных операторах. И с С++20 это стало возможным:
Вот и все. Мы также создаем lvalue объект, но теперь его время жизни ограничено телом цикла. При выходе из цикла он уничтожится и мы можем забыть про этот объект.
Кстати. А задавались вы вопросом "нужны ли инструкции инициализации в цикле while?"
В if есть, в во всех вариантах цикла for есть. Для единообразия нужно и в while.
Как думаете?
На самом деле это будет избыточно. while+инициализация выглядела бы так:
Это легко заменяется на классический for без указания инструкций, которые после каждой итерации выполняются:
Даже короче получилось. Поэтому инициализация в while не нужна.
Forget about garbage. Stay cool.
#cpp20
#новичкам
Решение из предыдущего поста не совсем идеальное. Посмотрите сами:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
auto data = generateData();
for (int x : data.items()) {
process(x);
}Переменная data больше нигде кроме цикла не нужна. Но она засоряет собой и своим именем скоуп, требует отдельной инструкции, да и хотелось бы сразу избавляться от ненужного.
Было бы идеально инициализировать data прям внутри скоупа цикла, как это делается в условных операторах. И с С++20 это стало возможным:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (auto data = generateData(); int x : data.items()) {
process(x);
}Вот и все. Мы также создаем lvalue объект, но теперь его время жизни ограничено телом цикла. При выходе из цикла он уничтожится и мы можем забыть про этот объект.
Кстати. А задавались вы вопросом "нужны ли инструкции инициализации в цикле while?"
В if есть, в во всех вариантах цикла for есть. Для единообразия нужно и в while.
Как думаете?
На самом деле это будет избыточно. while+инициализация выглядела бы так:
while(size_t i = 0; some_condition) {...}Это легко заменяется на классический for без указания инструкций, которые после каждой итерации выполняются:
for(size_t i = 0; some_condition;) {...}Даже короче получилось. Поэтому инициализация в while не нужна.
Forget about garbage. Stay cool.
#cpp20
❤25👍14🔥6
Продлеваем жизнь временного объекта range based for
#опытным
На самом деле у проблемы в этом коде:
есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
#опытным
На самом деле у проблемы в этом коде:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t1) { return t1; } // всегда возвращает висячую ссылку
T g();
void foo()
{
for (auto e : f1(g())) {} // OK: время жизни возвращаемого значения g() продлено
for (auto e : f2(g())) {} // UB: локальный объект t1 функции f2 все равно разрушается при выходе из скоупа f2
}
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
👍35☃14❤10🔥5
Предотвращаем висячие ссылки
#опытным
Давайте снова взглянем на этот пример:
Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
#опытным
Давайте снова взглянем на этот пример:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
auto& vec = generateData().items();
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
🔥20❤13👍9🤯3
split
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
А как сделатьпростейшую вещь разделить строку на С++?
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']
А как сделать
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
auto range = text | std::views::split(' ');
for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three fourЕсли вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
std::string text = "one two three four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
🔥38👍11❤9🤯2👎1
Прелести рэнджей
#опытным
Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.
Возьмем пример из прошлого поста:
Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.
И на самом деле мы получаем кучу аллокаций.
Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.
Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.
Но можно поступить проще и воспользоваться ренджами!
Рэнджи ленивые. Прям как студенты: всем на последнем курсе заранее дают задачу написать диплом, но обычно его начинают писать, когда научрук уже дает последнее китайское предупреждение вслед за пендалем, что нужно сделать работу уже вчера.
Но у рэнджей это скорее преимущество.
Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.
Мы просто пишем:
И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.
С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:
Отбрасываем первые два элемента и берем только первый оставшийся. И опять же, ничего не происходит.
Вычисления происходят, когда мы пытаемся что-то узнать о результирующем диапазоне, например, пустой ли он:
Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:
И здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.
Возможно find работал бы и быстрее(а это еще проверить надо), но рэнджи предлагают более понятную альтернативу самописному коду.
Be expressive. Stay cool.
#cpp20
#опытным
Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.
Возьмем пример из прошлого поста:
std::string text = "one two three four";
std::vector<std::string_view> strs;
boost::split(strs, text, boost::is_any_of(" "));
Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.
И на самом деле мы получаем кучу аллокаций.
Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.
Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.
Но можно поступить проще и воспользоваться ренджами!
Рэнджи ленивые. Прям как студенты: всем на последнем курсе заранее дают задачу написать диплом, но обычно его начинают писать, когда научрук уже дает последнее китайское предупреждение вслед за пендалем, что нужно сделать работу уже вчера.
Но у рэнджей это скорее преимущество.
Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.
Мы просто пишем:
auto range = text | std::views::split(' ');И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.
С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:
auto range = text | std::views::split(' ') | std::views::drop(2) | std::views::take(1);Отбрасываем первые два элемента и берем только первый оставшийся. И опять же, ничего не происходит.
Вычисления происходят, когда мы пытаемся что-то узнать о результирующем диапазоне, например, пустой ли он:
if (range.empty()) {
std::cerr << "There are less than three words in text";
}Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:
std::string_view str{*range.begin()};
std::cout << str << std::endl;
// OUTPUT: threeИ здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.
Возможно find работал бы и быстрее(а это еще проверить надо), но рэнджи предлагают более понятную альтернативу самописному коду.
Be expressive. Stay cool.
#cpp20
👍27🔥14❤9😁3
std::to_array
#опытным
std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):
либо надеяться на CTAD и не передавать никаких параметров:
Если так сложились звезды, что у вас тип инициализатора совпадает с желаемым типом элементов массива, то вам прекрасно подойдет последний вариант.
И вот здесь всплавает тот самый нюанс.
Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:
и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.
Мне конечно не жалко написать эту жалкую тройку, пальцы не сотрутся. Но зачем? Компилятор же может определить размер и это уменьшит количество потенциальных ошибок.
У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:
Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.
Применяется std::to_array так:
Да, пишем на пару символов больше, зато размер будет четко соблюдаться.
Automate your tools. Stay cool.
#cpp20
#опытным
std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):
std::array<int, 4> arr = {1, 2, 3, 4};либо надеяться на CTAD и не передавать никаких параметров:
std::array arr = {1, 2, 3, 4};Если так сложились звезды, что у вас тип инициализатора совпадает с желаемым типом элементов массива, то вам прекрасно подойдет последний вариант.
И вот здесь всплавает тот самый нюанс.
Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:
std::array<std::string_view> arr = {"La", "Bu", "Bu"};и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.
Мне конечно не жалко написать эту жалкую тройку, пальцы не сотрутся. Но зачем? Компилятор же может определить размер и это уменьшит количество потенциальных ошибок.
У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:
namespace detail {
template <class T, std::size_t N, std::size_t... I>
constexpr std::array<std::remove_cv_t<T>, N>
to_array_impl(T (&a)[N], std::index_sequence<I...>) {
return {{a[I]...}};
}
} // namespace detail
template <class T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T (&a)[N]) {
return detail::to_array_impl(a, std::make_index_sequence<N>{});
}Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.
Применяется std::to_array так:
constexpr auto names = std::to_array<std::string_view>({"Goku", "Luffy", "Ichigo", "Gojo", "Joseph", "L"});Да, пишем на пару символов больше, зато размер будет четко соблюдаться.
Automate your tools. Stay cool.
#cpp20
❤26👍15🔥9🥱2☃1
WAT
#опытным
Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.
Делаем вот так:
Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.
Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.
После получаем значение этой переменной и сравниваем ее с оригиналом.
Ну и в конце ищем среди массива строку эту runtime_str.
Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.
Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."
WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?
Дьявол кроется в деталях.
Вспоминаемшкольную математеку CTAD. Какой тип элементов массива выведется?
Правильно, const char *.
А как std::ranges::find сравнивает такие элементы?
Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.
Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.
Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.
А вот последний ассерт просто говорит о том, что указатель
И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.
В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.
Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.
Express your wishes precisely. Stay cool.
#cppcore #cpp17
#опытным
Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.
Делаем вот так:
constexpr std::array array = {"I", "love", "C++"};
int main() {
if (auto iter = std::ranges::find(array, "C++"); iter == std::end(array)) {
assert(false && "comptime arg");
}
// let's go with runtime now
if (setenv("RUNTIME", array[2], 0) != 0) {
assert(false && "setenv");
}
char *runtime_str = getenv("RUNTIME");
assert(strcmp(runtime_str, array[2]) == 0 && "equal strings");
if (auto magick_iter = std::ranges::find(array, runtime_str);
magick_iter == std::end(array)) {
assert(false && "runtime arg");
}
}Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.
Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.
После получаем значение этой переменной и сравниваем ее с оригиналом.
Ну и в конце ищем среди массива строку эту runtime_str.
Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.
Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."
WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?
Дьявол кроется в деталях.
Вспоминаем
Правильно, const char *.
А как std::ranges::find сравнивает такие элементы?
Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.
Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.
Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.
А вот последний ассерт просто говорит о том, что указатель
runtime_str не был найден в массиве, потому что там нет такого адреса.И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.
В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.
Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.
Express your wishes precisely. Stay cool.
#cppcore #cpp17
❤21👍10😁7🔥5👎1