Программирование C++C++ CoreC++ разработчик системного программного обеспечения

От чего зависит размер объекта пустого класса в C++ и почему он не равен нулю?

От чего зависит размер объекта пустого класса в C++ и почему он не равен нулю?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Размер объекта пустого класса не может быть нулевым: стандарт гарантирует, что sizeof(T) для любого объектного типа не меньше единицы. Обычно пустой класс занимает один байт, но фактический размер может быть больше из-за выравнивания и особенностей ABI.

Исключение на уровне размещения возможно для пустого базового класса: благодаря empty base optimization он может не увеличивать размер производного объекта.

Исторический контекст

Объектная модель C++ должна обеспечивать существование каждого объекта как отдельной сущности с адресом. Если бы обычный объект пустого класса имел нулевой размер, массив таких объектов не имел бы способа различать адреса соседних элементов.

Поэтому для обычных объектов введён ненулевой минимальный размер. Одновременно C++ допускает оптимизацию пустых базовых классов, поскольку базовая часть может не содержать данных и не обязана увеличивать представление производного объекта.

Постановка проблемы

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

Ошибочное предположение о нулевом размере приводит к неверным расчётам памяти, неправильным ожиданиям от массивов и неожиданному росту размера классов, использующих пустые типы как поля. Особенно это заметно в policy-based design и контейнерах, где пустые типы применяются как политики поведения.

Подробное решение

Для любого объектного типа T выражение sizeof(T) возвращает размер его представления в байтах, включая необходимое дополнение для выравнивания. Для пустого класса значение не может быть нулём; на практике наиболее распространённый результат — один байт.

Размер не обязан быть ровно одним байтом. Реализация может выбрать больший размер из-за требований ABI, выравнивания или других правил размещения. Кроме того, размер класса обычно дополняется так, чтобы элементы массива имели корректно выровненные адреса.

#include <cstddef> struct Empty {}; struct AsMember { Empty e; int value; }; struct AsBase : Empty { int value; }; static_assert(sizeof(Empty) >= 1);

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

Для пустого базового класса стандарт разрешает empty base optimization. В результате AsBase часто имеет тот же размер, что и один int, но полагаться на конкретное числовое значение без проверки реализации нельзя.

В C++20 похожую возможность для полей предоставляет атрибут [[no_unique_address]]. Он позволяет реализации не выделять отдельное уникальное место для пустого или потенциально пустого поля, однако это разрешение, а не безусловная гарантия одинакового адреса или нулевого дополнительного размера.

Ситуация из практики

В библиотеке есть класс, принимающий политику форматирования. Политика не хранит данных, поэтому её можно представить пустым типом. Если хранить её как обычное поле, каждый экземпляр основного класса может получить дополнительный байт и последующее выравнивание, что особенно невыгодно для большого массива объектов.

Вариант с обычным полем прост и прозрачен, но может увеличить размер каждого объекта. Вариант с наследованием от политики часто использует empty base optimization и уменьшает размер, но усложняет композицию, может создавать неоднозначности при нескольких одинаковых базовых типах и не всегда одинаково реализуется для сложных иерархий.

Выбор обычно делают в пользу пустого базового класса для внутренней реализации низкоуровневого шаблонного типа, если политика действительно не является частью обычного состояния объекта. Для публичного класса важнее сначала проверить читаемость интерфейса и фактический sizeof на целевых компиляторах; оптимизацию нельзя считать универсальной заменой измерения.

Что кандидаты часто упускают

  1. Может ли sizeof(Empty) быть равен нулю?

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

  1. Могут ли два объекта пустого класса иметь один и тот же адрес?

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

Это ограничение не следует переносить безоговорочно на подобъекты. Пустой базовый подобъект может иметь тот же адрес, что и производный объект, а [[no_unique_address]] разрешает реализации совместно размещать некоторые пустые поля.

  1. Гарантирует ли empty base optimization, что пустой базовый класс не увеличивает размер производного?

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

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