Search code examples
c++c++11gcctemplate-argument-deduction

return-value deduction for non-static method pointers


I was toying around with function pointers to non-static methods and stumbled upon some GCC-messages that don't quite make sense to me. Let's maybe look at some code:

#include<iostream>

struct X{  int print() const { std::cout << "hello"; return 0; } };
struct Y: public X{};

template<class T>
struct print_one
{
  const T& t;

  print_one(const T& _t): t(_t) {}

  template<class Something>
  void _call(Something (T::*fct)() const) const
  {
    std::cout << (t.*fct)() << '\n';
  }

  void do_print() const { _call(&T::print); }
};


int main()
{ 
  X x;
  Y y;
  print_one<X>(x).do_print();
  //print_one<Y>(y).do_print();
}

I was ready to see this fail because I thought the return value of a method does not contribute to its "ID" (EDIT: its signature) so to say. However, this compiles (gcc-9 --std=c++17) and works fine.

But, if I instanciate print_one with a Y (uncomment last line in main()) things go south:

test_ptr.cpp: In instantiation of 'void print_one<T>::do_print() const [with T = Y]':
test_ptr.cpp:28:28:   required from here
test_ptr.cpp:19:27: error: no matching function for call to 'print_one<Y>::_call(int (X::*)() const) const'
   19 |   void do_print() const { _call(&T::print); }
      |                           ^~~~~
test_ptr.cpp:14:8: note: candidate: 'template<class Something> void print_one<T>::_call(Something (T::*)() const) const [with Something = Something; T = Y]'
   14 |   void _call(Something (T::*fct)() const) const
      |        ^~~~~
test_ptr.cpp:14:8: note:   template argument deduction/substitution failed:
test_ptr.cpp:19:27: note:   mismatched types 'const Y' and 'const X'
   19 |   void do_print() const { _call(&T::print); }
      |                           ^~~~~

In particular with Something = Something seems weird to me. Further, the whole thing works if I explicitly give the template instanciation like so: _call<int>(&T::print).

So, the questions would be: why can GCC deduce the template argument Something although it's not part of the signature of the print method and why does the deduction fail when confronted with a class derived from the class defining the actual method?


Solution

  • It looks like you have two subtle factors working against you. First up, the conversion from int (X::*)() to int (Y::*)() is implicit, so often it can be used without a second thought. Often. But your error message mentions "argument deduction/substitution".

    During template argument deduction, implicit conversions are not considered. So, in this context, your provided argument of type int (X::*)() does not match the expected int (Y::*)(). (Whether or not this mismatch is correct is something I shall leave to others. It exists in gcc and clang.) If you were to explicitly provide a template argument, argument deduction would not be needed, and you would skip ahead to a step where implicit conversions are performed.

    The simple approach to providing a template argument is not suitable for your context, but it does provide a proof-of-concept.

    _call<int>(&T::print); // Specify that the return type must be `int`
    

    By specifying int, the template argument does not need to be deduced. However, this limits your options by largely defeating the point of your template. Fortunately, we can do better. We can tell the compiler how to "deduce" the template argument. (I used quotes because this is not the official template deduction step.)

    _call<decltype(t.print())>(&T::print); // The desired generic return type
    

    I thought the return value of a method does not contribute to its "ID" so to say.

    By "ID", you probably mean "signature"? It is true that a (non-template) function's signature does not include the return type, but you are dealing with types, not signatures. The type of a function pointer does include both the types of its parameters and its return type.