Search code examples
c++tuplesoffsetconstexprmember

How to determine the offset of an element of a tuple at compile time?


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.


Solution

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