it-roy-ru.com

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

Я обнаружил, что компиляторы 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 байт на класс без переменных (?!). Предположим, у нас есть собственный контейнер, который обычно будет содержать несколько значений типа int. Затем рассмотрим массив таких контейнеров (около 1000000 членов). Накладные расходы составят 16 * 1000000! Типичный случай, когда это может произойти, - контейнерный класс с предикатом сравнения, хранящимся в переменной-члене. Кроме того, учитывая, что экземпляр класса всегда должен занимать некоторое пространство, какого типа издержки следует ожидать при вызове A()(value)?

51
bkxp

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

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

74
Konrad Rudolph

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

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

Обратите внимание, что первое условие само по себе не требует ненулевого размера.

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

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

empty array[2];

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

26
celtschk

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

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

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

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

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

13
Mike Seymour

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

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

избегая стоимости владения A

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

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

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

Влияние sizeof A

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

1
R Sahu