17

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-constructor makes it non-trivial, but I was wondering if it can still be standard layout.

A quote from the spec would be good.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • If you want to know this because you'd like an optimization opportunity, you should use `std::is_standard_layout` and take a compile-time branch. Then you can rest knowing that you're being optimal without knowing all the details of the type itself. – GManNickG Mar 23 '12 at 03:18
  • It seems like it would, but I can't find any mention of `standard layout` in the section of the standard that covers `tuple`. There might be a mention somewhere else, but if so I haven't found it yet. – Jerry Coffin Mar 23 '12 at 03:21

4 Answers4

12

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.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • 2
    Typical implementations, yes, but not _good_ implementations ([according to Howard](http://stackoverflow.com/a/9643480/636019) :-P); I suspect they _could_ have required that tuples of standard-layout types only be standard layout... – ildjarn Mar 23 '12 at 03:11
  • 1
    I think it's impossible for user code (edit: I standard corrected here), but the standard library is allowed to use magic. (The compiler could generate classes as needed internally.) In any case, this answer is pretty much along the right vein: standard library implementations are allowed to derive from whatever they want, as long as it has a reserved name. And that 'whatever' could have virtual functions, members, mixed-access members, etc. (§17.6.5.11) – GManNickG Mar 23 '12 at 03:15
  • @Potatoswatter: having dug the implementation by Howard, he only avoided recursion, but not inheritance. `template struct __tuple_impl<__tuple_indices<_Indx...>, _Tp...> : public __tuple_leaf<_Indx, _Tp>...`. So, his tuple implementation has a single member of type `__tuple_impl` which itself inherits from one leaf for each element. He gets the best of both worlds: Empty Base Optimization and constant template depth. However, still **not standard layout**. – Matthieu M. Mar 23 '12 at 08:42
  • @MatthieuM. Okay, then that's completely different from my proposal. I guess I should implement it and take credit then :v) . (Probably not gonna happen. The implementation would be pretty hairy as a metaprogram would have to generate a properly aligned layout within the `char[]`. And my proposal is most practical for POD members only, to allow simply copying the byte array.) – Potatoswatter Mar 23 '12 at 09:10
  • @ildjarn According to MatthieuM's interpretation, Howard's implementation still "implements one member per base class"… it's really no different in this respect, since recursion isn't the issue. – Potatoswatter Mar 23 '12 at 09:12
  • 1
    @Potatoswatter: since we are talking C++11, you could be using a combination of `std::aligned_storage` to have a block suitably aligned to begin with and `constexpr` functions to give the offset of each member. However, you would not get EBO. – Matthieu M. Mar 23 '12 at 10:10
  • @Potatoswatter wouldn't the `reinterpret_cast` in your `char[]`-backed implementation break (strict-)aliasing rules? Is there a standard way of working around that issue? (I ask because I have that implementation done already, but then I realized it's problematic due the the aliasing rules...) – mitchnull Jun 08 '12 at 09:59
  • @mitchnull No, not only are you allowed to access any object as a sequence of `char`, but there is no aliasing at all here because such access isn't performed. You're using `char[]` as backing storage, but each byte is only ever accessed via one type. (It's not like a `union`.) – Potatoswatter Jun 09 '12 at 04:10
5

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.

Community
  • 1
  • 1
Aberrant
  • 3,423
  • 1
  • 27
  • 38
1

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).

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
0

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)...};
}
Alex
  • 612
  • 3
  • 9