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.