Подтвердить что ты не робот

Почему классы С++ без переменных-членов занимают пространство?

Я обнаружил, что оба компилятора MSVC и GCC выделяют по крайней мере один байт на каждый экземпляр класса, даже если класс является предикатом без переменных-члена (или с только статическими переменными-членами). Следующий код иллюстрирует точку.

#include <iostream>

class A
{
public:
   bool operator()(int x) const
   {
      return x>0;
   }
};

class B
{
public:
   static int v;
   static bool check(int x)
   {
      return x>0;
   }
};

int B::v = 0;

void test()
{
   A a;
   B b;
   std::cout << "sizeof(A)=" << sizeof(A) << "\n"
             << "sizeof(a)=" << sizeof(a) << "\n"
             << "sizeof(B)=" << sizeof(B) << "\n"
             << "sizeof(b)=" << sizeof(b) << "\n";
}

int main()
{
   test();
   return 0;
}

Вывод:

sizeof(A)=1
sizeof(a)=1
sizeof(B)=1
sizeof(b)=1

Мой вопрос: зачем компилятору это нужно? Единственная причина, по которой я могу придумать, - обеспечить, чтобы все указатели var-членов различались, поэтому мы можем различать два элемента типа A или B, сравнивая указатели с ними. Но стоимость этого довольно серьезная при работе с малогабаритными контейнерами. Учитывая возможное выравнивание данных, мы можем получить до 16 байтов на класс без vars (?!). Предположим, у нас есть пользовательский контейнер, который обычно содержит несколько значений int. Затем рассмотрим массив таких контейнеров (около 1000000 членов). Накладные расходы будут составлять 16 * 1000000! Типичным случаем, когда это может произойти, является контейнерный класс с предикатом сравнения, хранящимся в переменной-члене. Кроме того, учитывая, что экземпляр класса должен всегда занимать некоторое пространство, какой тип накладных расходов следует ожидать при вызове A() (значение)?

4b9b3361

Ответ 1

Его необходимо удовлетворить инварианту от стандарта С++: каждый объект С++ того же типа должен иметь уникальный адрес для идентификации.

Если объекты не занимали места, элементы в массиве будут иметь один и тот же адрес.

Ответ 2

В принципе, это взаимодействие между двумя требованиями:

  • Два разных объекта одного типа должны иметь разные адреса.
  • В массивах не должно быть никаких дополнений между объектами.

Обратите внимание, что первое условие не требует ненулевого размера: Учитывая

struct empty {};
struct foo { empty a, b; };

первое требование может быть легко выполнено с помощью нулевого размера a, за которым следует один байт заполнения, чтобы обеспечить соблюдение другого адреса, за которым следует нулевой размер b. Однако, учитывая

empty array[2];

который больше не работает, потому что отступы между различными объектами empty[0] и empty[1] не будут разрешены.

Ответ 3

Все полные объекты должны иметь уникальный адрес; поэтому они должны взять хотя бы один байт памяти - байт по их адресу.

Типичным случаем, когда это может произойти, является контейнерный класс с предикатом сравнения, хранящимся в переменной-члене.

В этом случае вы можете использовать пустую оптимизацию базового класса: базовому подобъекту разрешено иметь тот же адрес, что и полный объект, частью которого он является, поэтому он не может содержать никакого хранилища. Таким образом, вы можете приложить предикат к классу как (возможно, частный) базовый класс, а не к члену. Это немного сложнее иметь дело с чем-то, но должно устранить накладные расходы.

какой тип накладных расходов следует ожидать при вызове A() (значение)?

Единственные накладные расходы по сравнению с вызовом функции, не являющейся членом, будут передавать дополнительный аргумент this. Если функция включена, то это должно быть устранено (как, в общем случае, при вызове функции-члена, которая не имеет доступа к каким-либо переменным-членам).

Ответ 4

Уже есть отличные ответы, которые отвечают на главный вопрос. Я хотел бы затронуть проблемы, которые вы высказали:

Но стоимость этого довольно серьезная при работе с контейнерами небольшого размера. Учитывая возможное выравнивание данных, мы можем получить до 16 байтов на класс без vars (?!). Предположим, у нас есть пользовательский контейнер, который обычно содержит несколько значений int. Затем рассмотрим массив таких контейнеров (около 1000000 членов). Накладные расходы будут составлять 16 * 1000000! Типичным случаем, когда это может произойти, является контейнерный класс с предикатом сравнения, хранящимся в переменной-члене.

Избежать стоимости удерживания A

Если все экземпляры контейнера зависят от типа A, тогда нет необходимости хранить экземпляры A в контейнере. Накладные расходы, связанные с ненулевым размером A, можно избежать, просто создав экземпляр A в стеке, когда это необходимо.

Невозможно избежать стоимости удерживания A

Вы можете принудительно удерживать указатель на A в каждом экземпляре контейнера, если ожидается, что A будет полиморфным. Для такого ограничения стоимость каждого контейнера увеличивается на величину указателя. Имеются ли какие-либо переменные-члены в базовом классе A или нет, не имеет никакого значения для размера контейнера.

Влияние sizeof A

В любом случае размер пустого класса не должен влиять на требования к хранению контейнера.