4

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.

alfC
  • 14,261
  • 4
  • 67
  • 118

1 Answers1

4

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.

Artyer
  • 31,034
  • 3
  • 47
  • 75
  • For the second version, I think it has UB, because of [\[class.cdtor\]/1](https://eel.is/c++draft/class.cdtor#1), which doesn't allow forming a pointer to members of the tuple. – user17732522 Jan 10 '22 at 04:47
  • @user17732522 Yes the second one is UB. It's just the kind of UB that's difficult to detect during constexpr evaluation, so most compilers don't, which is why I've added the warning that it might not work. – Artyer Jan 10 '22 at 04:58
  • Genius, it works. https://godbolt.org/z/ahe1Md6oM – alfC Jan 10 '22 at 05:09
  • 1
    @alfC Watch out that the version you've linked doesn't compile with clang since it has bounds checking on pointer arithmetic (fixed with `char a[1];` -> `char a[sizeof(Tuple)];`) – Artyer Jan 10 '22 at 05:19
  • yes, now I fixed that. Thank you, I added some syntactic sugar here: https://godbolt.org/z/xq5Pnf6jq – alfC Jan 10 '22 at 05:43
  • Unfortunately, this doesn't work on NVCC, it even goes out of the bounds of `a` char array: https://cuda.godbolt.org/z/fKTYMbGcd . And in Intel compiler it doesn't create a constexpr value https://godbolt.org/z/b4Ef8G5Kq – alfC Jan 10 '22 at 07:53
  • I tried to use recursion with NVCC and Intel but failed, https://godbolt.org/z/YTbf4r71h . I think there is something more fundamental, not able to compare constexpr pointers? – alfC Jan 10 '22 at 08:12
  • 1
    @alfC As discussed above, the second version technically shouldn't work, but even with the first one icc and nvcc complain about the destructor call. That is correct since they are set to C++17, where the explicit call is not allowed. But in C++17 the whole thing works only with trivially destructible element types anyway, so it can be removed. But then they only work if the target element type is `char`. It seems they assume that the pointers can't be equal if the types differ, but that is wrong since `==` is defined to compare addresses, not pointer values. Seems a bug in icc/nvcc to me. – user17732522 Jan 11 '22 at 01:57
  • 1
    @Artyer I noticed now that you don't use the `off` variable in your code. Also, I think you need to define a destructor for the union if this is supposed to be usable with non-trivially-destructible types in C++20. Otherwise the union destructor is still called, but deleted. If it is only supposed to be workable with trivially-destructible types, then the destructor call is not needed. – user17732522 Jan 11 '22 at 02:06
  • @user17732522, exactly, none of the versions work in nvcc and icpc (I think they use the same EDG frontend). Maybe the compile assumes pointers can't be equal if the types differ because of aliasing rules? Maybe there is a compiler option to disable that (it is hard to find the compiler options for these compilers). About `off`, yes, I think Artyer wrote the loop as while initially, I simplified that in my code. – alfC Jan 11 '22 at 02:51
  • 1
    @alfC Aliasing rules don't matter to pointer comparison. `==` should only compare addresses, except if one pointer is one-past-the-end of a complete object. Really seems just a bug to me. – user17732522 Jan 11 '22 at 02:57