c++c++20c++-conceptsrequires-clauserequires-expression

C++20 concepts compile-time pointer address requirement check


I'm trying to write a requirement to check that a struct has a member with a given name and that it comes first in the struct. The address of the first member of a struct (At least standard layout) should have the same address as the encompassing struct, with no padding at the beginning. But since pointers are not normally compile-time constants, I would expect this to not compile. But it all compiles although the requirement then incorrectly passes.

template<typename T>
concept has_first_member = requires(T &s) {
    (static_cast<void *>(&s) == static_cast<void *>(&s.next));
};

So given

struct mystruct{
   int first;
   int second;
   int next;
};

the requirement would incorrectly accept this as valid.

Can anyone explain the behavior?


Solution

  • A solution that does not instantiate a variable of the type you're checking uses offsetof:

    template <typename T>
    concept is_next_first = offsetof(T, next) == 0;
    
    struct next_is_first {
        int next;
        int previous;
        int something;
    };
    
    struct next_is_middle {
        int previous;
        int next;
        int something;
    };
    
    struct next_is_last {
        int previous;
        int something;
        int next;
    };
    
    static_assert(is_next_first<next_is_first>);
    static_assert(not is_next_first<next_is_middle>);
    static_assert(not is_next_first<next_is_last>);
    

    Below is my first answer

    You could use a constexpr function to enable compile-time checks on a temporary variable.

    struct next_is_first {
        int next;
        int previous;
        int something;
    };
    struct next_is_middle {
        int previous;
        int next;
        int something;
    };
    struct next_is_last{
        int previous;
        int something;
        int next;
    };
    
    template <typename T>
    constexpr bool check_next_is_first(const T& t) noexcept {
        return (void *)&t == &t.next;
    }
    
    template <typename T>
    concept is_next_first = check_next_is_first(T{});
    
    static_assert( is_next_first<next_is_first> );
    static_assert( not is_next_first<next_is_middle> );
    static_assert( not is_next_first<next_is_last> );
    

    Of course, this requires allocating and initializing the memory that an instance of the type you pass to is_next_first needs, and that may be costly.

    The concept is_next_first can be used at a function

    template <typename T>
    requires is_next_first<T>
    void do_something() { }
    
    int main() {
        do_something<next_is_first>();
        do_something<next_is_middle>(); // does not compile
        do_something<next_is_last>(); // does not compile
    }