mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
201 lines
10 KiB
Markdown
201 lines
10 KiB
Markdown
# Типы «нулевого» размера
|
||
|
||
В С++ при определении собственных классов и структур никто нам не запрещает не указывать ни одного поля, оставляя структуру пустой:
|
||
|
||
```C++
|
||
struct MyTag {};
|
||
```
|
||
|
||
Конечно же, мы можем не только объявлять пустые структуры, но и создавать объекты этих типов
|
||
|
||
```C++
|
||
struct Tag {};
|
||
|
||
Tag func(Tag t1) {
|
||
Tag t2;
|
||
return Tag{};
|
||
}
|
||
```
|
||
Возможности несомненно полезные и широко используемые:
|
||
|
||
- Для определения абстрактного статического или динамического полиморфного интерфейса
|
||
- Для введения тегов выбора нужной перегрузки
|
||
- Для определения различных предикатов и метафункций над типами
|
||
|
||
А давайте сыграем в игру? Я буду показывать вам разные определения структур, а вы постараетесь угадать их размеры в байтах (`sizeof`). Начинаем?
|
||
|
||
```C++
|
||
struct StdAllocator {};
|
||
|
||
struct Vector1 {
|
||
int* data;
|
||
int* size_end;
|
||
int* capacity_end;
|
||
StdAllocator alloc;
|
||
};
|
||
|
||
struct Vector2 {
|
||
StdAllocator alloc;
|
||
int* data;
|
||
int* size_end;
|
||
int* capacity_end;
|
||
};
|
||
|
||
struct Vector3 : StdAllocator {
|
||
int* data;
|
||
int* size_end;
|
||
int* capacity_end;
|
||
};
|
||
```
|
||
|
||
[Угадали?](https://godbolt.org/z/MEETs4csc)
|
||
|
||
`Vector1` и `Vector2` имеют размеры `4*sizeof(int*)`.
|
||
Но как же так?! Откуда берутся `3*sizeof(int*)` совершенно очевидно. Но четвертый-то откуда?!
|
||
|
||
Все очень просто: в C++ не бывает структур нулевого размера. И потому размер пустой структуры: `sizeof(StdAllocator) == 1`
|
||
|
||
Но `sizeof(int*) != 1`. По крайней мере на x86. А это еще проще: выравнивание и паддинг. `Vector1` добивается байтами в конец, чтобы его размер был кратен выравниванию первого поля. А в `Vector2` добиваются дополнительные байты между `alloc` и `data`, чтобы смещение до `data` было кратным его выравниванию. Все очень просто и очевидно! Если же вам, как и многим другим людям, которые не задаются подобными вопросами каждый день, не очевидно наличие паддинга в той или иной структуре, то советую [использовать](https://godbolt.org/z/a8xof3eqG) флаг компилятора `-Wpadded` для GCC/Clang.
|
||
|
||
Хорошо, мы разобрались с `Vector1` и `Vector2`. А что там с `Vector3`? Тоже `4*sizeof(int*)`? Ведь мы же знаем, что подобъект базового класса должен быть где-то размещен, а его размер, как мы выяснили, не нулевой...
|
||
А вот и нет! Размер `Vector3` равен `3*sizeof(int*)`! Но как же так?! А это называется EBO [(empty base optimization)](https://en.cppreference.com/w/cpp/language/ebo).
|
||
|
||
Интересный zero-cost! Для сравнения, можно глянуть на аналогичные пустые структуры в Rust. Там их размер [может быть](https://godbolt.org/z/r9YTKrbb3) равен нулю.
|
||
|
||
Ну ладно, мы выяснили, что, неаккуратно использовав пустые структуры, мы можем получить увеличение потребления памяти. Давайте играть дальше.
|
||
|
||
```C++
|
||
struct StdAllocator {};
|
||
struct StdComparator {};
|
||
|
||
struct Map1 {
|
||
StdAllocator alloc;
|
||
StdComparator comp;
|
||
};
|
||
|
||
struct Map2 {
|
||
StdAllocator alloc;
|
||
[[no_unique_address]] StdComparator comp;
|
||
};
|
||
|
||
struct Map3 {
|
||
[[no_unique_address]] StdAllocator alloc;
|
||
[[no_unique_address]] StdComparator comp;
|
||
};
|
||
|
||
struct MapImpl1 : Map1 {
|
||
int x;
|
||
};
|
||
|
||
struct MapImpl2 : Map2 {
|
||
int x;
|
||
};
|
||
|
||
struct MapImpl3 : Map3 {
|
||
int x;
|
||
};
|
||
```
|
||
|
||
Чему равны размеры `Map1`, `Map2`, `Map3`?
|
||
|
||
Ну, тут все просто!:
|
||
- Очевидно, что `sizeof(Map1) == 2`, ведь она состоит из двух пустых структур, каждая из которых имеет размер 1.
|
||
- Благодаря атрибуту `[[no_unique_address]]` из стандарта C++20 (Clang поддерживает с C++11), `Map2` и `Map3` должны иметь размер 1. В `Map3` оба поля разделяют общий адрес. В `Map2` то же самое. Да и меньше чем 1 не бывает.
|
||
|
||
Хорошо. А что же теперь с наследующими структурами?
|
||
|
||
Все по `2*sizeof(int)`? А вот и нет: у `MapImpl3` [работает EBO](https://godbolt.org/z/exo5zP64E)!
|
||
|
||
Ну ладно. В этом есть какая-то логика и закономерность. Это еще можно принять. Хотя... На самом деле вы были правы! Ведь если у вас компилятор msvc, то `[[no_unique_address]]` просто [не работает](https://godbolt.org/z/6364qzYe6). И не будет работать. Потому что msvc долгое время просто игнорировал незнакомые ему атрибуты. И если поддержать `[[no_unique_address]]`, то сломается бинарная совместимость. Используйте `[[msvc::no_unique_address]]`! EBO, правда, пока [не работает](https://godbolt.org/z/GTjrsbPPK).
|
||
|
||
|
||
## zero-size array
|
||
|
||
Язык C (не С++), начиная с версии стандарта 99, позволяет использовать следующую любопытную конструкцию:
|
||
|
||
```C
|
||
struct ImageHeader{
|
||
int h;
|
||
int w;
|
||
};
|
||
|
||
struct Image {
|
||
struct ImageHeader header;
|
||
char data[];
|
||
};
|
||
```
|
||
|
||
Поле `data` в структуре `Image` имеет [нулевой размер](https://godbolt.org/z/d3xfdj3Ke). Это FAM (flexible array member). Очень удобная штука, чтобы получать доступ к массиву статически не известной длины, размещенному сразу после некоторого заголовка в бинарном буфере. Длина массива обычно указывается в самом заголовке. FAM может быть только последним полем в структуре.
|
||
|
||
Стандарт C++ такие фичи не разрешает. Но ведь есть GCC с его нестандартными включенными по умолчанию расширениями.
|
||
|
||
Что будет если сделать так?
|
||
```C++
|
||
struct S {
|
||
char data[];
|
||
};
|
||
```
|
||
Чему будет равен размер структуры `S`?
|
||
|
||
В стандартном C пустые структуры в принципе запрещены. И поведение программы с ними не определено. GCC определяет их размер нулевым при компиляции C программ. А при компиляции C++ — размер, как мы выяснили ранее, единичный. Дело пахнет страшными багами и ночными кошмарами при неосторожном проектировании C++ библиотек с сишным интерфейсом или использованием C-библиотек в C++!
|
||
|
||
Но вернемся все-таки к нашей структуре с FAM. Поле в ней есть. Стандартный C опять-таки требует, чтобы было еще хотя бы одно поле ненулевой длины перед FAM. GNU C же охотно сделает нам структуру нулевого размера.
|
||
|
||
А теперь [посмотрим](https://godbolt.org/z/osrzYbcsj) на GCC C++.
|
||
|
||
```C++
|
||
struct S1 {
|
||
char data[];
|
||
};
|
||
|
||
struct S2 {};
|
||
|
||
static_assert(sizeof(S1) != sizeof(S2));
|
||
static_assert(sizeof(S1) == 0);
|
||
```
|
||
|
||
И вот уже внезапно у нас в C++ структуры нулевого размера. Только C++ не стандартный.
|
||
Каким образом такие структуры будет взаимодействовать с EBO — нужно читать в спецификации к GCC.
|
||
|
||
## tag dispatching
|
||
|
||
Мы видели, что неаккуратное использование пустых структур приводит к увеличению размера других, не пустых структур.
|
||
А может еще есть какие-то подводные камни? Например, при использовании пустых структур-тегов для выбора перегрузки?
|
||
|
||
Есть ли разница между
|
||
```C++
|
||
struct Mul {};
|
||
struct Add {};
|
||
|
||
int op(Mul, int x, int y) {
|
||
return x * y;
|
||
}
|
||
|
||
int op(Add, int x, int y) {
|
||
return x + y;
|
||
}
|
||
```
|
||
|
||
и
|
||
|
||
```C++
|
||
int mul(int x, int y) {
|
||
return x * y;
|
||
}
|
||
|
||
int add(int x, int y) {
|
||
return x + y;
|
||
}
|
||
```
|
||
в плане генерируемого кода?
|
||
|
||
|
||
Краткий ответ: да. Есть разница. Зависит от конкретной имплементации. Стандарт не гарантирует оптимизацию пустых аргументов.
|
||
От перемены позиций тегов может меняться бинарный интерфейс. Поиграться с наиболее заметными изменениями можно на примере [msvc](https://godbolt.org/z/E68ojMb8f).
|
||
|
||
|
||
## Полезные ссылки
|
||
1. https://devblogs.microsoft.com/cppblog/msvc-cpp20-and-the-std-cpp20-switch
|
||
2. https://en.cppreference.com/w/cpp/language/attributes/no_unique_address
|
||
3. https://en.cppreference.com/w/cpp/language/ebo
|