CRTP + friend
#опытным
Допустим, вам нужно написать tcp и udp версии клиента для одной и той же задачи. Можно использовать динамический полиморфизм и переопределить все специфические методы. Но тогда у нас будет оверхед на динамическую диспетчеризацию вызова.
Зачем тратить драгоценные миллинаносекунды, если можно воспользоваться compile-time полиморфизмом, а конкретно паттерном CRTP?
Вот сильно упрощенная и обрезанная версия того, как это может выглядеть:
И меня всегда напрягало, что в любой статье вот эти *_impl методы находятся в публичном интерфейсе наследников. Зачем они там - непонятно и это по сути детали реализации. Надо бы их скрыть.
Но как? База CRTP должна иметь доступ к этим методам.
Вот тут-то мы и используем трюк. Сделаем все члены наследников приватными и дадим базе CRTP доступ к кишкам наследников через friend:
И все. Теперь все члены приватные, ClientBase имеет ко всем из них доступ и публичный интерфейс задает только ClientBase, ничего лишнего нет.
Не так много народу знает об этой технике, поэтому решил о ней отдельно рассказать.
Hide your secrets. Stay cool.
#template
#опытным
Допустим, вам нужно написать tcp и udp версии клиента для одной и той же задачи. Можно использовать динамический полиморфизм и переопределить все специфические методы. Но тогда у нас будет оверхед на динамическую диспетчеризацию вызова.
Зачем тратить драгоценные миллинаносекунды, если можно воспользоваться compile-time полиморфизмом, а конкретно паттерном CRTP?
Вот сильно упрощенная и обрезанная версия того, как это может выглядеть:
template <typename Derived>
class ClientBase {
protected:
int sock_ = -1;
sockaddr_in serv_addr_{};
std::string server_ip_;
uint16_t port_;
Derived& derived() { return static_cast<Derived&>(this); }
public:
void send_message(const std::string& message) {
ssize_t bytes_sent = derived().send_impl(message);
if (bytes_sent < 0) {
throw std::runtime_error("Send failed");
}
}
};
class TcpClient : public ClientBase<TcpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_STREAM;
ssize_t send_impl(const std::string& message) {
return send(sock_, message.c_str(), message.length(), 0);
}
};
class UdpClient : public ClientBase<UdpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_DGRAM;
ssize_t send_impl(const std::string& message) {
return sendto(sock_, message.c_str(), message.length(), 0,
reinterpret_cast<sockaddr>(&serv_addr_), sizeof(serv_addr_));
}
};
И меня всегда напрягало, что в любой статье вот эти *_impl методы находятся в публичном интерфейсе наследников. Зачем они там - непонятно и это по сути детали реализации. Надо бы их скрыть.
Но как? База CRTP должна иметь доступ к этим методам.
Вот тут-то мы и используем трюк. Сделаем все члены наследников приватными и дадим базе CRTP доступ к кишкам наследников через friend:
template <typename Derived>
class ClientBase {
// ...
};
class TcpClient : public ClientBase<TcpClient> {
private:
// ...
friend class ClientBase<TcpClient>;
};
class UdpClient : public ClientBase<UdpClient> {
private:
// ...
friend class ClientBase<UdpClient>;
};
И все. Теперь все члены приватные, ClientBase имеет ко всем из них доступ и публичный интерфейс задает только ClientBase, ничего лишнего нет.
Не так много народу знает об этой технике, поэтому решил о ней отдельно рассказать.
Hide your secrets. Stay cool.
#template
❤33👍18🔥14😁6🤯1
Доступ к приватным членам. 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
Квиз
#опытным
В С++ даже очевидный на первый взгляд код может привести к весьма неожиданному исполнению.
Допустим, мы вот хотим создать вектор пар строковых вьюшек и вывести это добро на консоль. Просто? Просто.
Ну раз просто, тогда поучаствуйте в #quiz'е: какой будет результат попытки компиляции и запуска этого кода на С++23?
#опытным
В С++ даже очевидный на первый взгляд код может привести к весьма неожиданному исполнению.
Допустим, мы вот хотим создать вектор пар строковых вьюшек и вывести это добро на консоль. Просто? Просто.
Ну раз просто, тогда поучаствуйте в #quiz'е: какой будет результат попытки компиляции и запуска этого кода на С++23?
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};
for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}
👍8🔥5❤4🤔1😭1
Какой результат попытки компиляции и запуска кода выше под С++23?
Anonymous Poll
18%
Ошибка компиляции
30%
one and two\nthree and four
9%
one and three
3%
two and four
41%
Где-то здесь, рядом с собакой, уб зарыто...
❤7😁7👍3🔥3
WAT. История скобок, изменивших все
#опытным
Спасибо, @d7d1cd, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Посмотрите еще раз на этот пример:
Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:
Но у вашего компилятора на это другое мнение. Кланг, например, выводит:
WAT? А где 2 и 4? И почему вообще 1 элемент?
С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.
Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:
Конструируя вектор с помощью универсальной инициализации, вы уже внутри самых внешних скобок должны перечислять элементы.
Вот и получается, что строка выше парсится компилятором, как одна пара.
Тогда получается, что вью на строку можно создать с помощью
Без проблем. Вот вам подходящий конструктор:
У нас же строковые литералы неявно приводятся к указателям. Очень уж похоже на то, что мы хотим создать вью на непрерывный поток байтов. Компилятор именно это и предполагает. Жаль, что только:
оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.
Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.
А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.
Пофиксить эту неприятную неожиданность можно либо убрав лишнюю пару скобок, либо явно сказав, где вы хотите видеть пары:
Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного
Avoid ambiguity. Stay cool.
#STL #cpp17 #cpp23
#опытным
Спасибо, @d7d1cd, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Посмотрите еще раз на этот пример:
int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};
for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}
Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:
one and two
three and four
Но у вашего компилятора на это другое мнение. Кланг, например, выводит:
one and three
WAT? А где 2 и 4? И почему вообще 1 элемент?
С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.
Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:
/->/{/<-/{"one", "two"}, {"three", "four"}/->/}/<-/Конструируя вектор с помощью универсальной инициализации, вы уже внутри самых внешних скобок должны перечислять элементы.
Вот и получается, что строка выше парсится компилятором, как одна пара.
Тогда получается, что вью на строку можно создать с помощью
{"one", "two"}?!? Без проблем. Вот вам подходящий конструктор:
template< class It, class End >
constexpr basic_string_view( It first, End last );
У нас же строковые литералы неявно приводятся к указателям. Очень уж похоже на то, что мы хотим создать вью на непрерывный поток байтов. Компилятор именно это и предполагает. Жаль, что только:
The behavior is undefined if [first, last) is not a valid range
оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.
Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.
А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.
Пофиксить эту неприятную неожиданность можно либо убрав лишнюю пару скобок, либо явно сказав, где вы хотите видеть пары:
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{std::pair{"one", "two"}, std::pair{"three", "four"}}
};
Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного
pair. Компилятор, следуя правилам инициализации из списка, развернул вложенный список и интерпретировал содержимое как два отдельных элемента для вектора. Можно убедиться тут. Спасибо @Shuomi за комментарий по поводу различного поведения при разных стандартах)Avoid ambiguity. Stay cool.
#STL #cpp17 #cpp23
❤26👍14🤯9🔥6
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
Обзор книжки #3
Начнем с цитаты: "Языки программирования бывают двух видов: которые ругают, и на которых не пишут."
С++ ругают все, кому не лень. Небезопасный, раздутый, нечитаемый и тд. Но есть индустрия, в которой С++ любят и никуда с него не будут уходить - геймдев. Им нужен высокоуровневый инструмент, с помощью которого можно просто реализовывать сложные абстракции. Но этого не хватает: в условиях, когда для портирования игры нужно экономить буквально мегабайты памяти и микросекунды исполнения, инструмент должен позволять управлять ресурсами компьютера на самом низком уровне.
Всем геймдевистам, активным и латентным, приготовиться. Потому что именно про это книга Сергея Кушниренко "Game++. Устройство и оптимизация игрового движка".
Как с помощью С++ выжимать все соки из железа, чтобы игроки получали плавную графику и стабильный fps, а разработчики могли поддерживать и расширять игровой функционал? - после прочтения у меня сложилось впечатление, что именно на этот вопрос отвечает "Game++".
Там не будет пошаговой инструкции для написания своего движка, коих на ютубе можно найти добрую кучу с тележкой. В книге описан скорее систематизированный опыт разработки игр: автор приводит много примеров из своей практики, фокусируется на выявлении проблем производительности и обсуждении общих принципов проектирования современных игр и игровых движков.
Да, немного об авторе. Больше 20 лет практики программирования. Работал в EA над The Sims и SimCity BuildIt под слабые устройства тех лет, в Gaijin Entertainment довел с командой с нуля до стора Nintendo Switch и AppleTV порты Blades Of Time и Cuisine Royale, а также занимался ИИ-монстрами во вселенной Metro. Сейчас работает над движком Age of Empires 2 — старая игра, но современные требования к производительности и функциональности заставляют бежать вслед за прогрессом. То есть, это и ААА, и АА, и инди проекты. Сергею точно есть, что рассказать о игрострое.
Пройдемся по поинтам в контенте.
Для таких чайников в геймдеве, как я, в начале книги дается довольно подробный рассказ о том, что такое игровой движок, из чего он состоит и высокоуровневое описание популярных движков.
Дальше добрую часть книги автор уделяет оптимизациям памяти. Как сэкономить количество обращений к памяти, ее занятый объем и время выделения|очистки(как на очистку может тратиться даже больше времени, чем на выделение, см. в книге). Мой краш - глава про кастомные аллокаторы. Тема и так не простая, а здесь все собрано вместе с кодом и реальными кейсами применения.
Минуя краткий обзор алгоритмов STL(с интересными повествовательными приемами, типа "вредные советы") вы попадаете в многопоточныймир ад. Вы посмотрите на примеры задач с многопоточностью аля тредпул и с выделенными потоками для конкретных игровых подсистем. А также узнаете, почему блокировки потоков отбрасывают производительность в каменный век и какого это, программировать без мьютексов.
Под конец вы обложитесь паттернами банды четырех и не только и поймете, как из них не Франкенштейна слепить, но систему, позволяющую гибко добавлять игровые механики и при этом отвязывать друг от друг компоненты движка и игры.
Очень круто, что в книге есть повествование. Я ее прочитал буквально за пару выходных и у меня не было желания отвлекаться из-за скуки. Хороший слог и крутые примеры буквально заставляют перелистывать страницы. Помимо кастомных аллокаторов мне зашла тема архитектуры и как правильно проектировать разные компоненты движка. Если вам не приходит в голову некостыльное решение задачи "выдать пользователю очивку при первом убийстве монстра из лука", то вам точно стоит ознакомиться с "Game++".
А знаете, в чем самая мякотка? Мы с автором разыграем в этом канале один экземпляр книжки.
Все, что вам надо сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом слово "Конкурс". Повторные комментарии будут удаляться. Шанс залететь у всех будет еще ровно 7 дней, начиная с этого момента. На 8 день выйдет пост с результатами.
Победителя естественно выберем рандомайзером.
Be lucky. Stay cool.
Начнем с цитаты: "Языки программирования бывают двух видов: которые ругают, и на которых не пишут."
С++ ругают все, кому не лень. Небезопасный, раздутый, нечитаемый и тд. Но есть индустрия, в которой С++ любят и никуда с него не будут уходить - геймдев. Им нужен высокоуровневый инструмент, с помощью которого можно просто реализовывать сложные абстракции. Но этого не хватает: в условиях, когда для портирования игры нужно экономить буквально мегабайты памяти и микросекунды исполнения, инструмент должен позволять управлять ресурсами компьютера на самом низком уровне.
Всем геймдевистам, активным и латентным, приготовиться. Потому что именно про это книга Сергея Кушниренко "Game++. Устройство и оптимизация игрового движка".
Как с помощью С++ выжимать все соки из железа, чтобы игроки получали плавную графику и стабильный fps, а разработчики могли поддерживать и расширять игровой функционал? - после прочтения у меня сложилось впечатление, что именно на этот вопрос отвечает "Game++".
Там не будет пошаговой инструкции для написания своего движка, коих на ютубе можно найти добрую кучу с тележкой. В книге описан скорее систематизированный опыт разработки игр: автор приводит много примеров из своей практики, фокусируется на выявлении проблем производительности и обсуждении общих принципов проектирования современных игр и игровых движков.
Да, немного об авторе. Больше 20 лет практики программирования. Работал в EA над The Sims и SimCity BuildIt под слабые устройства тех лет, в Gaijin Entertainment довел с командой с нуля до стора Nintendo Switch и AppleTV порты Blades Of Time и Cuisine Royale, а также занимался ИИ-монстрами во вселенной Metro. Сейчас работает над движком Age of Empires 2 — старая игра, но современные требования к производительности и функциональности заставляют бежать вслед за прогрессом. То есть, это и ААА, и АА, и инди проекты. Сергею точно есть, что рассказать о игрострое.
Пройдемся по поинтам в контенте.
Для таких чайников в геймдеве, как я, в начале книги дается довольно подробный рассказ о том, что такое игровой движок, из чего он состоит и высокоуровневое описание популярных движков.
Дальше добрую часть книги автор уделяет оптимизациям памяти. Как сэкономить количество обращений к памяти, ее занятый объем и время выделения|очистки(как на очистку может тратиться даже больше времени, чем на выделение, см. в книге). Мой краш - глава про кастомные аллокаторы. Тема и так не простая, а здесь все собрано вместе с кодом и реальными кейсами применения.
Минуя краткий обзор алгоритмов STL(с интересными повествовательными приемами, типа "вредные советы") вы попадаете в многопоточный
Под конец вы обложитесь паттернами банды четырех и не только и поймете, как из них не Франкенштейна слепить, но систему, позволяющую гибко добавлять игровые механики и при этом отвязывать друг от друг компоненты движка и игры.
Очень круто, что в книге есть повествование. Я ее прочитал буквально за пару выходных и у меня не было желания отвлекаться из-за скуки. Хороший слог и крутые примеры буквально заставляют перелистывать страницы. Помимо кастомных аллокаторов мне зашла тема архитектуры и как правильно проектировать разные компоненты движка. Если вам не приходит в голову некостыльное решение задачи "выдать пользователю очивку при первом убийстве монстра из лука", то вам точно стоит ознакомиться с "Game++".
А знаете, в чем самая мякотка? Мы с автором разыграем в этом канале один экземпляр книжки.
Все, что вам надо сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом слово "Конкурс". Повторные комментарии будут удаляться. Шанс залететь у всех будет еще ровно 7 дней, начиная с этого момента. На 8 день выйдет пост с результатами.
Победителя естественно выберем рандомайзером.
Be lucky. Stay cool.
❤31🔥15👍8❤🔥2👏2😁1
🤔 Не понимаете, как сделать код универсальным и при этом не потерять в скорости?
📆 На открытом уроке 18 марта в 20:00 (МСК) разберём, как описывать поведение через типажи (traits), писать обобщённый код с использованием обобщённых типов и осознанно выбирать между статическим и динамическим полиморфизмом.
На практике превратим конкретную функцию в универсальную, сохранив безопасность и производительность.
После онлайн-семинара вы сможете объяснить разницу между impl Trait и dyn Trait (способами задания типов), проектировать обобщённые структуры данных и читать сложный код в стандартной библиотеке и сторонних библиотеках.
⚡️ Встречаемся в преддверии старта курса «Rust Developer. Basic».
Если вы хотите писать универсальный и при этом быстрый код на Rust — регистрируйтесь и проверьте свои подходы на практике: https://otus.pw/OqFT/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
📆 На открытом уроке 18 марта в 20:00 (МСК) разберём, как описывать поведение через типажи (traits), писать обобщённый код с использованием обобщённых типов и осознанно выбирать между статическим и динамическим полиморфизмом.
На практике превратим конкретную функцию в универсальную, сохранив безопасность и производительность.
После онлайн-семинара вы сможете объяснить разницу между impl Trait и dyn Trait (способами задания типов), проектировать обобщённые структуры данных и читать сложный код в стандартной библиотеке и сторонних библиотеках.
⚡️ Встречаемся в преддверии старта курса «Rust Developer. Basic».
Если вы хотите писать универсальный и при этом быстрый код на Rust — регистрируйтесь и проверьте свои подходы на практике: https://otus.pw/OqFT/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
👎5❤3👍3🔥3😁3
Выравнивание. А зачем?
#новичкам
Представим, что мы пишем что-то связанное с графикой. У нас есть плоскость черно-белых пикселей. Каждый пиксель определяется через цвет черно-белого тона и координаты на плоскости:
Мы хотим сделать супер-мега-блезингово-быстрое приложение, которое при работе будет тратить всего 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
🔥23❤12👍10