I need to determine the offset of a certain indexed element of a tuple at compile time.
I tried this function, copied from https://stackoverflow.com/a/55071840/225186 (near the end),
template <std::size_t I, typename Tuple>
constexpr std::ptrdiff_t element_offset() {
Tuple p;
return
(char*)(&std::get<I>(*static_cast<Tuple *>(&p)))
- (char*)(static_cast<Tuple*>(&p))
;
}
including variants in which I eliminate p
and replace &p
by nullptr
.
This function seems to work well at runtime but I cannot evaluate it at compile time.
https://godbolt.org/z/MzGxfT1cc
int main() {
using Tuple = std::tuple<int, double, int, char, short, long double>;
constexpr std::size_t index = 3;
constexpr std::ptrdiff_t offset = element_offset<index, Tuple>(); // ERROR HERE, cannot evaluate constexpr context
Tuple t;
assert(( reinterpret_cast<char*>(&t) + offset == reinterpret_cast<char*>(&std::get<index>(t)) )); // OK, when compiles (without "constexpr" offset)
}
I understand this is probably because the reinterpret_cast
s cannot be done at compile time.
But so far it is basically the only function that proved to work (at runtime).
Is there a way to rewrite this function in a way that can be evaluated at compile type?
I also tried these approached list at the beginning of https://stackoverflow.com/a/55071840/225186, but they all give garbage results (at least in GCC) because they assume a certain ordering of the tuple elements and the offset are calculated by "walking" index by index and aligning bytes.
You can use this:
template <std::size_t I, typename Tuple>
constexpr std::size_t element_offset() {
using element_t = std::tuple_element_t<I, Tuple>;
static_assert(!std::is_reference_v<element_t>);
union {
char a[sizeof(Tuple)];
Tuple t{};
};
auto* p = std::addressof(std::get<I>(t));
t.~Tuple();
std::size_t off = 0;
for (std::size_t i = 0;; ++i) {
if (static_cast<void*>(a + i) == p) return i;
}
}
Which avoids having to reinterpret_cast to a char pointer, and shouldn't have any undefined behaviour.
You can also make this work with tuples that can't be default constructed in a constant expression by not initializing the tuple:
template <std::size_t I, typename Tuple>
constexpr std::size_t element_offset() {
using element_t = std::tuple_element_t<I, Tuple>;
static_assert(!std::is_reference_v<element_t>);
union u {
constexpr u() : a{} {} // GCC bug needs a constructor definition
char a[sizeof(Tuple)]{};
Tuple t;
} x;
auto* p = std::addressof(std::get<I>(x.t));
std::size_t off = 0;
for (std::size_t i = 0;; ++i) {
if (static_cast<void*>(x.a + i) == p) return i;
}
}
While this works in gcc, clang, and msvc today, it might not in the future.