4

Consider the following struct:

struct Particle
{
    float scale;
    float opacity;
    float rotation;
};

I want the following to compile:

static_assert(indexOf<&Particle::scale>() == 0);
static_assert(indexOf<&Particle::opacity>() == 1);
static_assert(indexOf<&Particle::rotation>() == 2);

I have managed to implement a working version using Boost.PFR:

template <auto PM>
consteval int indexOf()
{
    using ClassType = typename PMTraits<decltype(PM)>::ClassType;
    constexpr ClassType obj{};

    std::size_t result = -1;

    [&]<auto... Is>(std::index_sequence<Is...>)
    {
        (...,
            (
                (    static_cast<const void*>(&(boost::pfr::get<Is>(obj))) 
                == static_cast<const void*>(&(obj.*PM))                  ) 
                ? (result = Is) : 0
            )
        );    
    }(std::make_index_sequence<boost::pfr::tuple_size_v<ClassType>>{});

    return result;
}

But I am displeased by the fact that I need to create an object of type ClassType in order to make obj.*PM work.

Is there a better way of doing this that (1) does not require the creation of an object at compile-time and/or (2) is more efficient to compile?

4
  • Not really what you're asking for, and unlikely to be practically useful (yet), but I was curious how this can be done using P2996 from C++26. I got a working example, but seems like it also requires defining some partial specialization for getting class type from a member function pointer. godbolt.org/z/zMd3cWh7z Commented Sep 6, 2025 at 5:03
  • Couldn't you use a pointer to the type and just have it be null? (Would need to use ->* in that case), but this would not require an actual instance. Commented Sep 6, 2025 at 5:33
  • 1
    @SoronelHaetir dereference a nullptr is UB, which is not allowed during constant evaluation. Commented Sep 6, 2025 at 5:56
  • 2
    @ofo: I am trying to emulate basic reflection for AoS -> SoA transformation before C++26 (see x.com/supahvee1234/status/1963403310548812237). With C++26 reflection there are much better solutions not involving pointers to members :) Commented Sep 6, 2025 at 11:43

2 Answers 2

3

You can declare an extern variable to obtain constexpr reference without actually instantitate the object. Also explicitly cache std::array<void const*, N> ptrs<T> so boost::pfr::structure_tie is run only once per struct type.

https://godbolt.org/z/M6KEdbo3n

template <class T>
struct fake_object_wrapper { T const value; };

template <class T>
extern const fake_object_wrapper<T> fake_object;

template<class T>
constexpr auto ptrs = std::apply([](auto const&... ref) {
    return std::array{static_cast<void const*>(std::addressof(ref))...};
}, boost::pfr::structure_tie(fake_object<T>.value));

template<auto MemPtr>
constexpr size_t index_of = []<class T, class V>(V T::* p) {
    return std::find(ptrs<T>.begin(), ptrs<T>.end(),
        std::addressof(fake_object<T>.value.*p)) - ptrs<T>.begin();
}(MemPtr);

////

struct Particle {
    float scale;
    float opacity;
    float rotation;
};

static_assert(index_of<&Particle::scale> == 0);
static_assert(index_of<&Particle::opacity> == 1);
static_assert(index_of<&Particle::rotation> == 2);

Note that this will fail if you have [[no_unique_address]] members in your struct.

Sign up to request clarification or add additional context in comments.

6 Comments

How can this boost::pfr::structure_tie(fake_object<T>.value) work without an object being an actual thing?
Reference to an extern variable is usable as constant expression, see eel.is/c++draft/temp#arg.nontype-7. We never actually need the object at runtime, so we don't get linker error when linking the program.
Sure but std::addressof(fake_object<T>.value.*p)) actually uses the object, no?
We "used" it, but only during compile-time. The complied object file doesn't contain any references to fake_object, which makes the linker happy. You will get linker error if you actually use ptrs<T> value in non-constexpr context, e.g., godbolt.org/z/oGdrTWxve
Richard Smith replied on twitter with this link eel.is/c++draft/expr.const#17.sentence-1 " id-expressions ... that refer to an object or reference whose lifetime did not begin with the evaluation of E treated as referring to a specific instance of that object ... whose lifetime ... includes the entire constant evaluation" So - the standard requires the compiler to imagine an object here that we can use. Awesome! Thank you @Weeekly
Barry Revzin shared the proposal wg21.link/P2280
0

Obviously you should use reflection instead but this is the idea

https://godbolt.org/z/8Mzjx8K5Y

#include <tuple>
#include <type_traits>



template <std::size_t N, auto ptr, class T>
constexpr bool isCorrect(const T& t) {
    auto&&[... ref] = t;
    auto tuple = std::tie(ref...);
    auto* actual = std::addressof(t.*ptr);
    auto* expected = std::addressof(std::get<N>(tuple));
    if constexpr (std::is_same_v<decltype(actual), decltype(expected)>) {
        return actual == expected;
    } else {
        return false;
    }
}

template <std::size_t N, auto ptr, class T>
constexpr auto find_impl(const T& t) {
    if constexpr(isCorrect<N, ptr>(t)) {
        return std::cw<N>;
    } else {
        return find_impl<N+1, ptr>(t);
    }
}

template <auto Ptr> struct member_class;

template <class C, class M, M C::* Ptr>
struct member_class<Ptr> {
    using type = C;
};

template <auto ptr>
inline constexpr auto index_of = decltype(find_impl<0, ptr>(std::declval<typename member_class<ptr>::type>()))::value;


struct X {
    int a;
    double b;
    int c;
};

static_assert(index_of<&X::a> == 0);
static_assert(index_of<&X::b> == 1);
static_assert(index_of<&X::c> == 2);

How are you able to pass a runtime parameter in if constexpr

That is what P2280 about. We don’t care about the identity of the parameter, all objects of the type will produce the same results. This is DRed to all versions

what's std::cw

Just a short hand creating a wrapper into a type from a value. If you don’t have 26, replace it with integral_constant<size_t, N>{}

Is it all 26

The only thing that you can’t really do without 26 is the structured binding introducing a pack. But you can replace it with boost.pfr ( I did not check its implementation, but I guess it just try using structure bindings for N members and select one that succeed

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.