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. :-(
}
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)();
}
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:
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
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.