std::tuple 和标准布局

17
如果std::tuple的所有成员都是标准布局类型,那么std::tuple本身是否是标准布局?存在用户定义的复制构造函数会使其非平凡,但我想知道它是否仍然可以是标准布局。 规范中的一句引语将是有用的。

如果您想了解这个问题是因为您想进行优化,那么您应该使用 std::is_standard_layout 并进行编译时分支。然后您可以放心地知道自己在进行优化而不必了解类型本身的所有细节。 - GManNickG
看起来应该是这样,但我在涵盖“元组”的标准部分中找不到任何关于“标准布局”的提及。可能在其他地方有提及,但如果有的话,我还没有找到。 - Jerry Coffin
4个回答

12

不,标准布局要求所有非静态数据成员属于一个基类子对象或直接属于最派生的类型。而且std::tuple的典型实现会为每个基类实现一个成员。

由于成员声明不能是pack expansion,在上述要求下,标准布局的tuple不能有多个成员。一个实现仍然可以通过将所有tuple "成员"存储在一个char[]中,并通过reinterpret_cast获取对象引用来规避此问题。元程序必须生成类布局,特殊成员函数必须重新实现。这将会相当麻烦。


2
典型的实现方式,没错,但并不是“好”的实现方式(根据 Howard 的说法):-P;我怀疑他们可能要求只有标准布局的类型元组才能是标准布局... - ildjarn
1
我认为用户代码(编辑:此处我进行了标准修正)是不可能的,但是标准库允许使用魔法。(编译器可以按需在内部生成类。)无论如何,这个答案基本上沿着正确的方向:标准库实现允许从任何他们想要的东西派生,只要它有一个保留的名称。而那个“任何”可以有虚函数、成员、混合访问成员等。(§17.6.5.11) - GManNickG
@MatthieuM。好的,那么这与我的建议完全不同。我想我应该实现它并获得功劳:v)。(可能不会发生。由于元编程需要在char[]内生成正确对齐的布局,因此实现将非常困难。而且我的建议只适用于POD成员,以允许简单地复制字节数组。) - Potatoswatter
1
@Potatoswatter:既然我们在谈论C++11,你可以使用std::aligned_storage的组合来使块适当地对齐,并使用constexpr函数来给出每个成员的偏移量。但是,你将无法获得EBO。 - Matthieu M.
@Potatoswatter,你在使用基于char[]的实现中的reinterpret_cast是否会违反(strict-)别名规则?有没有标准的方法来解决这个问题?(我问这个问题是因为我已经完成了这个实现,但后来意识到由于别名规则而存在问题...) - mitchnull
显示剩余3条评论

5

受PotatoSwatter答案的启发,我花了一整天时间为C++14创建了一个标准布局元组。

这段代码实际上可以工作,但目前不适合使用,因为它涉及到未定义的行为。请将其视为概念验证。 以下是我最终得出的代码:

#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;
}

实时示例: https://ideone.com/4LEnSS

以下是我不满意的几点:

这还不适合直接使用,只能将其视为概念验证。我可能会回来改进其中一些问题。或者,如果有人可以改进它,可以随意编辑。


1

std::tuple 不能是标准布局的一个原因,与任何具有成员和基类成员的类一样,是因为标准允许在派生即使是非空基类时进行空间优化。例如:

#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));
}

输出:

sizeof(X) is 16
sizeof(Y) is 2
sizeof(XY) is 16

以上显示标准允许在派生类成员中使用类尾填充。类XY有两个额外的uint16_t成员,然而它的大小等于基类X的大小。
换句话说,类XY的布局与另一个没有基类且所有XY成员按地址排序的类相同,例如struct XY2 { uint64_t a; uint32_t b; uint16_t c; uint16_t d; };
导致它不符合标准布局的原因是派生类的大小不是基类和派生类成员大小的函数。
请注意,struct/class的大小是其具有最大对齐要求的成员的对齐倍数。因此,对象数组适当地对齐以适应这样的成员。对于内置类型,通常sizeof(T) == alignof(T)。因此,sizeof(X)sizeof(uint64_t)的倍数。
我不确定标准是否需要对struct进行特殊处理,但是使用g++-5.1.1,如果将class替换为struct,上述代码会产生不同的输出结果:
sizeof(X) is 16
sizeof(Y) is 2
sizeof(XY) is 24

换句话说,当涉及到struct时,不会使用尾部填充空间优化(没有测试确切条件)。

0

“列表”方法可用于获取标准布局的元组(以下示例存在一些不准确之处,但演示了这个想法):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接