If all of the members of std::tuple
are of standard layout types, is that std::tuple
itself standard layout? The presence of a user-defined copy-constr
One reason std::tuple
cannot be of standard layout, as any classes with members and base classes with members, is that the standard allows for space optimization when deriving even non-empty base classes. For example:
#include <cstdio>
#include <cstdint>
class X
{
uint64_t a;
uint32_t b;
};
class Y
{
uint16_t c;
};
class XY : public X, public Y
{
uint16_t d;
};
int main() {
printf("sizeof(X) is %zu\n", sizeof(X));
printf("sizeof(Y) is %zu\n", sizeof(Y));
printf("sizeof(XY) is %zu\n", sizeof(XY));
}
Outputs:
sizeof(X) is 16
sizeof(Y) is 2
sizeof(XY) is 16
The above shows that the standard allows for class trailing padding to be used for the derived class members. Class XY
has two extra uint16_t
members, yet its size equals to the size of base class X
.
In other words, class XY
layout is the same as that of another class that has no base classes and all the members of XY
ordered by address, e.g. struct XY2 { uint64_t a; uint32_t b; uint16_t c; uint16_t d; };
.
What makes it non-standard layout is that the size of a derived class is not a function of sizes of base classes and derived class members.
Note that the size of a struct
/class
is a multiple of the alignment of one of its members with the largest alignment requirement. So that an array of objects is suitably aligned for such a member. For built-in types normally sizeof(T) == alignof(T)
. Hence sizeof(X)
is a multiple of sizeof(uint64_t)
.
I am not sure whether the standard requires special treatment for struct
, but with g++-5.1.1
if class
is replaced with struct
the above code yields different output:
sizeof(X) is 16
sizeof(Y) is 2
sizeof(XY) is 24
In other words, the trailing padding space optimization is not used when struct
is involved (did not test for exact conditions).
The "list" approach can be used to get standard layout tuple
(the following example has some inaccuracies but demonstrates the idea):
template <class... Rest>
struct tuple;
template <class T, class... Rest>
struct tuple<T, Rest...> {
T value;
tuple<Rest...> next;
};
template <>
struct tuple<> {};
namespace details {
template <size_t N>
struct get_impl {
template <class... Args>
constexpr static auto process(const tuple<Args...>& t) {
return get_impl<N - 1>::process(t.next);
}
};
template <>
struct get_impl<0> {
template <class... Args>
constexpr static auto process(const tuple<Args...>& t) {
return t.value;
}
};
}
template <size_t N, class... Args>
constexpr auto get(const tuple<Args...>& t) {
return details::get_impl<N>::process(t);
}
template <class... Args>
constexpr auto make_tuple(Args&&... args) {
return tuple<Args...>{std::forward<Args>(args)...};
}
No, standard layout requires that all nonstatic data members belong to either one base subobject or directly to the most derived type, and typical implementations of std::tuple
implement one member per base class.
Because a member-declaration cannot be a pack expansion, in light of the above requirement, a standard layout tuple
cannot have more than one member. An implementation could still sidestep the issue by storing all the tuple
"members" inside one char[]
, and obtaining the object references by reinterpret_cast
. A metaprogram would have to generate the class layout. Special member functions would have to be reimplemented. It would be quite a pain.
Inspired by PotatoSwatter's answer, I've dedicated my day to creating a standard layout tuple for C++14.
The code actually works, but is not currently suited for use as it involves undefined behaviour. Treat it as a proof-of-concept. Here's the code I ended up with:
#include <iostream>
#include <type_traits>
#include <array>
#include <utility>
#include <tuple>
//get_size
template <typename T_head>
constexpr size_t get_size()
{
return sizeof(T_head);
}
template <typename T_head, typename T_second, typename... T_tail>
constexpr size_t get_size()
{
return get_size<T_head>() + get_size<T_second, T_tail...>();
}
//concat
template<size_t N1, size_t... I1, size_t N2, size_t... I2>
constexpr std::array<size_t, N1+N2> concat(const std::array<size_t, N1>& a1, const std::array<size_t, N2>& a2, std::index_sequence<I1...>, std::index_sequence<I2...>)
{
return { a1[I1]..., a2[I2]... };
}
template<size_t N1, size_t N2>
constexpr std::array<size_t, N1+N2> concat(const std::array<size_t, N1>& a1, const std::array<size_t, N2>& a2)
{
return concat(a1, a2, std::make_index_sequence<N1>{}, std::make_index_sequence<N2>{});
}
//make_index_array
template<size_t T_offset, typename T_head>
constexpr std::array<size_t, 1> make_index_array()
{
return {T_offset};
}
template<size_t T_offset, typename T_head, typename T_Second, typename... T_tail>
constexpr std::array<size_t, (sizeof...(T_tail) + 2)> make_index_array()
{
return concat(
make_index_array<T_offset, T_head>(),
make_index_array<T_offset + sizeof(T_head),T_Second, T_tail...>()
);
}
template<typename... T_args>
constexpr std::array<size_t, (sizeof...(T_args))> make_index_array()
{
return make_index_array<0, T_args...>();
}
template<int N, typename... Ts>
using T_param = typename std::tuple_element<N, std::tuple<Ts...>>::type;
template <typename... T_args>
struct standard_layout_tuple
{
static constexpr std::array<size_t, sizeof...(T_args)> index_array = make_index_array<T_args...>();
char storage[get_size<T_args...>()];
//Initialization
template<size_t T_index, typename T_val>
void initialize(T_val&& val)
{
void* place = &this->storage[index_array[T_index]];
new(place) T_val(std::forward<T_val>(val));
}
template<size_t T_index, typename T_val, typename T_val2, typename... T_vals_rest>
void initialize(T_val&& val, T_val2&& val2, T_vals_rest&&... vals_rest)
{
initialize<T_index, T_val>(std::forward<T_val>(val));
initialize<T_index+1, T_val2, T_vals_rest...>(std::forward<T_val2>(val2), std::forward<T_vals_rest>(vals_rest)...);
}
void initialize(T_args&&... args)
{
initialize<0, T_args...>(std::forward<T_args>(args)...);
}
standard_layout_tuple(T_args&&... args)
{
initialize(std::forward<T_args>(args)...);
}
//Destruction
template<size_t T_index, typename T_val>
void destroy()
{
T_val* place = reinterpret_cast<T_val*>(&this->storage[index_array[T_index]]);
place->~T_val();
}
template<size_t T_index, typename T_val, typename T_val2, typename... T_vals_rest>
void destroy()
{
destroy<T_index, T_val>();
destroy<T_index+1, T_val2, T_vals_rest...>();
}
void destroy()
{
destroy<0, T_args...>();
}
~standard_layout_tuple()
{
destroy();
}
template<size_t T_index>
void set(T_param<T_index, T_args...>&& data)
{
T_param<T_index, T_args...>* ptr = reinterpret_cast<T_param<T_index, T_args...>*>(&this->storage[index_array[T_index]]);
*ptr = std::forward<T_param<T_index, T_args...>>(data);
}
template<size_t T_index>
T_param<T_index, T_args...>& get()
{
return *reinterpret_cast<T_param<T_index, T_args...>*>(&this->storage[index_array[T_index]]);
}
};
int main() {
standard_layout_tuple<float, double, int, double> sltuple{5.5f, 3.4, 7, 1.22};
sltuple.set<2>(47);
std::cout << sltuple.get<0>() << std::endl;
std::cout << sltuple.get<1>() << std::endl;
std::cout << sltuple.get<2>() << std::endl;
std::cout << sltuple.get<3>() << std::endl;
std::cout << "is standard layout:" << std::endl;
std::cout << std::boolalpha << std::is_standard_layout<standard_layout_tuple<float, double, int, double>>::value << std::endl;
return 0;
}
Live example: https://ideone.com/4LEnSS
There's a few things I'm not happy with:
This is not yet suitable for use as-is, really only treat it as a proof-of-concept in this state. I will probably come back to improve on some of these issues. Or, if anyone else can improve it, feel free to edit.