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

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

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

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

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

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

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

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

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

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


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

struct Base {
int x;
};

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

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

// Constructor body (if presented)
}

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

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

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


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

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

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

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

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

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


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

struct Base {
};

struct Derived {
struct Base _base;

};

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

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

// Destructor Derived body

Base_destructor(&this->_base);
}


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

Have a legacy. Stay cool.

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

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

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

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

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

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

virtual ~Person() = default;
};

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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


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


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

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

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
👍23🔥159
​​Компилятору достаточно лишь правильно положить аргументы функции в правильные регистры, согласно calling conventions. Какие аргументы нужно подготовить компилятор знает заранее, так как сигнатура виртуальных методов одинакова для всех переопределенных вариантов. Остается лишь call'ьнуть нужный указатель и происходит виртуальный вызов.

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

Know whats under the hood. Stay cool.

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

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

Вместо

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


Там пишут:

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


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

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

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

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

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


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

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

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

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

// transform into

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


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

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

// transform into

void Graphics_Canvas_clear(struct Canvas* this)


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

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

Stack<int> stack;

// transforms into

Stack_int_push_i(int value);


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

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


void Circle::rotate(int);

// transforms into

_ZN6Circle6rotateEi

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


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

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

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


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

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

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
22👍21🔥13
​​Время жизни и range-based for
#новичкам

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

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
​​WAT
#опытным

Спасибо, ₿ 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?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?

Дьявол кроется в деталях.

Вспоминаем школьную математеку CTAD. Какой тип элементов массива выведется?

Правильно, const char *.

А как std::ranges::find сравнивает такие элементы?

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

Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.

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

А вот последний ассерт просто говорит о том, что указатель runtime_str не был найден в массиве, потому что там нет такого адреса.

И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.

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

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

Express your wishes precisely. Stay cool.

#cppcore #cpp17
21👍12😁7🔥5👎1
Пользовательские литералы
#новичкам

Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.

В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:

👉🏿 Целочисленные: 5, 42, 0xFF.

👉🏿 С плавающей точкой: 3.14, 6.02e23.

👉🏿 Символьные: 'a', '\n'.

👉🏿 Строковые: "Hello, world!".

👉🏿 Логические: true, false.

👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя - nullptr.

Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.

Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.

А мы живем все-таки в мире объектов
. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.

Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.

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

Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!

Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:

class Temperature {
private:
double kelvin; // for precicion and consistency

explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};

Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}

{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}


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

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

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

Extend your capabilities. Stay cool.

#cppcore #cpp11
24👍13🔥11🤯2🤷‍♂1🤪1
​​Пользовательские литералы. А зачем?
#опытным

В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.

Поехали:

🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:

auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;


Меньше деталей, больше фокуса на происходящем.

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

double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree


Получится неожиданный результат, даже если функция работает верно.

Вот шобы такого не было, можно использовать соответствующие литералы:

class Radian {...};

Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}

SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error


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

Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:

using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char *>);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);


Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.

🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:

template<size_t N>
struct FixedString {
char data[N];

constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}

constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};

template <FixedString str>
class Class {};

Class<"Hello World!"> cl;


И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.

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

Be useful. Stay cool.

#cppcore #cpp11 #cpp20
👍159🔥7😁1🤯1
​​Доступ к приватным членам. Указатали
#новичкам

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

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

То есть банально

class MyClass {
private:
int secret = 42;
};

void illegalAccess(MyClass &obj) {
int *ptr = (int *)&obj; // assume that secret is first member
std::cout << "Illegal: " << *ptr << std::endl;
}


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

Но здесь есть целых 2 проблемы.

1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.

2️⃣ Вторая еще серьезнее. Цитата из стандарта:

The interpretation of a given construct is established without regard to access control. If the interpretation established makes use of inaccessible members or base classes, the construct is ill-formed.

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

Первую проблему можно обойти с помощью дыры в стандарте memcpy:

void AccessWithMemcpy(MyClass& obj) {
int value;
std::memcpy(&value, &obj, sizeof(int));
std::cout << value << std::endl;
}


Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.

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

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

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

Be legal. Stay cool.

#cppcore #badpractice
23👍17😁9🔥7❤‍🔥1😭1
​​WAT
#новичкам

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

Ответ на квиз из поста выше - на экран выведется 8.

