Sunday, October 9, 2016

constexpr offsetof, a practical way to find the offset of a member in a constexpr

constexpr function to find the offset of a data member

I’ve been working on high performing code and needed to know at compile time whether the offset of a member from the beginning of its containing object is exactly a multiple of a SIMD vector size.

There exists the standard C-library macro offsetof swept into C++. For something as simple as a concrete structure and member, we can get its offset:

#include <cstddef>

struct ConcreteAggregate {
   double member1;
   int concreteMember;
};

static_assert(8 == offsetof(ConcreteAggregate, concreteMember), "");

However, offsetof is extremely limited, this slight change, of moving "member1" to a concrete base class, is already too much for offsetof:

#include <cstddef>

struct Base { double member1; };
struct Derived: Base { int concreteMember; };

static_assert(8 == offsetof(Derived, concreteMember);

Fails to compile. offsetof requires the type to be of standard layout, which for C++ metaprogramming already makes it useless.

Besides, the macro requires the name of the member. In templates you don’t know the name of the members, what you can have is its address as member pointer.

There’s a simple trick that would give the answer, if we had an object of the given type, we could compare its address to the address of the member:

template <typename T, typename MT, MT T::*MPtr>
std::size_t offset_of(const T &value) {
    return (char *)&(value.*MPtr) - (char *)&value;
}

std::size_t fun(const Derived &d) {
    return offset_of<Derived, int, &Derived::concreteMember>(d);
}

Will “work”, but alas, not in a constexpr. It turns out casting is forbidden in constexprs. Besides, that function is not really very useful since it requires a value of the given type. It should not require it, because what does it matter, for the purposes of determining the offset of a member, where is the value? – A null pointer can not be dereferenced, not even in a sort of un-evaluated context such as a constexpr.

I’ve been trying to find a way to solve this issue in an standard compliant way, and I am almost certain there is no way because there are no conversions allowed in standard C++ that would give us “char pointers” to calculate the bytes.

However, I think I have devised a portable solution: through a sleight of hand the code tricks the compiler into using rules for constant expressions of C++ 98 which allowed these casts. Because current compilers compile C++ 98, this trick in practice should work:

namespace detail {

template<typename T> struct declval_helper { static T value; };

template<typename T, typename Z, Z T::*MPtr>
struct offset_helper {
    using TV = declval_helper<T>;
    char for_sizeof[
        (char *)&(TV::value.*MPtr) -
        (char *)&TV::value
    ];
};

}

template<typename T, typename Z, Z T::*MPtr>
constexpr int offset_of() {
    return sizeof(detail::offset_helper<T, Z, MPtr>::for_sizeof);
}

The three keys in this code are:

  1. Using old-style constant expressions in the declaration of offset_helper::for_sizeof
  2. Creating our own declval equivalent since we want to use the value
  3. In the constexpr using sizeof