Under what circumstances does the standard require that T
has exactly the same size and alignment as std::aligned_storage<sizeof(T), alignof(T)>
?
I thought the answer was, always, but from other SO answers today, I learned that for some obscure T
, it may be implementation defined. If the implementation supports extended alignment types, like 128 bit integers, it is up to the implementation whether std::aligned_storage
will end up having that 128 bit alignment, if I understood correctly.
Are there other types T
where the answer to the question may be implementation-defined?
In my application, I basically have a tuple std::tuple<T, U, V>
of unknown types, and I want to be able to get the offsets of the members T
, U
, V
within this tuple as efficiently as possible, ideally at compile-time.
(I realize that this is a very strange thing to want. I'm trying to fix up some existing code where a tuple is being overlaid on top of a "layout-compatible" type using reinterpret_cast
. That kind of cast between unrelated objects is illegal and violates strict aliasing rules. If I can get the offsets of the members instead, then I can factor out the illegal cast.)
AFAIK, this can't quite be done within C++14 constexpr
. But it can come pretty close, my code looks like this:
#include <type_traits>
#include <tuple>
#include <utility>
#include <memory>
template <typename T>
struct tuple_of_aligned_storage;
template <typename... Ts>
struct tuple_of_aligned_storage<std::tuple<Ts...>> {
using type = std::tuple<std::aligned_storage_t<sizeof(Ts), alignof(Ts)>...>;
};
template <typename T>
using tuple_of_aligned_storage_t = typename tuple_of_aligned_storage<T>::type;
template <typename S>
struct offset_helper {
// Get offset of idx'th member
template <std::size_t idx>
static std::ptrdiff_t offset() {
constexpr tuple_of_aligned_storage_t<S> layout{};
// TODO: Do modern compilers optimize `layout` object out of the binary?
// If so, this call is probably inlined.
return reinterpret_cast<const char *>(&std::get<idx>(layout)) - reinterpret_cast<const char *>(&layout);
}
};
int main() {
using T = std::tuple<int, float, double>;
return offset_helper<T>::offset<0>(); // offset of 0'th member
}
In experiments, most modern compilers optimize this through to a constant, even though there are reinterpret casts involved and such, and the offset
function can't quite be marked constexpr
.
The point of using std::aligned_storage
here is, even if the members of the tuple are not default constructible, have nontrivial initialization, etc. replacing them with std::aligned_storage
makes them trivially constructible without changing the layout of the tuple.
But, if for some types, swapping the type for std::aligned_storage
changes the size or alignment, that can break my whole approach and lead to the wrong offsets being calculated.