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
Есть несколько вещей, которые мне не нравятся:
- Выравнивание не обрабатывается должным образом, что означает, что ввод смещенных типов в настоящее время дает вам поведение undefined
- Пока не представлена вся функциональность кортежа.
- Я не думаю, что управление памятью в настоящее время безопасно.
- Он использует кортеж для определения типа для каждого индекса.
- Общее качество кода является бесполезным.
- Там могут быть лучшие, более сжатые способы обработки некоторых рекурсивных функций шаблона.
- Я не совсем понимаю все, что я сделал. Я понимаю основные основы, но я использую некоторые из более сильного синтаксиса по доброй воле. Здесь наиболее важные источники, которые я использовал:
Это еще не подходит для использования как есть, на самом деле только рассматривайте его как доказательство концепции в этом состоянии. Вероятно, я вернусь, чтобы улучшить некоторые из этих проблем. Или, если кто-то еще может его улучшить, не стесняйтесь редактировать.
Ответ 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
(не проверялся для точных условий).