WAT? Строковые литералы конкатенируются? Да еще и пользовательский суффикс между двух литералов применяется к конкатенации?

Вообще, да. Сейчас во всем разберемся.

Для начала. Да, c-style строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.

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

Можно это делать с помощью символов экранирования, например так:

auto str = "Suuuuuuuuuuuuuuupppeeeeeeeeeeeeeeeeeeeeeeeeeeerrrr
loooooooooooooooooooooooooooooong \ striiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";


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

Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:

auto str = "Hello "
// void
"World!";
std::cout << str << std::endl;

// OUTPUT
// Hello World!


Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.

Ну и теперь понятно, почему пользовательский суффикс применяется к полной конкатенации c-style строки. Фаза конкатенации строковых литералов идет раньше этапа компиляции, на котором определяется значение аргументов оператора. Поэтому аргументом и является уже склеенная строка.

Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.

Кстати, такая склейка есть только у строковых литералов. Цифры в числовых литералах обязательно должны идти подряд:

int num1 = 123; // OK
int num2 = 12 23 // ERROR
int num3 = 1'234; // if you want to logicaly devide large number


Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).

Don't break into pieces. Be whole. Stay cool.

#cppcore #cpp11
🔥25👍7🤯65❤‍🔥1
​​Шо там не так с union?
#опытным

Объединения в С++, мягко говоря, не любят. И сегодня мы обсудим почему.

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

union U {
int integer;
float floating;
char character;
};

U u;
u.integer = 42; // now u hold int
u.floating = 3.14; // now u hold float
u.character = 'a'; // now u hold char


U может хранить или целое число, или число с плавающей точкой, или символ. Последнее записанное поле становится "активным" полем.

Объединение позволяет, например, хранить несвязанные типы в одном массиве.

Но с юнионами нужно быть очень осторожным. Вот представьте, вам на вход функции пришел массив объединений. Как понять, какое реально поле в нем лежит?

Никак. Такого функционала нет. Поэтому приходится делать разные обвязки:

struct Helper {
enum class Type { Int, Float, Char };

union U {
int integer;
float floating;
char character;
};

Type objectType;
U value;

Helper(int i) : objectType(Type::Int), value{.integer = i} {}
Helper(float f) : objectType(Type::Float), value{.floating = f} {}
Helper(char c) : objectType(Type::Char), value{.character = c} {}
};


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

Helper h{42};

h.value.floating = 3.14;


Мы изменили активное поле без изменения objectType. А теперь где-то в другом месте кода:

if (h.objectType == Helper::Type::Int) {
std::cout << h.value.integet << std::endl;
}


Вот и все. Мы вышли на кривую дорожку неопределенного поведения.

Доступ до неактивного члена объединения - UB.

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

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

В С++ давно уже есть прекрасная типобезопасная альтернатива - std::variant. При доступе не к тому типу он хотя бы выкинет исключение std::bad_variant_access. И будет понятно хотя бы, где собака зарыта. Точнее не всегда понятно где конкретно, но точно понятно, что зарыта именно собака. А это уже облегчает поиски.

Be safe. Stay cool.

#cppcore
27🔥12👍11
​​union class
#опытным

В прошлом посте мы упомянули, что union - это такой специальный класс. Это что значит, объединение может иметь методы?

Представьте себе, да!

Начиная с С++11 union'ы могут иметь полноценные конструкторы, деструкторы и другие методы.

Но есть ограничения:

👉🏿 не должно быть виртуальных методов

👉🏿 юнион не может быть наследником

👉🏿 юнион не может быть базовым классом

👉🏿 юнион не может хранить ссылочные типы

Во всем остальном - такой же класс!

Но вот как-то не можется мне придумать юзкейсы методов объединения.

Конструкторы и деструкторы нужны, чтобы union мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.

Например:

union U {
int i;
float f;
std::string s;
};

U u;


Попытка скомпилировать это дело приведет к ошибкам:
error: use of deleted function 'U::U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string()
requires is_default_constructible_v<_Alloc>

error: use of deleted function 'U::~U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::~basic_string()


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

union U {
U() {}
~U() {}
int i;
float f;
std::string s;
};

U u; // ОК


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

А вы используете методы объединений в своих проектах? Если да, то расскажите зачем оно может понадобиться, будет интересно.

Expand your horizons. Stay cool.

#cppcore #cpp11
17👍8🔥8