Std:: кортеж и стандартная компоновка

Если все члены std::tuple имеют стандартные типы макета, является ли это std::tuple сам стандартный макет? Наличие пользовательского конструктора-копии делает его нетривиальным, но мне было интересно, может ли он по-прежнему быть стандартным.

Цитата из спецификации будет хорошей.

Ответ 1

Нет, стандартная компоновка требует, чтобы все нестатические члены данных принадлежали либо одному базовому подобъекту, либо непосредственно к самому производному типу, а типичные реализации std::tuple реализовали один член на базовый класс.

Поскольку декларация участника не может быть расширением пакета, в свете вышеизложенного требования, стандартный макет tuple не может иметь более одного члена. Реализация по-прежнему может обойти проблему, сохранив все tuple "члены" внутри одного char[] и получив ссылки на объекты с помощью reinterpret_cast. Метапрограмма должна была бы генерировать макет класса. Специальные функции-члены должны быть переопределены. Было бы больно.

Ответ 2

Вдохновленный ответом от PotatoSwatter, я посвятил свой день созданию стандартного макета для С++ 14.

Код действительно работает, , но в настоящее время он не подходит для использования, поскольку он включает в себя поведение undefined. Рассматривайте это как доказательство концепции. Здесь код, в который я попал:

#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

Есть несколько вещей, которые мне не нравятся:

Это еще не подходит для использования как есть, на самом деле только рассматривайте его как доказательство концепции в этом состоянии. Вероятно, я вернусь, чтобы улучшить некоторые из этих проблем. Или, если кто-то еще может его улучшить, не стесняйтесь редактировать.

Ответ 3

Одна из причин 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; };.

Что делает его нестандартным макетом, так это то, что размер производного класса не является функцией размеров базовых классов и членов производного класса.

Обратите внимание, что размер a 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 (не проверялся для точных условий).