Ad

How To Determine The Offset Of An Element Of A Tuple At Compile Time?

- 1 answer

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_casts 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.

Ad

Answer

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.

Ad
source: stackoverflow.com
Ad