Empty class in std :: tuple

The size of any object or subobject of a participant must be at least 1, even if the type is an empty type of the class [...] in order to be able to guarantee that the addresses of individual objects of the same type are always different.

cppreference quote

I knew that. I just found out that some types of libraries, such as std::tuple , do not use any size for contained empty classes. It's true? If so, how is it normal?


Edit: after reading @bolov's final note about his answer, I have one more question: since Empty is POD is memcpy safe for him. But if you memcpy to the address "phantom" (see @bolov answer), you will effectively write inside the int element ( sizoef(Empty) is 1). This does not look normal.

+7
c ++ language-lawyer sizeof tuples
source share
2 answers

The size of the object must be greater than zero. Subelement Size does not have such a limitation. This leads to optimization of the empty base (EBO), in which an empty base class does not take up space (compilers started to implement this almost 20 years ago). And this, in turn, leads to the usual implementation of std::tuple as an inheritance chain in which empty base classes do not take up space.

+5
source share

tl, dr This further increased my respect for library developers. The rules that they had to follow to achieve this optimization for std::tuple are impressive when you start thinking about how this can be implemented.


Naturally, I continued and played a little to understand how this happens.

Setup:

 struct Empty {}; template <class T> auto print_mem(const T& obj) { cout << &obj << " - " << (&obj + 1) << " (" << sizeof(obj) << ")" << endl; } 

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; } 

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) 

Perfect link

From the very beginning we can see 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 see that sometimes there is no place reserved for Empty , but in some cases 1 byte is allocated for Empty (the rest is filling). It looks like it could be 0 when the Empty elements are the last.

Carefully studying the addresses obtained with get , we see that never two tuple Empty elements have the same address (which corresponds to the above rule), even if these addresses (from Empty ) are inside the int element. Moreover, the address of the Empty element is not outside the container’s memory cell.

This made me think: what if we had more trailing Empty than sizeof(int) . Will the size of the tuple increase? Really:

 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 it okay to have "phantom" addresses for Empty elements (they seem to "share" memory with an int element). Since Empty is ... well ... an empty class, it does not have non-static data elements, which means that access to the memory received for them cannot be available.

+2
source share

All Articles