c++templateslanguage-lawyersfinaepointer-to-member

Template function accepting a pointer-to-member refuses to compile


Consider this class:

template <typename T>
struct Buffer {
    template <typename Field>
    int add_member_attribute(
            std::string&& name,
            Field T::* member_ptr)
    {
        // ...
        return 0;
    }  
};

struct Thing {
    int x;
    double y;
};

int main() {
    Buffer<Thing> bt;
    Buffer<float> bf;
    bt.add_member_attribute("y", &Thing::y);
}

If T is a non-class type, then the compiler complains:

buffer.h:313:22: error: member pointer refers into non-class type 'float'
            Field T::* member_ptr,
                    ^
fixture.h:78:22: note: in instantiation of template class 'Buffer<float>' requested here
        return f_real->items.data() + 32 * i;
                    ^

This is unexpected to me because no code is even trying to invoke the function when T is non-class. It seems like the error is being generated too early; why doesn't SFINAE prevent this?

Protecting add_member_attribute with a requires std::is_class_v<T> does not avoid the problem.

I've tried writing a helper class with a template specialization, including with deduction guides, in the hopes of using SFINAE to prevent the compiler from "seeing" the offending type expression:

template <typename Class, typename Field=void>
struct MemberPointer {
    template <typename U>
    MemberPointer(const U&) {} // no op
};

template <typename Class, typename Field>
requires std::is_class_v<Class>
struct MemberPointer<Class, Field> {
    using type = Field Class::*;
    
    type value;
    
    MemberPointer(type value): value(value) {}
    
    operator type() const {
        return value;
    }
};

// deduction guides
template <typename Class, typename Field>
MemberPointer(Field Class::* v) -> MemberPointer<Class, Field>;

template <typename T>
MemberPointer(T t) -> MemberPointer<T, void>;

template <typename T>
struct Buffer {
    template <typename Field>
    int add_member_attribute(
            std::string&& name,
            MemberPointer<T,Field> member_ptr)
    {
        // ...
    }  
};

Instead this prevents existing functions from matching the type:

cards.h:70:16: error: no matching member function for call to 'add_member_attribute'
        cards->add_member_attribute(PrimitiveVariable::BOUND, &Card::extents);
        ~~~~~~~^~~~~~~~~~~~~~~~~~~~
buffer.h:311:19: note: candidate template ignored: could not match 'MemberType<Card, Field>' against 'range2 behold::Card::*'
    AttributeInfo add_member_attribute(

How do I declare this function so that it is permitted for T to be a non-class type?

(I'm using Apple Clang 15.0.)


Solution

  • SFINAE applies to "direct" context, here

    template <typename T>
    struct Buffer {
        template <typename Field>
        int add_member_attribute(
                std::string&& name,
                Field T::* member_ptr)
        {
            // ...
            return 0;
        }  
    };
    

    T is already fixed, and so

    template <typename Field>
    int add_member_attribute(
            std::string&& name,
            Field float::* member_ptr) // WRONG
    {
        // ...
        return 0;
    }  
    

    is wrong for any Field.

    As often, extra indirection helps:

    template <typename T>
    struct Buffer {
        template <typename Field,
                  typename U = T, // To allow SFINAE
                  std::enable_if_t<std::is_same_v<T, U>, bool> = false> // Enforce that U == T
        int add_member_attribute(
                std::string&& name,
                Field U::* member_ptr)
        {
            // ...
            return 0;
        }  
    };