Доступ к приватным членам. Интерпретация.
#опытным
Продолжаем исследовать грязь и разврат. Предыдущий пост тут.
Указатели, блин, сложная тема. А для задачи доступа к приватным полям - особенно. Для объектов классов с несколькими полями нужно считать много деталей: учитывать выравнивание полей в объекте и на этой базе высчитывать сдвиг типизированного указателя. Возможно даже несколько кастов понадобиться.
Программисты - люди ленивые, поэтому давайте как-то упрощать.
Допустим, у меня есть класс 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
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