简体   繁体   中英

Template argument deduction for inherited member function

I struggle with the following issue but are somehow too blinded to see the (probably obvious) solution:

I'm writing a class to store invocables with a specific signature (which is given as template arguments to that class). For my MCVE, I left this part out and just assume void() because this part is not the issue.

So, to register an invocable, I use something like this:

template <typename F>
void call(F &&func)
{
  std::cout << "call(f()):\n";
  func();
}

(For the MCVE, I replaced storage by just calling the invocable.)

For convenience, I added a second flavor:

template <typename C>
void call(C *pObj, void(C::*pFunc)())
{
  std::cout << "call(C *pObj, void(C::*pFunc)()):\n";
  (pObj->*pFunc)();
}

to cover objects with member function (matching the required signature).

This works but only if the member function is defined in the class of the given object itself. It fails if the member function is inherited.

This probably has something to do with templates and inheritance (where the latter is not resolvable while templates are compiled).

Complete MCVE:

#include <iostream>

template <typename F>
void call(F &&func)
{
  std::cout << "call(f()):\n";
  func();
}

template <typename C>
void call(C *pObj, void(C::*pFunc)())
{
  std::cout << "call(C *pObj, void(C::*pFunc)()):\n";
  (pObj->*pFunc)();
}

// Test:

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

void test() { std::cout << "test()\n"; }

struct TestBase {
  void test() { std::cout << "TestBase::test()\n"; }
};

struct Test: TestBase {
#ifdef FIX
  void test() { TestBase::test(); }
#endif // FIX
};

int main()
{
  DEBUG(call(&test));
  DEBUG(Test obj);
  DEBUG(call((TestBase*)&obj, &TestBase::test));
  DEBUG(call([&]() { obj.test(); }));
  DEBUG(call<Test>(&obj, &Test::test));
  DEBUG(call(&obj, &Test::test)); // <== PROBLEM! Doesn't work without -DFIX. :-(
}

Live Demo on coliru

Instead of defining the wrapper function Test::test() , I probably could add something to template <typename C> void call(C*, void(C::*)()) to make this working but I cannot imagine what.

Needless to say that template s are not just my strength…

I already had a look at std::invoke hoping to find inspiration there but this didn't help as well.


Trivia:

After having struggled with this from Sunday evening until Tuesday morning, I got 3 working solutions in less than 10 minutes. That's impressive…

The problem is that if Test doesn't have test , the type of &Test::test would be void(TestBase::*)() . Then the template deduction fails because the type deduced for template parameter C from the 1st and 2nd function argument conflicts.

As another solution, you can exclude the 2nd argument pFunc from deduction, with the help of std::type_identity (since C++20) (which takes advantage of non-deduced context ).

template <typename C>
void call(C *pObj, void(std::type_identity_t<C>::*pFunc)())
{
  std::cout << "call(C *pObj, void(C::*pFunc)()):\n";
  (pObj->*pFunc)();
}

Issue with inherited member is that you might have deduction as

void call(Derived *pObj, void(Base::*pFunc)())

So doesn't match your signature.

You might fix it with

template <typename C, typename C2>
void call(C *pObj, void(C2::*pFunc)())
{
  std::cout << "call(C *pObj, void(C::*pFunc)()):\n";
  (pObj->*pFunc)();
}

possibly with SFINAE

or directly

template <typename C, typename Member>
auto call(C *pObj, Member m) -> decltype((pObj->*m)(), void())
{
  std::cout << "call(C *pObj, void(C::*pFunc)()):\n";
  (pObj->*m)();
}

Demo

After having received two solutions, I was spoiled for choice.

Two justify this, I considered the case of an editors mistake (as I remembered with scare of the flood of complaints the compiler spits out sometimes for the slightest typos in combination with templates).

The two most common mistakes which came in my mind:

  1. typo in member function name
  2. member function with wrong signature used.

For this I made another MCVE which I tested against MSVC (my primary platform), and g++ and clang (possible alternatives, used for cross-checking the code):

#include<iostream>

template <typename C, typename CF>
void call1(C *pObj, void(CF::*pFunc)())
{
  (pObj->*pFunc)();
}

template< class T >
struct type_identity {
    using type = T;
};

template< class T >
using type_identity_t = typename type_identity<T>::type;

template <typename C>
void call2(C *pObj, void(type_identity_t<C>::*pFunc)())
{
  (pObj->*pFunc)();
}

// Test:

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

void test() { std::cout << "test()\n"; }

struct TestBase {
  void test() { std::cout << "TestBase::test()\n"; }
  void other(int) { std::cout << "TestBase::other()\n"; }
};

struct Test: TestBase {
};

int main()
{
  Test obj;
  // wrong member function
  call1(&obj, &Test::testit);
  call2(&obj, &Test::testit);
  // member function with wrong signature
  call1(&obj, &Test::other);
  call2(&obj, &Test::other);
}

All three compilers made complaints that I consider as amazingly compact and clean.

MSCV 19.27:

