Why does std::tuple break small-size struct calling convention optimization in C++?

It seems to be a matter of ABI. For instance, the Itanium C++ ABI reads:

If the parameter type is non-trivial for the purposes of calls, the caller must allocate space for a temporary and pass that temporary by reference.

And, further:

A type is considered non-trivial for the purposes of calls if it has a non-trivial copy constructor, move constructor, or destructor, or all of its copy and move constructors are deleted.

The same requirement is in AMD64 ABI Draft 1.0.

For instance, in libstdc++, std::tuple has non-trivial move constructor: https://godbolt.org/z/4j8vds. The Standard prescribes both copy and move constructor as defaulted, which is satisfied here. However, at the same time, tuple inherits from _Tuple_impl and _Tuple_impl has a user-defined move constructor. Consequenlty, move constructor of tuple itself cannot be trivial.

On the contrary, in libc++, both copy and move constructors of std::tuple<int> are trivial. Therefore, the argument is passed in a register there: https://godbolt.org/z/WcTjM9.

As for Microsoft STL, std::tuple<int> is trivially neither copy-constructible nor move-constructible. It even seems to break the C++ Standard rules. std::tuple is defined recursively and, at the end of recursion, std::tuple<> specialization defines non-defaulted copy constructor. There is a comment about this issue: // TRANSITION, ABI: should be defaulted. Since tuple<> has no move constructor, both copy and move constructors of tuple<class...> are non-trivial.

Leave a Comment