Наследие 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
Время жизни и 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
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👍12😁7🔥5👎1
Пользовательские литералы
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.
В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:
👉🏿 Целочисленные:
👉🏿 С плавающей точкой:
👉🏿 Символьные: '
👉🏿 Строковые:
👉🏿 Логические:
👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя -
Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.
Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.
А мы живем все-таки в мире объектов. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.
Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.
То есть в С++ давно был механизм, с помощью которого можно было изменять тип литерала через суффикс. Стоит лишь дать возможность программистам самостоятельно определять свои суффиксы, чтобы по-своему интерпретировать литерал.
Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!
Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:
Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.
Просто посмотрите, насколько сократился код, уменьшилось количество скобок и увеличилась читаемость. Выглядит круто.
Это было небольшое интро, в следующий раз рассмотрим кейсы, когда пользовательские литералы могут принести реальную пользу.
Extend your capabilities. Stay cool.
#cppcore #cpp11
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому 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.
Поехали:
🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:
Меньше деталей, больше фокуса на происходящем.
🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании
Получится неожиданный результат, даже если функция работает верно.
Вот шобы такого не было, можно использовать соответствующие литералы:
🥨 Автоматический вывод типов может легкосломаться вывести не тот тип, который вы ожидаете, если вы работаете с сырыми литералами. Пользовательский литерал же сразу на месте конструирует нужный объект и компилятор будет правильно интерпретировать его тип.
Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:
Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.
🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:
И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.
В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.
Be useful. Stay cool.
#cppcore #cpp11 #cpp20
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать 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
👍15❤9🔥7😁1🤯1
Доступ к приватным членам. Указатали
#новичкам
Если по-честному, то все эти спецификаторы доступа к членам класса, это чисто синтаксическое ограничение на непреднамеренное использование в коде имен непубличных членов. Ну и способ выделение в классе интерфейса, чтобы сказать другим программистам, какими легальными способами можно оперировать объектом.
Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.
То есть банально
Как бы логично предположить, что если у класса только одно поле, то сам объект будет состоять только из этого поля. И можно спокойно привести указатель на объект к указателю на поле.
Но здесь есть целых 2 проблемы.
1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.
2️⃣ Вторая еще серьезнее. Цитата из стандарта:
Если ваша кодовая конструкция интерпретируется, как использование недоступных вам мемберов, то конструкция ill-formed. Не сказано, что сама программа ifndr, но это все равно значит, что код выше не соответствует правилам языка.
Первую проблему можно обойти с помощьюдыры в стандарте memcpy:
Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.
Если мемберов много, то нужно будет учитывать выравнивание полей в объекте.
В общем, это все может работать на конкретной архитектуре и компиляторе, если вы сами все руками в каждом конкретном случае проверяете. Но стандарт вас осуждает и ничего не обещает.
Пусть в конце каждого поста из серии будет эпилог: получать доступ к приватным полям - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.
Be legal. Stay cool.
#cppcore #badpractice
#новичкам
Если по-честному, то все эти спецификаторы доступа к членам класса, это чисто синтаксическое ограничение на непреднамеренное использование в коде имен непубличных членов. Ну и способ выделение в классе интерфейса, чтобы сказать другим программистам, какими легальными способами можно оперировать объектом.
Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.
То есть банально
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, но это все равно значит, что код выше не соответствует правилам языка.
Первую проблему можно обойти с помощью
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 строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.
С длинными строками, которые целиком не влезают на экран, неудобно работать: читать и редактировать. А если у вас настроены линтеры на ограничение длины строки, то все равно придется как-то разбивать на части эту длинную строку.
Можно это делать с помощью символов экранирования, например так:
Но здесь будут проблемы с тем, что вторая часть должна начинаться с самого начала следующей строки, иначе пробелы будут включены в сам литерал. Представьте, что будет с кодом, в котором внутри функций(например для репорта ошибок) будут так разделяться литералы.
Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:
Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.
Ну и теперь понятно, почему пользовательский суффикс применяется к полной конкатенации c-style строки. Фаза конкатенации строковых литералов идет раньше этапа компиляции, на котором определяется значение аргументов оператора. Поэтому аргументом и является уже склеенная строка.
Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.
Кстати, такая склейка есть только у строковых литералов. Цифры в числовых литералах обязательно должны идти подряд:
Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).
Don't break into pieces. Be whole. Stay cool.
#cppcore #cpp11
#новичкам
Спасибо, @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🤯6❤5❤🔥1
Шо там не так с union?
#опытным
Объединения в С++, мягко говоря, не любят. И сегодня мы обсудим почему.
Юнион - это такой специальный класс, который хранит только одно из своих полей. Любое, но одно. Объект объединения может хранить разные поля в одной и той же памяти. А размер union равен размеру максимального поля.
U может хранить или целое число, или число с плавающей точкой, или символ. Последнее записанное поле становится "активным" полем.
Объединение позволяет, например, хранить несвязанные типы в одном массиве.
Но с юнионами нужно быть очень осторожным. Вот представьте, вам на вход функции пришел массив объединений. Как понять, какое реально поле в нем лежит?
Никак. Такого функционала нет. Поэтому приходится делать разные обвязки:
При создании объекта Helper мы теперь явно перечислением задаем тип, который лежит в юнион. И все кажется безопасным, пока мы не сделаем ошибку:
Мы изменили активное поле без изменения objectType. А теперь где-то в другом месте кода:
Вот и все. Мы вышли на кривую дорожку неопределенного поведения.
Доступ до неактивного члена объединения - UB.
То есть. Мало того, что эта штука не самостоятельная. Так еще и если мы набагали, то это может обернуться вагоном трудноотловимых проблем в рантайме.
По сути, объединениями можно пользоваться, если руки из нужного места растут. Но если с руками не повезло - расстрел. И вы будете долго ковыряться в кишках проекта с надеждой хоть на какую-то подсказку о том, что произошло.
В С++ давно уже есть прекрасная типобезопасная альтернатива - std::variant. При доступе не к тому типу он хотя бы выкинет исключение std::bad_variant_access. И будет понятно хотя бы, где собака зарыта. Точнее не всегда понятно где конкретно, но точно понятно, что зарыта именно собака. А это уже облегчает поиски.
Be safe. Stay cool.
#cppcore
#опытным
Объединения в С++, мягко говоря, не любят. И сегодня мы обсудим почему.
Юнион - это такой специальный класс, который хранит только одно из своих полей. Любое, но одно. Объект объединения может хранить разные поля в одной и той же памяти. А размер 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 charU может хранить или целое число, или число с плавающей точкой, или символ. Последнее записанное поле становится "активным" полем.
Объединение позволяет, например, хранить несвязанные типы в одном массиве.
Но с юнионами нужно быть очень осторожным. Вот представьте, вам на вход функции пришел массив объединений. Как понять, какое реально поле в нем лежит?
Никак. Такого функционала нет. Поэтому приходится делать разные обвязки:
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 мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.
Например:
Попытка скомпилировать это дело приведет к ошибкам:
Плюс объекты хочется перемещать и мувать, там тоже могут быть нетривиальные специальные методы.
Ну а если уж разрешили специальные методы определять, то и обычные разрешили до кучи.
А вы используете методы объединений в своих проектах? Если да, то расскажите зачем оно может понадобиться, будет интересно.
Expand your horizons. Stay cool.
#cppcore #cpp11
#опытным
В прошлом посте мы упомянули, что 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
Доступ к приватным членам. Интерпретация.
#опытным
Продолжаем исследовать грязь и разврат. Предыдущий пост тут.
Указатели, блин, сложная тема. А для задачи доступа к приватным полям - особенно. Для объектов классов с несколькими полями нужно считать много деталей: учитывать выравнивание полей в объекте и на этой базе высчитывать сдвиг типизированного указателя. Возможно даже несколько кастов понадобиться.
Программисты - люди ленивые, поэтому давайте как-то упрощать.
Допустим, у меня есть класс Target с множеством полей. А что, если где-то будет структурка Hack, идентичная по расположению полей? Может быть как-то можно интерпретировать данные Target, как данные Hack?
Конечно можно. В плюсах можно ВСЕ.
Конкретно интерпретировать одни данные, как другие, позволяет делать union. Он хранит один объект любого типа из списка, перечисленного внутри union. Но объединение не знает, объект какого конкретного типа он хранит. Это возлагается на плечи программиста, а union позволяет "достать" этот объект через любой тип.
Внутри union определяет структуру с точно таким же layout'ом, как у Target, только члены у нее публичные. И интерпретируем данные Target как набор полей этой структуры. Так мы можем достать stolen_x и stolen_y.
И это даже не UB, а вполне валидное поведение! Стандарт гарантирует, что обе структуры layout-compatible и имеют общую начальную последовательность полей. И в этом случае union может их читать без угроз со стороны библии С++.
Еcли же классы будут не такие простые, даже просто с полями с разными доступами, то стандарт уже гарантировать не будет. Как мы помним, в общем случае доступ к неактивному члену юниона - ub.
Кстати, если уж мы хотим "интерпретировать" один объект как другой, можно использовать приведение типов реинтерпретацией aka reinterpret_cast:
Тут уже без сомнения UB, потому что нарушаем strict aliasing, но это не мешает программе успешно отрабатывать.
Пс: Получать доступ к приватным полям вне класса - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.
#cppcore #badpractice
#опытным
Продолжаем исследовать грязь и разврат. Предыдущий пост тут.
Указатели, блин, сложная тема. А для задачи доступа к приватным полям - особенно. Для объектов классов с несколькими полями нужно считать много деталей: учитывать выравнивание полей в объекте и на этой базе высчитывать сдвиг типизированного указателя. Возможно даже несколько кастов понадобиться.
Программисты - люди ленивые, поэтому давайте как-то упрощать.
Допустим, у меня есть класс Target с множеством полей. А что, если где-то будет структурка Hack, идентичная по расположению полей? Может быть как-то можно интерпретировать данные Target, как данные Hack?
Конечно можно. В плюсах можно ВСЕ.
Конкретно интерпретировать одни данные, как другие, позволяет делать union. Он хранит один объект любого типа из списка, перечисленного внутри union. Но объединение не знает, объект какого конкретного типа он хранит. Это возлагается на плечи программиста, а union позволяет "достать" этот объект через любой тип.
struct Target {
private:
int x = 42;
double y = 3.14;
};
union HackUnion {
HackUnion() {}
Target target;
struct {
int stolen_x;
double stolen_y;
} hacker;
};
int main() {
HackUnion u;
u.target = Target();
std::cout << "Stolen x: " << u.hacker.stolen_x << std::endl;
std::cout << "Stolen y: " << u.hacker.stolen_y << std::endl;
}
// OUTPUT:
// Stolen x: 42
// Stolen y: 3.14Внутри union определяет структуру с точно таким же layout'ом, как у Target, только члены у нее публичные. И интерпретируем данные Target как набор полей этой структуры. Так мы можем достать stolen_x и stolen_y.
И это даже не UB, а вполне валидное поведение! Стандарт гарантирует, что обе структуры layout-compatible и имеют общую начальную последовательность полей. И в этом случае union может их читать без угроз со стороны библии С++.
Еcли же классы будут не такие простые, даже просто с полями с разными доступами, то стандарт уже гарантировать не будет. Как мы помним, в общем случае доступ к неактивному члену юниона - ub.
Кстати, если уж мы хотим "интерпретировать" один объект как другой, можно использовать приведение типов реинтерпретацией aka reinterpret_cast:
struct Target {
private:
int x = 42;
double y = 3.14;
};
struct Hacker {
int stolen_x;
double stolen_y;
};
Target target;
Hacker* hacker = reinterpret_cast<Hacker*>(&target);
std::cout << "Stolen x: " << hacker->stolen_x << std::endl;
std::cout << "Stolen y: " << hacker->stolen_y << std::endl;Тут уже без сомнения UB, потому что нарушаем strict aliasing, но это не мешает программе успешно отрабатывать.
Пс: Получать доступ к приватным полям вне класса - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.
#cppcore #badpractice
❤23👍15🔥6😁2
Доступ к приватным членам. Други-функции.
#новичкам
В этой серии постов мы рассматриваем в том числе легальные способы получения доступа к приватным членам.
С++ - вообще амбассадор языковых средств обходов ограничений. Есть константный объект, которых хочется изменить? Пожалуйста, const_cast. Застряли где-то в пучинах кода и не можете красиво выйти? Пожалуйста, goto.
В ту же копилку можно отнести друзей. Ключевое слово friend в С++ позволяет сущности получить доступ ко всем непубличным члена класса. Сегодня рассмотрим друзей-функций.
Завести друга очень просто(по крайней мере в С++). Вам нужно объявить или определить функцию внутри скоупа класса и пометить ее ключевым словом friend. Тогда класс начинает доверять вашей функции настолько, что раскрывает ей все свои секретики.
Можно также просто объявить функцию внутри класса, а определить ее вне:
Разница в том, что в первом варианте функция ищется только с помощью ADL по аргументу, а во втором - по скоупу, в котором она определена. Но это уже для гиков.
В чем особенность френдов? Их недолюбливают, но иногда без них никуда.
С одной стороны, вы даете другой сущности доступ к приватным полям класса. И сразу возникает вопрос: а зачем? Почему нельзя обойтись публичным интерфейсом?
Но иногда действительно нельзя. И хрестоматийным примером здесь будет перегрузка операторов для симметричных операций.
Например, у вас есть класс для работы с комплексными числами и вы хотите определить для этого класса операцию умножения на скаляр:
Честный метод-operator* будет работать только в том случае, когда объект комплексного числа стоит слева. А за умножение
Конкретно для этого примера есть вопросики по тому, зачем делать приватными поля комплексного числа, но тут важна суть работы с friend.
Если ваш класс работает со стандартными бинарными операторами(сравнения, арифметика) и у него есть непубличные поля, то вам может понадобиться определить эти стандартные глобальные операторы друзьями. Вот ссылочка на пример из библиотеки Eigen. Тот же самый принцип работает, например, с оператором вывода в поток:
Тоже пример дружественной перегрузки вывода из Eigen.
Страуструп кстати дает один интересный иллюстрационный пример: в рэнджах момент окончания последовательности задается в более общем виде, чем итератор - sentinel(ограничитель). Это по сути объект любого типа, главное чтобы итератор мог уметь сравниться с ним. И вот для сравнения итератора и ограничителя может понадобиться определить дружественные операторы == и !=.
За пределами же кейсов с операторами использование функций-друзей должно подвергаться строгим проверкам на предмет попыток обойти кривой дизайн.
Value your friends. Stay cool.
#cppcore
#новичкам
В этой серии постов мы рассматриваем в том числе легальные способы получения доступа к приватным членам.
С++ - вообще амбассадор языковых средств обходов ограничений. Есть константный объект, которых хочется изменить? Пожалуйста, const_cast. Застряли где-то в пучинах кода и не можете красиво выйти? Пожалуйста, goto.
В ту же копилку можно отнести друзей. Ключевое слово friend в С++ позволяет сущности получить доступ ко всем непубличным члена класса. Сегодня рассмотрим друзей-функций.
struct Foo {
friend void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}
private:
int a = 42;
};Завести друга очень просто(по крайней мере в С++). Вам нужно объявить или определить функцию внутри скоупа класса и пометить ее ключевым словом friend. Тогда класс начинает доверять вашей функции настолько, что раскрывает ей все свои секретики.
Можно также просто объявить функцию внутри класса, а определить ее вне:
struct Foo {
friend void foo(Foo& foo);
private:
int a = 42;
};
void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}Разница в том, что в первом варианте функция ищется только с помощью ADL по аргументу, а во втором - по скоупу, в котором она определена. Но это уже для гиков.
В чем особенность френдов? Их недолюбливают, но иногда без них никуда.
С одной стороны, вы даете другой сущности доступ к приватным полям класса. И сразу возникает вопрос: а зачем? Почему нельзя обойтись публичным интерфейсом?
Но иногда действительно нельзя. И хрестоматийным примером здесь будет перегрузка операторов для симметричных операций.
Например, у вас есть класс для работы с комплексными числами и вы хотите определить для этого класса операцию умножения на скаляр:
class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// It works only for complex * double
Complex operator*(double scalar) const {
return Complex(real * scalar, imag * scalar);
}
// This is for double * complex
friend Complex operator*(double scalar, const Complex& c);
};
Complex operator*(double scalar, const Complex& c) {
return Complex(c.real * scalar, c.imag * scalar);
}Честный метод-operator* будет работать только в том случае, когда объект комплексного числа стоит слева. А за умножение
double * complex отвечает глобальный оператор*. И конкретную его перегрузку нужно сделать другом, чтобы он имел доступ к приватным полям.Конкретно для этого примера есть вопросики по тому, зачем делать приватными поля комплексного числа, но тут важна суть работы с friend.
Если ваш класс работает со стандартными бинарными операторами(сравнения, арифметика) и у него есть непубличные поля, то вам может понадобиться определить эти стандартные глобальные операторы друзьями. Вот ссылочка на пример из библиотеки Eigen. Тот же самый принцип работает, например, с оператором вывода в поток:
class Complex {
// ...
friend std::ostream& operator<<(std::ostream& os, const Complex& obj);
};
std::ostream& operator<<(std::ostream& os, const Complex& obj) {
os << "Real: " << obj.real << ", Imaginary: " << imag;
return os;
}Тоже пример дружественной перегрузки вывода из Eigen.
Страуструп кстати дает один интересный иллюстрационный пример: в рэнджах момент окончания последовательности задается в более общем виде, чем итератор - sentinel(ограничитель). Это по сути объект любого типа, главное чтобы итератор мог уметь сравниться с ним. И вот для сравнения итератора и ограничителя может понадобиться определить дружественные операторы == и !=.
За пределами же кейсов с операторами использование функций-друзей должно подвергаться строгим проверкам на предмет попыток обойти кривой дизайн.
Value your friends. Stay cool.
#cppcore
❤22🔥8👍6😁2
Доступ к приватным членам. Други-классы.
#новичкам
Ключевое слово friend может использоваться также, чтобы дать доступ ко всем непубличным членам одного класса другому классу. Ну или точнее - сразу всем его методам.
Внутри класса Matrix делаем объявление класса MatrixAnalyzer и говорим, что он нам друг. Тогда в реализации MatrixAnalyzer мы сможем пользоваться приватными членами Matrix.
С друзьями классами ситуация еще серьезнее. Вы даете доступ к непубличным членам буквально каждому методу другого класса. И все эти методы могут вертеть все поля объекта на всех потенциальных вертелах. Это реально бывает где-то нужно?
Для любой гаечки в С++ найдется свое применение. И для другов-классов тоже:
1️⃣ У вас есть класс, который содержит в своем публичном скоупе еще один класс. Но у этого вложенного класса есть какие-то приватные члены, к которым хотелось бы получить доступ.
Например, есть класс контейнера-связного списка. В него вложен публичный класс итератора и приватный класс ноды.
Мы не хотим, чтобы какой-то левый код мог создавать итератор, поэтому пометили его конструктор приватным. Но LinkedList-то должен уметь создавать итератор. Для этого мы и помечает LinkedList другом итератора, чтобы разрешить ему вызывать приватный конструктор.
2️⃣ Юнит-тестирование. Обычно конечно тестируют только публичный интерфейс класса и стараются покрыть все пути исполнения и кейсы использования, чтобы абсолютно вся логика, включая приватные методы, была протестирована. Это называется black-box тестирование.
Но иногда очень хочется отдельно протестировать конкретные приватные методы и проверить значения приватных полей. Если ваши приватные методы реализуют какие-то логически полные части общего алгоритма, то удобно явно в коде их и тестировать, чтобы ни один баг не проскочил. Это называется white-box тестирование.
И вот для белого ящика нужно пометить класс тестов другом тестируемого класса.
А какой вид тестирования предпочитаете вы? Почему?
Во время подготовки к этому посту нашел еще один интересный кейс применения, но он заслуживает отдельного поста.
Have friends. Stay cool.
#cppcore
#новичкам
Ключевое слово friend может использоваться также, чтобы дать доступ ко всем непубличным членам одного класса другому классу. Ну или точнее - сразу всем его методам.
class Matrix {
private:
int data[10][10];
public:
friend class MatrixAnalyzer;
};
class MatrixAnalyzer {
public:
int sum(const Matrix& m) {
int sum = 0;
for (int i = 0; i < 10; i++)
for (int j = 0; j < 10; j++)
sum += m.data[i][j]; // Access private data
return sum;
}
};Внутри класса Matrix делаем объявление класса MatrixAnalyzer и говорим, что он нам друг. Тогда в реализации MatrixAnalyzer мы сможем пользоваться приватными членами Matrix.
С друзьями классами ситуация еще серьезнее. Вы даете доступ к непубличным членам буквально каждому методу другого класса. И все эти методы могут вертеть все поля объекта на всех потенциальных вертелах. Это реально бывает где-то нужно?
Для любой гаечки в С++ найдется свое применение. И для другов-классов тоже:
1️⃣ У вас есть класс, который содержит в своем публичном скоупе еще один класс. Но у этого вложенного класса есть какие-то приватные члены, к которым хотелось бы получить доступ.
Например, есть класс контейнера-связного списка. В него вложен публичный класс итератора и приватный класс ноды.
template <typename T>
class LinkedList {
private:
struct Node {
T data;
Node *next;
public:
Node(const T &value, Node *n = nullptr) : data(value), next(n) {}
};
Node *head = nullptr;
public:
class Iterator {
private:
Node *current;
Iterator(Node *node) : current(node) {}
friend class LinkedList<T>;
public:
// ...
};
// ...
};
Мы не хотим, чтобы какой-то левый код мог создавать итератор, поэтому пометили его конструктор приватным. Но LinkedList-то должен уметь создавать итератор. Для этого мы и помечает LinkedList другом итератора, чтобы разрешить ему вызывать приватный конструктор.
2️⃣ Юнит-тестирование. Обычно конечно тестируют только публичный интерфейс класса и стараются покрыть все пути исполнения и кейсы использования, чтобы абсолютно вся логика, включая приватные методы, была протестирована. Это называется black-box тестирование.
Но иногда очень хочется отдельно протестировать конкретные приватные методы и проверить значения приватных полей. Если ваши приватные методы реализуют какие-то логически полные части общего алгоритма, то удобно явно в коде их и тестировать, чтобы ни один баг не проскочил. Это называется white-box тестирование.
И вот для белого ящика нужно пометить класс тестов другом тестируемого класса.
class Database {
private:
bool validateConnectionString(const std::string& connStr) {
// Very complicated logic
return true;
}
friend class DatabaseTest; // HERE
};
class DatabaseTest {
public:
static bool testValidation() {
Database db;
return db.validateConnectionString("valid string");
}
};А какой вид тестирования предпочитаете вы? Почему?
Во время подготовки к этому посту нашел еще один интересный кейс применения, но он заслуживает отдельного поста.
Have friends. Stay cool.
#cppcore
❤23😁13👍9🔥3
Доступ к приватным членам. Sutter hack
#опытным
Спасибо @d7d1cd за идею для поста)
В любой системе есть дырки, которые могут(и обязательно будут) эксплуатировать заинтересованные люди. Вот и в С++ так же. Сегодня мы раскроем, как можно стандартными относительно неинвазивными(не изменяя первоначальный код класса) инструментами изменять приватные поля класса.
Возьмем простой класс:
Чтобы трюк сработал, в классе должен быть шаблонный метод.
Теперь следите за руками.
Стандарт говорит, что вы самые хамские-хамы, если пытаетесь получить доступ к непубличным членам и будете за это жестко наказаны. Они должны быть использованы только внутри методов класса.
Дак, мы и не против. Давайте просто впишем новый метод класса, где изменим приватное поле, как нам нужно. И для этого даже не нужно менять код класса. И ключ ко всему - шаблонный метод.
Мы можем вне класса специлизировать шаблон метода для работы с конкретным типом. Специализация шаблона метода - это такой же метод с такими же правами, он может получать доступ к непубличным полям.
И тогда класс будет себя вести именно так, как мы ему скажем. А скажем мы ему пару ласковых:
В специализированном методе мы изменяем приватное поле и для наглядности выводим значение приватного поля в консоль. Можете сами убедиться, что это работает.
Этот трюк был описан Гербом Саттером, поэтому и называется Sutter hack.
Однако с его помощью нельзя менять поведение стандартных объектов:
потому что явные специализации методов из STL приводят к ub.
В общем, интересно, как на стыке двух концепций - ООП и шаблонов - появляются такие интересные спецэффекты)
Hack the life. Stay cool.
#cppcore #template #fun
#опытным
Спасибо @d7d1cd за идею для поста)
В любой системе есть дырки, которые могут(и обязательно будут) эксплуатировать заинтересованные люди. Вот и в С++ так же. Сегодня мы раскроем, как можно стандартными относительно неинвазивными(не изменяя первоначальный код класса) инструментами изменять приватные поля класса.
Возьмем простой класс:
class X {
public:
X() : private_(1) {}
template <class T>
void f(const T &t) {}
int Value() { return private_; }
private:
int private_;
};Чтобы трюк сработал, в классе должен быть шаблонный метод.
Теперь следите за руками.
Стандарт говорит, что вы самые хамские-хамы, если пытаетесь получить доступ к непубличным членам и будете за это жестко наказаны. Они должны быть использованы только внутри методов класса.
Дак, мы и не против. Давайте просто впишем новый метод класса, где изменим приватное поле, как нам нужно. И для этого даже не нужно менять код класса. И ключ ко всему - шаблонный метод.
Мы можем вне класса специлизировать шаблон метода для работы с конкретным типом. Специализация шаблона метода - это такой же метод с такими же правами, он может получать доступ к непубличным полям.
И тогда класс будет себя вести именно так, как мы ему скажем. А скажем мы ему пару ласковых:
struct Y {};
template <>
void X::f(const Y &) {
private_ = 2;
}
int main() {
X x;
std::cout << x.Value() << std::endl; // prints 1
x.f(Y());
std::cout << x.Value() << std::endl; // prints 2
}В специализированном методе мы изменяем приватное поле и для наглядности выводим значение приватного поля в консоль. Можете сами убедиться, что это работает.
Этот трюк был описан Гербом Саттером, поэтому и называется Sutter hack.
Однако с его помощью нельзя менять поведение стандартных объектов:
The behavior of a C++ program is undefined if it declares
- an explicit specialization of any member function of a standard library class template, or
- an explicit specialization of any member function template of a standard library class or class template, or
- an explicit or partial specialization of any member class template of a standard library class or class template, or
- a deduction guide for any standard library class template.
потому что явные специализации методов из STL приводят к ub.
В общем, интересно, как на стыке двух концепций - ООП и шаблонов - появляются такие интересные спецэффекты)
Hack the life. Stay cool.
#cppcore #template #fun
👍28🔥12❤🔥5🤯4❤3
Доступ к приватным членам. Явная инстанциация
#опытным
В прошлый раз мы уже выяснили, что явно инстанцируя шаблонный метод класса, можно написать свою реализацию, которая будет жонглировать непубличными членами в самых виртуозных позах.
Но!
Мы так и не вышли за пределы класса. Ручная специализация шаблонного метода - это такой же метод класса, поэтому он и умеет трогать приватные поля.
Хочется прям снаружи получить доступ к полю и уже не ограничиваться реализацией метода.
С++ и это может воплотить в реальность.
Хоть стандарт и бьет по рукам за упоминание имен непубличных членов за пределами скоупа класса, все-таки есть исключения из правил:
[temp.spec.partial.general]/10
The usual access checking rules do not apply to non-dependent names used to specify template arguments of the simple-template-id of the partial specialization.
[temp.spec.general]/6
The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization
Если по-человечески, то проверка доступа к имени не проверяется при явной специализации и инстанциации шаблона. То есть:
Этот код вполне влиден. Здесь мы используем указатель на поле класса
Однако, даже если вы явно инстанцируете шаблон, содержащий приватные типы, вы не сможете создать такой объект напрямую.
Выход заключается в том, чтобы сохранить значение указателя в статическом члене и передать его в другой класс.
Когда вы явно инстанцируете
Это работает!
То есть мы просто взяли и вывели на консоль значение приватного члена класса, при этом никак не меняя его код. Можно также легко его изменить. И все это четко согласовано со стандартом.
Еще один раз шаблоны сломали инкапсуляцию. Да что ж это такое!
Спасибо, @SoulslikeEnjoyer, за материалы для поста)
Exploit loopholes. Stay cool.
#cppcore #template #fun
#опытным
В прошлый раз мы уже выяснили, что явно инстанцируя шаблонный метод класса, можно написать свою реализацию, которая будет жонглировать непубличными членами в самых виртуозных позах.
Но!
Мы так и не вышли за пределы класса. Ручная специализация шаблонного метода - это такой же метод класса, поэтому он и умеет трогать приватные поля.
Хочется прям снаружи получить доступ к полю и уже не ограничиваться реализацией метода.
С++ и это может воплотить в реальность.
Хоть стандарт и бьет по рукам за упоминание имен непубличных членов за пределами скоупа класса, все-таки есть исключения из правил:
[temp.spec.partial.general]/10
The usual access checking rules do not apply to non-dependent names used to specify template arguments of the simple-template-id of the partial specialization.
[temp.spec.general]/6
The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization
Если по-человечески, то проверка доступа к имени не проверяется при явной специализации и инстанциации шаблона. То есть:
class Foo {
private:
int data = 42;
};
template <auto V>
struct Bar {};
template struct Bar<&Foo::data>;Этот код вполне влиден. Здесь мы используем указатель на поле класса
Foo::data в качестве NTTP. Это валидно, потому что указатель на поле класса - это по сути смещение от начала объекта и оно известно на момент компиляции.Однако, даже если вы явно инстанцируете шаблон, содержащий приватные типы, вы не сможете создать такой объект напрямую.
Bar<&Foo::data> b;
// error: 'int Foo::data' is private within this context
Выход заключается в том, чтобы сохранить значение указателя в статическом члене и передать его в другой класс.
template <typename PtrType>
struct Storage {
inline static PtrType ptr;
};
template <auto V>
struct PtrTaker {
struct Transferer {
Transferer() {
Storage<decltype(V)>::ptr = V;
}
};
inline static Transferer tr;
};
template struct PtrTaker<&Foo::data>;
Когда вы явно инстанцируете
PtrTaker<&Foo::data>, его статический член tr будет инициализирован, и в его конструкторе Storage<PtrType>::ptr получит значение. Теперь вы можете получить доступ к нему через:Foo foo;
std::cout << foo.Storage<int Foo::>::ptr;
Это работает!
То есть мы просто взяли и вывели на консоль значение приватного члена класса, при этом никак не меняя его код. Можно также легко его изменить. И все это четко согласовано со стандартом.
Еще один раз шаблоны сломали инкапсуляцию. Да что ж это такое!
Спасибо, @SoulslikeEnjoyer, за материалы для поста)
Exploit loopholes. Stay cool.
#cppcore #template #fun
❤11👍8🔥6🤯6❤🔥4
Доступ к приватным членам. Явная инстанциация и друзья
#опытным
Оказывается способов легально залезть в непубличные кишки вашего класса довольно много, и сегодня обсудим еще один метод.
Мы уже с вами говорили про дружественные функции и что им дозволено получать доступ к приватным членами класса.
Но давайте посмотрим на следующий пример:
У нас есть шаблонная структура и у нее есть дружественная функция. Пока шаблон не инстанцирован, компилятор не видит определения функции. Поэтому чтобы вызвать
Давайте проследим, что произошло. Функция bar - по сути свободная функция, которая может использовать все члены Foo. Но не только их. Она еще может использовать шаблонные параметры конкретной инстанциации.
И вот тут мы возвращаемся к тому, что использование имени приватного поля абсолютно законно в контексте явной инстанциации(см. предыдущий пост).
Давайте сделаем шаблонный параметр Foo указателем на поле и инстанцируем этот шаблон с указателем на приватное поле класса:
Вот и все, получаем ссылку на приватное поле и крутим его, как хотим. Можно поиграться с примером тут.
И еще один из шаблоны сломали инкапсуляцию. Да что ж это такое!
Спасибо, @SoulslikeEnjoyer, за материалы для поста)
Exploit loopholes. Stay cool.
#cppcore #template #fun
#опытным
Оказывается способов легально залезть в непубличные кишки вашего класса довольно много, и сегодня обсудим еще один метод.
Мы уже с вами говорили про дружественные функции и что им дозволено получать доступ к приватным членами класса.
Но давайте посмотрим на следующий пример:
template <typename T>
struct Foo {
friend void bar() { cout << "Got it!" << endl; }
};
void bar();
template struct Foo<int>;
bar();
У нас есть шаблонная структура и у нее есть дружественная функция. Пока шаблон не инстанцирован, компилятор не видит определения функции. Поэтому чтобы вызвать
bar, нужно явно инстанцировать шаблон и объявить функцию во внешнем скоупе, чтобы компилятор мог найти ее по имени.Давайте проследим, что произошло. Функция bar - по сути свободная функция, которая может использовать все члены Foo. Но не только их. Она еще может использовать шаблонные параметры конкретной инстанциации.
И вот тут мы возвращаемся к тому, что использование имени приватного поля абсолютно законно в контексте явной инстанциации(см. предыдущий пост).
Давайте сделаем шаблонный параметр Foo указателем на поле и инстанцируем этот шаблон с указателем на приватное поле класса:
class Private {
private:
int data{};
};
template<int Private::* Member> // pointer to data member
struct Stealer {
friend int& dataGetter(Private& iObj) {
return iObj.*Member;
}
};
template struct Stealer<&Private::data>; // explicit instantiation
int& dataGetter(Private&);
int main() {
Private obj;
dataGetter(obj) = 42;
}Вот и все, получаем ссылку на приватное поле и крутим его, как хотим. Можно поиграться с примером тут.
И еще один из шаблоны сломали инкапсуляцию. Да что ж это такое!
Спасибо, @SoulslikeEnjoyer, за материалы для поста)
Exploit loopholes. Stay cool.
#cppcore #template #fun
❤17🔥15❤🔥6👍3😱1
Бросаем дичь
#новичкам
В С++ есть исключения. Вы можете их любить или ненавидеть, но от этого не сбежать(почти).
Обычно как происходит. Есть стандартный std::exception или любой кастомный базовый класс исключения my_exception::BaseException. У них куча наследников и вот вы их бросаете в подходящих ситуациях.
Но это же С++: "Вы думали, что бросать можно только исключения? Пфф. Не смешите мои подковы и подержите мое пиво."
Бросать можно почти все, что угодно, что можно считать объектом.
Например так:
Бросаем число. А что, какие-то проблемы?
Или вот так:
Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.
Самое уморительное, что можно кинуть даже лямбду. Ведь это всего лишь объект замыкания, ничего более:
Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.
Так что при споре с коллегами вы теперь можете бросаться в них всеми предметами, от стула докакашеклямбды. Благо знаете как.
Be amazed. Stay cool.
#cppcore
#новичкам
В С++ есть исключения. Вы можете их любить или ненавидеть, но от этого не сбежать(почти).
Обычно как происходит. Есть стандартный std::exception или любой кастомный базовый класс исключения my_exception::BaseException. У них куча наследников и вот вы их бросаете в подходящих ситуациях.
Но это же С++: "Вы думали, что бросать можно только исключения? Пфф. Не смешите мои подковы и подержите мое пиво."
Бросать можно почти все, что угодно, что можно считать объектом.
Например так:
throw 1;
Бросаем число. А что, какие-то проблемы?
Или вот так:
throw nullptr;
throw "This is the end!";
void panic() { std::cout << "PANIC!" << std::endl; }
throw static_cast<void(*)()>(panic); // Указатель на функцию
Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.
Самое уморительное, что можно кинуть даже лямбду. Ведь это всего лишь объект замыкания, ничего более:
throw []{std::cout << "Things are going really bad...\n"; };Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.
Так что при споре с коллегами вы теперь можете бросаться в них всеми предметами, от стула до
Be amazed. Stay cool.
#cppcore
🔥39❤13😁8👍6🤯1
switch
#новичкам
switch - это такая базовая штука, которая изучается в самом начале вместе с другими core-конструкциями языка типа условий и циклов. Правда после изучения она становится той книжкой на полке, которую редко трогают и ее достают скорее, чтобы протереть. Поэтому некоторые особенности switch забываются, а в голове новичков зачастую нет понимания, когда и как его все-таки использовать.
Сегодня проясним особенности, а в следующий раз поговорим о кейсах применимости.
Но для начала пойдем с начала. switch - это чуть более компактное и продвинутое условие. Скажем, у вас есть переменная типа int и в зависимости от ее значений, вы по разному обрабатываете данные. Пусть у вас есть http статус код и вы на его основе строите какую-то логику:
То есть 1 переменная - много вариантов развития событий.
Теперь погнали по особенностям:
👉🏿 Вы будете проваливаться в нижележащие кейсы, если не напишите break в конце текущего кейса. Это накладывает свои когнитивные сложности, но дает и возможности. Если у вас одинаково обрабатываются разные значения, то вы можете проваливаться в нижележащие кейсы, пока не дойдете до нужного обработчика и не брякнитесь(встретите break):
👉🏿 Не очень удобно в switch работать с диапазонами значений. Скорее всего выразительнее и безопаснее воспользоваться обычными условиями, если у вас обработчики соответствуют длинным диапазонам.
Существуют расширения компилятора, которые позволяют указывать в switch диапазон:
Но это непереносимо и вообще не для этого розочка цвела. switch хорошо оптимизируется через jump table(вместо кучи if-else с линейной сложностью используется массив меток обработчиков, в котором за О(1) ищется нужный), а диапазоны значений в эту концепцию не вписываются.
👉🏿 В С++ очень органично вписаны объекты и пользовательские типы, наряду с тривиальными С-совместимыми типами. Поэтому может сложиться впечатление, что switch может, например, со строками работать. Но нет, он работает только с целочисленными типами, то есть с целыми числами и перечислениями(scoped и unscoped).
Если хотите выбирать обработчики на основе пользовательских типов, нужно использовать ассоциативные контейнеры:
👉🏿 Значения кейсов должны быть константными выражениями. То есть значениями, известными на этапе компиляции. Вы не можете выбрать обработчик, соотвествующий runtime значению.
При использовании enum'ов такой проблемы в принципе не возникает, но вот с int'ами да:
Вроде простая конструкция, но все равно есть нюансы.
Explore nuances. Stay cool.
#cppcore
#новичкам
switch - это такая базовая штука, которая изучается в самом начале вместе с другими core-конструкциями языка типа условий и циклов. Правда после изучения она становится той книжкой на полке, которую редко трогают и ее достают скорее, чтобы протереть. Поэтому некоторые особенности switch забываются, а в голове новичков зачастую нет понимания, когда и как его все-таки использовать.
Сегодня проясним особенности, а в следующий раз поговорим о кейсах применимости.
Но для начала пойдем с начала. switch - это чуть более компактное и продвинутое условие. Скажем, у вас есть переменная типа int и в зависимости от ее значений, вы по разному обрабатываете данные. Пусть у вас есть http статус код и вы на его основе строите какую-то логику:
switch(status_code) {
case 200: // OK
processSuccess();
break;
// ...
case 400: // Bad Request
logClientError(status_code);
break;
// ...
case 500: // Service Unavailable
logServerError(status_code);
break;
// ...
default:
handleUnknownStatus(status_code);
break;
}То есть 1 переменная - много вариантов развития событий.
Теперь погнали по особенностям:
👉🏿 Вы будете проваливаться в нижележащие кейсы, если не напишите break в конце текущего кейса. Это накладывает свои когнитивные сложности, но дает и возможности. Если у вас одинаково обрабатываются разные значения, то вы можете проваливаться в нижележащие кейсы, пока не дойдете до нужного обработчика и не брякнитесь(встретите break):
switch(status_code) {
case 200: // OK
processSuccess();
break;
case 400: // Bad Request
case 401: // Unauthorized
case 403: // Forbidden
case 404: // Not Found
logClientError(status_code);
break;
}👉🏿 Не очень удобно в switch работать с диапазонами значений. Скорее всего выразительнее и безопаснее воспользоваться обычными условиями, если у вас обработчики соответствуют длинным диапазонам.
Существуют расширения компилятора, которые позволяют указывать в switch диапазон:
switch(x) {
case 0 ... 9: // GNU Extension
std::cout << "0-9\n";
break;
case 10 ... 19: // GNU Extension
std::cout << "10-19\n";
break;
}Но это непереносимо и вообще не для этого розочка цвела. switch хорошо оптимизируется через jump table(вместо кучи if-else с линейной сложностью используется массив меток обработчиков, в котором за О(1) ищется нужный), а диапазоны значений в эту концепцию не вписываются.
👉🏿 В С++ очень органично вписаны объекты и пользовательские типы, наряду с тривиальными С-совместимыми типами. Поэтому может сложиться впечатление, что switch может, например, со строками работать. Но нет, он работает только с целочисленными типами, то есть с целыми числами и перечислениями(scoped и unscoped).
Если хотите выбирать обработчики на основе пользовательских типов, нужно использовать ассоциативные контейнеры:
using Handler = std::function<void()>;
std::unordered_map<std::string, Handler> handlers = {
{"start", &start},
{"stop", []{ stop(); }}
};
void execute(const std::string& cmd) {
if (auto it = handlers.find(cmd); it != handlers.end()) {
it->second();
}
}
👉🏿 Значения кейсов должны быть константными выражениями. То есть значениями, известными на этапе компиляции. Вы не можете выбрать обработчик, соотвествующий runtime значению.
При использовании enum'ов такой проблемы в принципе не возникает, но вот с int'ами да:
constexpr int getValue() {
return 10;
}
int main() {
constexpr int y = getValue();
int z = getValue();
int x = 10;
switch (x) {
case y: // OK: y - constant expression
std::cout << "x equals y\n";
break;
case z: // ERROR: z is runtime value
std::cout << "x equals z\n";
break;
default:
std::cout << "default\n";
}
return 0;
}Вроде простая конструкция, но все равно есть нюансы.
Explore nuances. Stay cool.
#cppcore
❤20👍11🔥7⚡2
Как применять switch?
#новичкам
Мы вспомнили про switch и некоторые его особенности. Теперь посмотрим, в каких сценариях их адекватно применять и как это делать.
Для начала повторим ограничения. switch может работать только с целочисленными значениями и перечислениями. В основном, конечно, с перечислениями как с именованными числами. В коде не должно быть магических чисел, каждая ветка должна иметь наглядную семантику, а перечисления помогают "называть" числа без неконтролируемого размножения локальных или статических переменных.
В прошлом посте мы свичались по http статус кодам. Тогда они были просто числами, так было проще вспомнить механику этой инструкции. Давайте перепишем в нормальный вид:
Теперь магических чисел нет.
Плюс к этому все кейсы должны быть известны на этапе компиляции. Динамически менять ничего не получится.
Эти особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).
Дальше идем.
Чем вообще может быть плох switch?
👉🏿 Его использование смешивает логику выбора и обработки. Это может быть нормой при небольшом количестве веток. Все находится рядышком, не нужно прыгать по коду, чтобы понять полную картину.
Но это совершенно точно становится проблемой, когда количество кейсов больше 10-20. Свитч целиком перестает помещаться на экран и вместо того, чтобы при чтении кода понимать логику работы кода, вы разбираетесь в том, какой обработчик для каждого кейса вызывается.
👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.
👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.
Из этих проблем можно сделать несколько выводов:
1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.
2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.
3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.
Когда не надо применять switch?
🔞 Пользовательский тип под условием.
🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)
🔞 Если нужен какой-то дикий полиморфизм, когда обработчик полиморфно выбирается на основе значения кейса.
В этих случаях обычно используют статическую std::unordered_map, сопоставляя значения кейсов их обработчикам.
Use the right tool. Stay cool.
#cppcore #goodpractice #design
#новичкам
Мы вспомнили про switch и некоторые его особенности. Теперь посмотрим, в каких сценариях их адекватно применять и как это делать.
Для начала повторим ограничения. switch может работать только с целочисленными значениями и перечислениями. В основном, конечно, с перечислениями как с именованными числами. В коде не должно быть магических чисел, каждая ветка должна иметь наглядную семантику, а перечисления помогают "называть" числа без неконтролируемого размножения локальных или статических переменных.
В прошлом посте мы свичались по http статус кодам. Тогда они были просто числами, так было проще вспомнить механику этой инструкции. Давайте перепишем в нормальный вид:
switch(status_code) {
case HTTPStatus::OK:
processSuccess();
break;
case HTTPStatus::BadRequest:
case HTTPStatus::Unauthorized:
case HTTPStatus::Forbidden:
case HTTPStatus::NotFound:
logClientError(status_code);
break;
case HTTPStatus::InternalServerError:
case HTTPStatus::BadGateway:
case HTTPStatus::ServiceUnavailable:
return logServerError(status_code);
default:
// ...
}Теперь магических чисел нет.
Плюс к этому все кейсы должны быть известны на этапе компиляции. Динамически менять ничего не получится.
Эти особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).
Дальше идем.
Чем вообще может быть плох switch?
👉🏿 Его использование смешивает логику выбора и обработки. Это может быть нормой при небольшом количестве веток. Все находится рядышком, не нужно прыгать по коду, чтобы понять полную картину.
Но это совершенно точно становится проблемой, когда количество кейсов больше 10-20. Свитч целиком перестает помещаться на экран и вместо того, чтобы при чтении кода понимать логику работы кода, вы разбираетесь в том, какой обработчик для каждого кейса вызывается.
👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.
👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.
Из этих проблем можно сделать несколько выводов:
1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.
2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.
3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.
void dispatch(Editor &editor, Command cmd) {
switch (cmd.type) {
case Command::NewFile:
executeNewFile(editor);
return;
case Command::OpenFile:
executeOpenFile(editor);
return;
// ...
}
}
void processUserInput(Editor &editor, UserInput input) {
if (auto cmd = interpretInput(input)) {
dispatch(editor, *cmd);
}
}Когда не надо применять switch?
🔞 Пользовательский тип под условием.
🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)
🔞 Если нужен какой-то дикий полиморфизм, когда обработчик полиморфно выбирается на основе значения кейса.
В этих случаях обычно используют статическую std::unordered_map, сопоставляя значения кейсов их обработчикам.
Use the right tool. Stay cool.
#cppcore #goodpractice #design
❤18👍10🔥5🤣5😁1
Выравнивание. А зачем?
#новичкам
Представим, что мы пишем что-то связанное с графикой. У нас есть плоскость черно-белых пикселей. Каждый пиксель определяется через цвет черно-белого тона и координаты на плоскости:
Мы хотим сделать супер-мега-блезингово-быстрое приложение, которое при работе будет тратить всего 1 бит памяти. Ну или сколько получится, но как можно меньше. Поэтому мы паримся, как о перфомансе, так и о эффективности по памяти.
Если паримся, значит надо считать.
Посчитаем, сколько байт занимает структура Pixel. Это по сути
Теория - это прекрасно, но надо все проверять:
И получаем..... 12 байт.
/+ 33% от расчетов. Вообще неоптимально получается. Но почему расчеты не сходятся с реальностью?
Дело вот в чём. Процессор читает данные из памяти не хаотично, а определёнными порциями, например, по 4, 8 байта за раз. Причём эти порции он читает только с определённых адресов. Например, 4-байтовое число int обычно нужно читать с адреса, который делится на 4. Чтобы понять, почему так, нужно лезть в устройство процессора и организации работы с памятью. Не будем так глубоко погружаться.
Просто представим аналогию, что процессор, как автобус, может забирать данные определенного размера только с определенных адресов, как людей с остановок.
Иногда на между остановок вообще нельзя останавливаться. Некоторые процессоры этого не позволяют. Иногда можно. Например, можно прочитать int с адреса, который на 3 делится. Но тогда нужно читать 2 соседние порции и из их кусочков составлять нужное число. А это лишние операции и накладные расходы.
То есть высокая скорость обработки данных достигается в случае, когда адреса выровнены по нужной границе.
Теперь вернемся к Pixel. Представим, что мы создаем объект структуры:
Исходя из рассуждений выше, логично предположить, что начало
Но компилятор у нас - не тупая машина, питающаяся текстом программы. Он умный и умеет оптимизировать. Если для скорости работы программы нужно, что адреса
Ну ладно, мы еще повоюем за память. Попробуем перетасовать поля:
Теперь-то будет 9 байт размер?!
Да нет. Все те же 12 байт.
Представьте, что вы сделали массив пикселей. Да, у первого элемента
Кстати, эти дополнительные куски памяти, которые компилятор добавляет для обеспечения выравнивания, называются паддингами(padding). В них не хранятся данные, они нужны чисто для выравнивания.
Сегодня мы рассмотрели, фактически, только предназначение выравнивания. В следующих постах будем раскрывать подробности.
Align yourself. Stay cool.
#cppcore #optimization #compiler
#новичкам
Представим, что мы пишем что-то связанное с графикой. У нас есть плоскость черно-белых пикселей. Каждый пиксель определяется через цвет черно-белого тона и координаты на плоскости:
struct Pixel {
unsigned char grayscale;
int x, y;
};Мы хотим сделать супер-мега-блезингово-быстрое приложение, которое при работе будет тратить всего 1 бит памяти. Ну или сколько получится, но как можно меньше. Поэтому мы паримся, как о перфомансе, так и о эффективности по памяти.
Если паримся, значит надо считать.
Посчитаем, сколько байт занимает структура Pixel. Это по сути
sizeof(unsigned char) + 2 * sizeof(int). размер char'a 1 байт, размер int на современных десктопах 4 байта. С помощью применения дифференциального исчисления в пространстве Минковского получаем 9 байт.Теория - это прекрасно, но надо все проверять:
std::cout << sizeof(Pixel) << std::endl;
И получаем..... 12 байт.
/+ 33% от расчетов. Вообще неоптимально получается. Но почему расчеты не сходятся с реальностью?
Дело вот в чём. Процессор читает данные из памяти не хаотично, а определёнными порциями, например, по 4, 8 байта за раз. Причём эти порции он читает только с определённых адресов. Например, 4-байтовое число int обычно нужно читать с адреса, который делится на 4. Чтобы понять, почему так, нужно лезть в устройство процессора и организации работы с памятью. Не будем так глубоко погружаться.
Просто представим аналогию, что процессор, как автобус, может забирать данные определенного размера только с определенных адресов, как людей с остановок.
Иногда на между остановок вообще нельзя останавливаться. Некоторые процессоры этого не позволяют. Иногда можно. Например, можно прочитать int с адреса, который на 3 делится. Но тогда нужно читать 2 соседние порции и из их кусочков составлять нужное число. А это лишние операции и накладные расходы.
То есть высокая скорость обработки данных достигается в случае, когда адреса выровнены по нужной границе.
Теперь вернемся к Pixel. Представим, что мы создаем объект структуры:
Pixel p{255, 1, 1};Исходя из рассуждений выше, логично предположить, что начало
p будет лежать по уже выравненному адресу для более быстрого чтения. Допустим, он делится на 4. В начале структуры лежит 1 байт цвета. А после лежат 2 int'а по 4 байта, доступ к которым должен быть выровнен. Но если к числу, кратному 4-м, прибавить 1, то оно перестанет делиться на 4. То есть, если данные лежат прям последовательно, то доступ будет невыровненный, а значит медленный.Но компилятор у нас - не тупая машина, питающаяся текстом программы. Он умный и умеет оптимизировать. Если для скорости работы программы нужно, что адреса
x и y должны быть выровнены по четверке, то он сделает магию и выровняет их. Заклинание простое - добавляем 3 байта после поля grayscale и дело в шляпе! Вот поэтому на практике размер Pixel равен 12 байтам.Ну ладно, мы еще повоюем за память. Попробуем перетасовать поля:
struct Pixel {
int x, y;
unsigned char grayscale;
};Теперь-то будет 9 байт размер?!
Да нет. Все те же 12 байт.
Представьте, что вы сделали массив пикселей. Да, у первого элемента
x и y выровнены по 4-ке. Но начиная со второго элемента выравнивание поедет к черту. Так не годится и приходят на помощь все те же 3 байта, только уже в конце структуры.Кстати, эти дополнительные куски памяти, которые компилятор добавляет для обеспечения выравнивания, называются паддингами(padding). В них не хранятся данные, они нужны чисто для выравнивания.
Сегодня мы рассмотрели, фактически, только предназначение выравнивания. В следующих постах будем раскрывать подробности.
Align yourself. Stay cool.
#cppcore #optimization #compiler
❤32👍17🔥7❤🔥3😁3
Выравнивание. Принципы
#новичкам
Сегодня поговорим о выравнивании чуть поближе, чтобы вы в первом приближении могли разобраться, как лежат данные в структурах.
🔍 Однобайтовые данные не нужно выравнивать. Да, процессор читает данные определенными порциями, которые больше 1 байта. Но для любой взятой char переменной, при попытке ее прочитать ее значение будет лежат в самом начале порции. Минимальная адресуемая единица - это 1 байт, поэтому однобайтовые данные неделимы. Не нужно никаких дополнительных плясок.
Для наглядности можно представить себе массив чаров:
Это просто 10 подряд идущих символов. И адреса каждого символа не нужно выравнивать. Поэтому и количество байт, которое занимает этот массив, равно просто количеству его элементов. Без всяких дополнений.
🔍 Адрес поля тривиального типа должен быть выровнен по размеру этого типа
- для short адреса должны быть кратными 2
- для int адреса должны быть кратными 4
- для size_t адреса должны быть кратными 8
- для float адреса должны быть кратными 4
- для double адреса должны быть кратными 8
- для указателей адреса должны быть кратными 8
Если сделать меньше, то придется читать несколькими порциями и вычислять из них нужное значение, что долго.
Если сделать больше, то будет понапрасну тратится память.
Все это актуально для современных 64-битных десктопов.
🔍 Адреса полей кастомных классов должны быть выровнены по длине самого жирного тривиального типа, входящего в состав класса. Не по размеру самого объекта, а по самому жирному его полю.
Например:
У нас есть составная структура А, размер которой равен 2 байтам. Структура B содержит А в качестве поля. Так как в А только char поля, для доступа к ним не нужны паддинги. Поэтому размер В равен 3-м.
Но если будет вот так:
Мало того, что паддинг размером 7 добавися перед полем
🔍 Паддинг добавляется в основном перед тем полем, адрес которого должен быть выровнен. Мы это уже увидели на практике.
В реальности это также значит, что паддинг может добавляться в конце, чтобы обеспечить корректное выравнивание следующих данных.
Допустим, у нас есть структура и мы хотим из нее сделать массив:
Паддинг в 7 байт нужно добавить для того, чтобы адреса поля
Не нужно добавлять паддинги после каждого поля, чтобы для всех них было одинаковое выравнивание. Выровненным по определенной границе должен быть лишь адрес данного конкретного поля. Паддинги добавляются независимо.
🔍 Чтобы оптимально использовать память, лучше располагать поля в порядке убывания их требований к выравниванию. Так вы предыдущим полем гарантируете нужное выравнивание следующему. Сравните:
Реальный размер данных - 15 байт. Для double поля в любом случае нужно выравнивание по 8-ке. Но правильно расположив данные, мы экономим 8 байт.
Тема выравнивания довольно большая и мы прошлись только по верхам. Но даже эти простые принципы помогут вам правильно оценивать размер структур и организовывать их поля оптимальным образом.
Align yourself. Stay cool.
#cppcore #optimization #compiler
#новичкам
Сегодня поговорим о выравнивании чуть поближе, чтобы вы в первом приближении могли разобраться, как лежат данные в структурах.
🔍 Однобайтовые данные не нужно выравнивать. Да, процессор читает данные определенными порциями, которые больше 1 байта. Но для любой взятой char переменной, при попытке ее прочитать ее значение будет лежат в самом начале порции. Минимальная адресуемая единица - это 1 байт, поэтому однобайтовые данные неделимы. Не нужно никаких дополнительных плясок.
Для наглядности можно представить себе массив чаров:
char array[10];
std::cout << sizeof(array) << std::endl;
// OUTPUT: 10
Это просто 10 подряд идущих символов. И адреса каждого символа не нужно выравнивать. Поэтому и количество байт, которое занимает этот массив, равно просто количеству его элементов. Без всяких дополнений.
🔍 Адрес поля тривиального типа должен быть выровнен по размеру этого типа
- для short адреса должны быть кратными 2
- для int адреса должны быть кратными 4
- для size_t адреса должны быть кратными 8
- для float адреса должны быть кратными 4
- для double адреса должны быть кратными 8
- для указателей адреса должны быть кратными 8
Если сделать меньше, то придется читать несколькими порциями и вычислять из них нужное значение, что долго.
Если сделать больше, то будет понапрасну тратится память.
Все это актуально для современных 64-битных десктопов.
🔍 Адреса полей кастомных классов должны быть выровнены по длине самого жирного тривиального типа, входящего в состав класса. Не по размеру самого объекта, а по самому жирному его полю.
Например:
struct A {
char a;
char b;
};
struct B {
char first;
A second;
};
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 2
// 3У нас есть составная структура А, размер которой равен 2 байтам. Структура B содержит А в качестве поля. Так как в А только char поля, для доступа к ним не нужны паддинги. Поэтому размер В равен 3-м.
Но если будет вот так:
struct A {
char a;
double b;
};
struct B {
char first;
A second;
};
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 16
// 24Мало того, что паддинг размером 7 добавися перед полем
b структуры А, так он же еще добавится перед `second` в В. И в B он такого размера именно потому, чтобы доступ до b в итоге всех размещений был выровнен.🔍 Паддинг добавляется в основном перед тем полем, адрес которого должен быть выровнен. Мы это уже увидели на практике.
В реальности это также значит, что паддинг может добавляться в конце, чтобы обеспечить корректное выравнивание следующих данных.
Допустим, у нас есть структура и мы хотим из нее сделать массив:
struct A {
double a;
char b;
};
A array[10];Паддинг в 7 байт нужно добавить для того, чтобы адреса поля
a из всех элементов, начиная со второго, тоже были выровнены по границе 8.Не нужно добавлять паддинги после каждого поля, чтобы для всех них было одинаковое выравнивание. Выровненным по определенной границе должен быть лишь адрес данного конкретного поля. Паддинги добавляются независимо.
🔍 Чтобы оптимально использовать память, лучше располагать поля в порядке убывания их требований к выравниванию. Так вы предыдущим полем гарантируете нужное выравнивание следующему. Сравните:
struct A {
char a;
// 7 byte padding
double d;
short b;
// 2 byte padding
int c;
};
struct B {
double d;
int c;
short b;
char a;
// 1 byte padding
};
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 24
// 16Реальный размер данных - 15 байт. Для double поля в любом случае нужно выравнивание по 8-ке. Но правильно расположив данные, мы экономим 8 байт.
Тема выравнивания довольно большая и мы прошлись только по верхам. Но даже эти простые принципы помогут вам правильно оценивать размер структур и организовывать их поля оптимальным образом.
Align yourself. Stay cool.
#cppcore #optimization #compiler
🔥24❤12👍10