简体   繁体   中英

Empty class in std::tuple

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.

cppreference quote

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)

Ideone link

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM