1

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

16
  • 1
    "why doesn't SFINAE prevent this" - because you haven't SFINAE'd the function away in your top example. Commented Jun 26, 2024 at 19:43
  • 2
    minimal reproducible example please, showing how you are instantiating the template. We're just guessing here. Commented Jun 26, 2024 at 19:44
  • 1
    @catnip, I've edited. Commented Jun 26, 2024 at 19:48
  • 1
    @trbabb That's because you didn't do what I did. Note: Field U::* member_ptr not Field T::* member_ptr Commented Jun 26, 2024 at 20:01
  • 1
    Ok. Well, that addresses the issue. Thank you! Commented Jun 26, 2024 at 20:04

1 Answer 1

2

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;
    }  
};
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for this! I understood the problem, but didn't consider the indirection hack shown here.
This is clarifying; thank you. In my mental model, any template object— including a template member function— isn't defined / "doesn't exist" until it's instantiated, and substitution only happens then. It sounds like there are "stages" of substitution, which is a subtle point I was not aware of!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.