<source>(41): error C2039: 'testit': is not a member of 'Test'
<source>(34): note: see declaration of 'Test'
<source>(41): error C2065: 'testit': undeclared identifier
<source>(42): error C2039: 'testit': is not a member of 'Test'
<source>(34): note: see declaration of 'Test'
<source>(42): error C2065: 'testit': undeclared identifier
<source>(44): error C2672: 'call1': no matching overloaded function found
<source>(44): error C2784: 'void call1(C *,void (__cdecl CF::* )(void))': could not deduce template argument for 'void (__cdecl CF::* )(void)' from 'void (__cdecl TestBase::* )(int)'
<source>(4): note: see declaration of 'call1'
<source>(45): error C2664: 'void call2<Test>(C *,void (__cdecl Test::* )(void))': cannot convert argument 2 from 'void (__cdecl TestBase::* )(int)' to 'void (__cdecl Test::* )(void)'
        with
        [
            C=Test
        ]
<source>(45): note: Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
<source>(18): note: see declaration of 'call2'

g++ 10.2:

<source>: In function 'int main()':
<source>:41:22: error: 'testit' is not a member of 'Test'
   41 |   call1(&obj, &Test::testit);
      |                      ^~~~~~
<source>:42:22: error: 'testit' is not a member of 'Test'
   42 |   call2(&obj, &Test::testit);
      |                      ^~~~~~
<source>:44:27: error: no matching function for call to 'call1(Test*, void (TestBase::*)(int))'
   44 |   call1(&obj, &Test::other);
      |                           ^
<source>:4:6: note: candidate: 'template<class C, class CF> void call1(C*, void (CF::*)())'
    4 | void call1(C *pObj, void(CF::*pFunc)())
      |      ^~~~~
<source>:4:6: note:   template argument deduction/substitution failed:
<source>:44:27: note:   candidate expects 1 argument, 2 provided
   44 |   call1(&obj, &Test::other);
      |                           ^
<source>:45:15: error: cannot convert 'void (TestBase::*)(int)' to 'void (Test::*)()'
   45 |   call2(&obj, &Test::other);
      |               ^~~~~~~~~~~~
      |               |
      |               void (TestBase::*)(int)
<source>:18:21: note:   initializing argument 2 of 'void call2(C*, void (type_identity<T>::type::*)()) [with C = Test; typename type_identity<T>::type = Test; type_identity_t<C> = Test]'
   18 | void call2(C *pObj, void(type_identity_t<C>::*pFunc)())
      |                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

clang 11.0.0:

<source>:41:16: error: no member named 'testit' in 'Test'; did you mean '::Test::test'?
  call1(&obj, &Test::testit);
               ^~~~~~~~~~~~
               ::Test::test
<source>:30:8: note: '::Test::test' declared here
  void test() { std::cout << "TestBase::test()\n"; }
       ^
<source>:42:16: error: no member named 'testit' in 'Test'; did you mean '::Test::test'?
  call2(&obj, &Test::testit);
               ^~~~~~~~~~~~
               ::Test::test
<source>:30:8: note: '::Test::test' declared here
  void test() { std::cout << "TestBase::test()\n"; }
       ^
<source>:44:3: error: no matching function for call to 'call1'
  call1(&obj, &Test::other);
  ^~~~~
<source>:4:6: note: candidate template ignored: failed template argument deduction
void call1(C *pObj, void(CF::*pFunc)())
     ^
<source>:45:3: error: no matching function for call to 'call2'
  call2(&obj, &Test::other);
  ^~~~~
<source>:18:6: note: candidate function [with C = Test] not viable: no known conversion from 'void (TestBase::*)(int)' to 'void (type_identity_t<Test>::*)()' for 2nd argument
void call2(C *pObj, void(type_identity_t<C>::*pFunc)())
     ^

Live Demo on Compiler Explorer


The last issue, I did consider – something which I noticed with Qt5 signals where qOverload is provided for:

Are these solutions capable to select the right member function out of multiple candidates with distinct signatures:

#include<iostream>

template <typename C, typename CF>
void call1(C *pObj, void(CF::*pFunc)())
{
  (pObj->*pFunc)();
}

template< class T >
struct type_identity {
    using type = T;
};

template< class T >
using type_identity_t = typename type_identity<T>::type;

template <typename C>
void call2(C *pObj, void(type_identity_t<C>::*pFunc)())
{
  (pObj->*pFunc)();
}

// Test:

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

void test() { std::cout << "test()\n"; }

struct TestBase {
  void test() { std::cout << "TestBase::test()\n"; }
  void test(int) { std::cout << "TestBase::test(int)\n"; }
};

struct Test: TestBase {
};

int main()
{
  Test obj;
  // choose member function with right signature
  call1(&obj, &Test::test);
  call2(&obj, &Test::test);
}

Yes.

MSVC v19.27:

Compiler returned: 0

g++ 10.2:

Compiler returned: 0

clang 11.0.0:

Compiler returned: 0

Live Demo on Compiler Explorer

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM