Ответ 1
Его необходимо удовлетворить инварианту от стандарта С++: каждый объект С++ того же типа должен иметь уникальный адрес для идентификации.
Если объекты не занимали места, элементы в массиве будут иметь один и тот же адрес.
Я обнаружил, что оба компилятора 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() (значение)?
Его необходимо удовлетворить инварианту от стандарта С++: каждый объект С++ того же типа должен иметь уникальный адрес для идентификации.
Если объекты не занимали места, элементы в массиве будут иметь один и тот же адрес.
В принципе, это взаимодействие между двумя требованиями:
Обратите внимание, что первое условие не требует ненулевого размера: Учитывая
struct empty {};
struct foo { empty a, b; };
первое требование может быть легко выполнено с помощью нулевого размера a
, за которым следует один байт заполнения, чтобы обеспечить соблюдение другого адреса, за которым следует нулевой размер b
. Однако, учитывая
empty array[2];
который больше не работает, потому что отступы между различными объектами empty[0]
и empty[1]
не будут разрешены.
Все полные объекты должны иметь уникальный адрес; поэтому они должны взять хотя бы один байт памяти - байт по их адресу.
Типичным случаем, когда это может произойти, является контейнерный класс с предикатом сравнения, хранящимся в переменной-члене.
В этом случае вы можете использовать пустую оптимизацию базового класса: базовому подобъекту разрешено иметь тот же адрес, что и полный объект, частью которого он является, поэтому он не может содержать никакого хранилища. Таким образом, вы можете приложить предикат к классу как (возможно, частный) базовый класс, а не к члену. Это немного сложнее иметь дело с чем-то, но должно устранить накладные расходы.
какой тип накладных расходов следует ожидать при вызове A() (значение)?
Единственные накладные расходы по сравнению с вызовом функции, не являющейся членом, будут передавать дополнительный аргумент this
. Если функция включена, то это должно быть устранено (как, в общем случае, при вызове функции-члена, которая не имеет доступа к каким-либо переменным-членам).
Уже есть отличные ответы, которые отвечают на главный вопрос. Я хотел бы затронуть проблемы, которые вы высказали:
Но стоимость этого довольно серьезная при работе с контейнерами небольшого размера. Учитывая возможное выравнивание данных, мы можем получить до 16 байтов на класс без vars (?!). Предположим, у нас есть пользовательский контейнер, который обычно содержит несколько значений int. Затем рассмотрим массив таких контейнеров (около 1000000 членов). Накладные расходы будут составлять 16 * 1000000! Типичным случаем, когда это может произойти, является контейнерный класс с предикатом сравнения, хранящимся в переменной-члене.
Избежать стоимости удерживания A
Если все экземпляры контейнера зависят от типа A
, тогда нет необходимости хранить экземпляры A
в контейнере. Накладные расходы, связанные с ненулевым размером A
, можно избежать, просто создав экземпляр A
в стеке, когда это необходимо.
Невозможно избежать стоимости удерживания A
Вы можете принудительно удерживать указатель на A
в каждом экземпляре контейнера, если ожидается, что A
будет полиморфным. Для такого ограничения стоимость каждого контейнера увеличивается на величину указателя. Имеются ли какие-либо переменные-члены в базовом классе A
или нет, не имеет никакого значения для размера контейнера.
Влияние sizeof A
В любом случае размер пустого класса не должен влиять на требования к хранению контейнера.