如何在编译时确定元组元素的偏移量?

2022-02-24 00:00:00 tuples offset c++ member constexpr

我需要在编译时确定元组的某个索引元素的偏移量。

我尝试了此函数,从https://stackoverflow.com/a/55071840/225186(接近尾声)复制,

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))
    ;
}

包括我删除p并将&p替换为nullptr的变体。

此函数似乎在运行时运行良好,但我无法在编译时计算它。

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)
}

我理解这可能是因为reinterpret_cast不能在编译时完成。 但到目前为止,它基本上是被证明(在运行时)有效的唯一函数。

是否有办法以可在编译类型计算的方式重写此函数?

我在https://stackoverflow.com/a/55071840/225186开头也尝试过这些接近的列表,但它们都给出垃圾结果(至少在GCC中是这样),因为它们假定元组元素有一定的顺序,偏移量是通过按索引遍历索引并对齐字节来计算的。


解决方案

您可以使用此命令:

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;
    }
}

这避免了将_cast重新解释为字符指针,并且不应该有任何未定义的行为。

您还可以通过不初始化元组来处理不能在常量表达式中缺省构造的元组:

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;
    }
}

虽然此功能目前在GCC、Cang和MSVC中有效,但将来可能不会。

相关文章