The size of any object or member subobject is required to be at least 1 even if the type is an empty class type [...], in order to be able to guarantee that the addresses of distinct objects of the same type are always distinct.
This I knew. What I just found out is that some library types, like std::tuple
don't use any size for contained empty classes. Is this true? If yes, how is that ok?
Edit: after reading @bolov's final note on his answer I still one question: since Empty
is POD
it is safe to memcpy
to it. But if you would memcpy to a "phantom" address (see @bolov's answer) you would effectively write inside an int
element ( sizoef(Empty)
is 1). That doesn't seem ok.
The size of an object must be greater than zero. The size of a subobject does not have that constraint. This leads to the Empty Base Optimization (EBO), in which an empty base class does not take up space (compilers started implementing this nearly 20 years ago). And that, in turn, leads to the usual implementation of std::tuple
as an inheritance chain, in which empty base classes don't take up space.
tl,dr This increased my respect for the library implementors even more. The rules they had to navigate to achieve this optimization for std::tuple
is awe inspiring once you start thinking how could this be implemented.
Naturally I went on and played a little to see how this goes.
The setup:
struct Empty {};
template <class T> auto print_mem(const T& obj)
{
cout << &obj << " - " << (&obj + 1) << " (" << sizeof(obj) << ")" << endl;
}
The test:
int main() {
std::tuple<int> t_i;
std::tuple<Empty> t_e;
std::tuple<int, Empty, Empty> t_iee;
std::tuple<Empty, Empty, int> t_eei;
std::tuple<int, Empty, Empty, int> t_ieei;
cout << "std::tuple<int>" << endl;
print_mem(t_i);
cout << endl;
cout << "std::tuple<Empty>" << endl;
print_mem(t_e);
cout << endl;
cout << "std::tuple<int, Empty, Empty" << endl;
print_mem(t_iee);
print_mem(std::get<0>(t_iee));
print_mem(std::get<1>(t_iee));
print_mem(std::get<2>(t_iee));
cout << endl;
cout << "std::tuple<Empty, Empty, int>" << endl;
print_mem(t_eei);
print_mem(std::get<0>(t_eei));
print_mem(std::get<1>(t_eei));
print_mem(std::get<2>(t_eei));
cout << endl;
print_mem(t_ieei);
print_mem(std::get<0>(t_ieei));
print_mem(std::get<1>(t_ieei));
print_mem(std::get<2>(t_ieei));
print_mem(std::get<3>(t_ieei));
cout << endl;
}
The results:
std::tuple<int>
0xff83ce64 - 0xff83ce68 (4)
std::tuple<Empty>
0xff83ce63 - 0xff83ce64 (1)
std::tuple<int, Empty, Empty
0xff83ce68 - 0xff83ce6c (4)
0xff83ce68 - 0xff83ce6c (4)
0xff83ce69 - 0xff83ce6a (1)
0xff83ce68 - 0xff83ce69 (1)
std::tuple<Empty, Empty, int>
0xff83ce6c - 0xff83ce74 (8)
0xff83ce70 - 0xff83ce71 (1)
0xff83ce6c - 0xff83ce6d (1)
0xff83ce6c - 0xff83ce70 (4)
std::tuple<int, Empty, Empty, int>
0xff83ce74 - 0xff83ce80 (12)
0xff83ce7c - 0xff83ce80 (4)
0xff83ce78 - 0xff83ce79 (1)
0xff83ce74 - 0xff83ce75 (1)
0xff83ce74 - 0xff83ce78 (4)
We can see right from the start that
sizeof(std:tuple<Empty>) == 1 (because the tuple cannot be empty)
sizeof(std:tuple<int>) == 4
sizeof(std::tuple<int, Empty, Empty) == 4
sizeof(std::tuple<Empty, Empty, int) == 8
sizeof(std::tuple<int, int>) == 8
sizeof(std::tuple<int, Empty, Empty, int>) == 12
We can see that sometimes indeed no space is reserved for Empty
, but in some circumstances 1 byte
is allocated for Empty
(rest is padding). It looks like it can be 0
when Empty
elements are the last elements.
Closely examining the addresses obtained via get
we can see that never two Empty
tuple elements have the same address (which is in line with the rule above), even if those addresses (of Empty
) appear to be inside the int
element. Moreover no address of an Empty
element is outside the memory location of the container tuple.
This got me thinking: what if we have more trailing Empty
than sizeof(int)
. Would that increase the sizeof the tuple? Indeed it does:
sizeof(std::tuple<int>) // 4
sizeof(std::tuple<int, Empty>) // 4
sizeof(std::tuple<int, Empty, Empty>) // 4
sizeof(std::tuple<int, Empty, Empty, Empty>) // 4
sizeof(std::tuple<int, Empty, Empty, Empty, Empty>) // 4
sizeof(std::tuple<int, Empty, Empty, Empty, Empty, Empty>) // 8 yep. Magic
One final note: Is is OK to have "phantom" addresses for Empty
elements (they appear to "share" the memory with the int
element). Since Empty
is an... well... empty class, it has no non-static data members, which means that the memory obtained for them can not be accessed.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.