DarthRubik
DarthRubik

Reputation: 3985

Converting member function pointers in templates

Suppose I have the following two classes:

template<typename T>
struct Base
{
    void foo();
};

struct Derived : Base<Derived> {};

I can do this:

void (Derived::*thing)() = &Derived::foo; 

And the compiler is happy (as I would expect).

When I put this in two levels of templates suddenly it explodes:

template<typename T, T thing>
struct bar {};

template<typename T>
void foo()
{
    bar<void (T::*)(),&T::foo>{};
}

int main()
{
    foo<Derived>();  // ERROR
    foo<Base<Derived>>(); // Works fine
}

This fails with:

non-type template argument of type 'void (Base<Derived>::*)()' cannot be converted to a value of type 'void (Derived::*)()'

godbolt

Why does the simple case work and the more complicated one fail? I believe this is related to this question but am not entirely sure....

Upvotes: 6

Views: 159

Answers (3)

@YSC nailed the type of &Derived::foo;. Since you are wondering why this implicit conversion...

void (Derived::*thing)() = &Derived::foo; 

... flies normally but not in the template, the reason is as follows:

[temp.arg.nontype]

2 A template-argument for a non-type template-parameter shall be a converted constant expression of the type of the template-parameter.

[expr.const]

4 A converted constant expression of type T is an expression, implicitly converted to type T, where the converted expression is a constant expression and the implicit conversion sequence contains only

  • [...]

The list I omitted does not contain pointer to member conversions. Thus making that template argument invalid for the parameter you specify.


A trivial fix would be to use decltype(&T::foo) instead of void (T::*)() as the type argument. This is a well-formed replacement:

bar<decltype(&T::foo), &T::foo>{};

Whether or not is acceptable, is of course dependent on your use case, beyond the scope of the MCVE.

Upvotes: 6

Jonathan Mee
Jonathan Mee

Reputation: 38969

Note that this is going to be an error even without templates, for example if your classes were not templates:

struct Base {
    void foo();
};

struct Derived : Base {};

You still can't do foo<Base>(): https://ideone.com/MQE0ff


A possible alternative solution would involve, first for simplification, rather than taking 2 template parameters I'm going to have bar use the auto template parameter type:

template<auto thing>
struct bar{};

Next we'd need to implement is_specialization_of:

template<template<typename...> class T, typename U>
struct is_specialization_of : std::false_type {};

template<template<typename...> class T, typename... Ts> 
struct is_specialization_of<T, T<Ts...>> : std::true_type {};

Now we can rewrite foo to use is_specialization_of we can determine if we're passed a Base specialization or another class (which we presume derives from a Base specialization.)

template<typename T>
void foo()
{
    conditional_t<is_specialization_of<Base, T>::value, bar<&T::foo>, bar<&Base<T>::foo>>{};
}

I've expanded your example slightly to actually call thing in bar and just otherwise incorporated my suggestions. You can check that out here: https://coliru.stacked-crooked.com/a/2a33b8bd38896ff5

Upvotes: 0

YSC
YSC

Reputation: 40160

That's because &Derived::foo is in fact of type void (Base<Derived>::*)():

[expr.unary]/3

The result of the unary & operator is a pointer to its operand. The operand shall be an lvalue or a qualified-id. If the operand is a qualified-id naming a non-static or variant member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C​::​m.

Note the "member m of some class C with type T"... Terrible wording.

Upvotes: 4

Related Questions