I made the following function a member of my class:
template <typename... _Types>
void NotifyAllDelayed(_Types&&... _Args)
{
delay_runner->Add([=, this] { this->NotifyAll<_Types...>(std::forward<_Types>(_Args)...); });
}
I created this, because I wanted to keep myself from repeating:
delay_runner->Add([=, ¬ifications] { notifications->NotifyAll(specific_listener_ptr, &ISpecificUserDataListener::OnSpecificUserDataUpdated, 2022); });
// and
delay_runner->Add([=, ¬ifications] { notifications->NotifyAll(&ISpecificUserDataListener::OnSpecificUserDataUpdated, 3033); });
So I can write:
notifications->NotifyAllDelayed(specific_listener_ptr, &ISpecificUserDataListener::OnSpecificUserDataUpdated, 9);
notifications->NotifyAllDelayed(&ISpecificUserDataListener::OnSpecificUserDataUpdated, 3033);
But then I get these errors (MSVC, C 20 enabled):
Build started...
1>------ Build started: Project: Forward, Configuration: Debug x64 ------
1>Forward.cpp
1> Forward.cpp(133,65): error C2665: 'std::forward': none of the 2 overloads could convert all the argument types
1>C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.33.31629\include\type_traits(1416,28): message : could be '_Ty (__cdecl ISpecificUserDataListener::* &&std::forward<void(__cdecl ISpecificUserDataListener::* )(int)>(void (__cdecl ISpecificUserDataListener::* &&)(int)) noexcept)(int)'
1> with
1> [
1> _Ty=void (__cdecl ISpecificUserDataListener::* )(int)
1> ]
1>C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.33.31629\include\type_traits(1410,28): message : or '_Ty (__cdecl ISpecificUserDataListener::* &&std::forward<void(__cdecl ISpecificUserDataListener::* )(int)>(void (__cdecl ISpecificUserDataListener::* &)(int)) noexcept)(int)'
1> with
1> [
1> _Ty=void (__cdecl ISpecificUserDataListener::* )(int)
1> ]
1> Forward.cpp(133,65): message : '_Ty (__cdecl ISpecificUserDataListener::* &&std::forward<void(__cdecl ISpecificUserDataListener::* )(int)>(void (__cdecl ISpecificUserDataListener::* &)(int)) noexcept)(int)': cannot convert argument 1 from 'void (__cdecl ISpecificUserDataListener::* const )(int)' to 'void (__cdecl ISpecificUserDataListener::* &)(int)'
1> with
1> [
1> _Ty=void (__cdecl ISpecificUserDataListener::* )(int)
1> ]
1> Forward.cpp(133,81): message : Conversion loses qualifiers
1>C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.33.31629\include\type_traits(1410,28): message : see declaration of 'std::forward'
1> Forward.cpp(132,1): message : while trying to match the argument list '(void (__cdecl ISpecificUserDataListener::* const )(int))'
1> Forward.cpp(178): message : see reference to function template instantiation 'void NotificationManager::NotifyAllDelayed<void(__cdecl ISpecificUserDataListener::* )(int),int>(void (__cdecl ISpecificUserDataListener::* &&)(int),int &&)' being compiled
1> Forward.cpp(133,37): error C2672: 'NotificationManager::NotifyAll': no matching overloaded function found
1> Forward.cpp(116,7): message : could be 'bool NotificationManager::NotifyAll(T *,_Fx &&,_Types &&...)'
1> Forward.cpp(133,37): message : 'bool NotificationManager::NotifyAll(T *,_Fx &&,_Types &&...)': expects 3 arguments - 1 provided
1> Forward.cpp(116): message : see declaration of 'NotificationManager::NotifyAll'
1> Forward.cpp(105,7): message : or 'bool NotificationManager::NotifyAll(_Fx &&,_Types &&...)'
1>Done building project "Forward.vcxproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
I tried coliru with the same code but it gives another error:
main.cpp: In instantiation of 'void NotificationManager::NotifyAllDelayed(_Types&& ...) [with _Types = {void (ISpecificUserDataListener::*)(int), int}]':
main.cpp:168:33: required from here
main.cpp:133:95: error: binding reference of type 'void (ISpecificUserDataListener::*&)(int)' to 'void (ISpecificUserDataListener::* const)(int)' discards qualifiers
133 | delay_runner->Add([=, this] { this->NotifyAll<_Types...>(std::forward<_Types>(_Args)...); });
What does this error mean?
How can I fix it?
My other templated function with variadic template arguments that is more complicated (NotifyAll) works just fine:
//primary template
template<typename> struct extract_class_from_member_function_ptr;
template <typename A, typename B, class... _Types>
struct extract_class_from_member_function_ptr<A(B::*)(_Types...)> {
using type = B;
};
template <class _Fx, class... _Types>
bool NotifyAll(_Fx&& _Func, _Types&&... _Args) {
using T = extract_class_from_member_function_ptr<_Fx>::type;
return ExecuteForListenerTypePerEntry(T::GetListenerType(), [&](IListener* listener) {
T* casted_listener = dynamic_cast<T*>(listener);
if (casted_listener) {
std::invoke(std::forward<_Fx>(_Func), casted_listener, std::forward<_Types>(_Args)...);
}
});
}
template <typename T, class _Fx, class... _Types>
bool NotifyAll(T* one_time_specific_listener, _Fx&& _Func, _Types&&... _Args) {
using BaseT = extract_class_from_member_function_ptr<_Fx>::type;
return ExecuteForListenerTypePerEntry(BaseT::GetListenerType(), one_time_specific_listener, [&](IListener* listener) {
BaseT* casted_listener = dynamic_cast<BaseT*>(listener);
if (casted_listener) {
std::invoke(std::forward<_Fx>(_Func), casted_listener, std::forward<_Types>(_Args)...);
}
});
}
I tried making a few variants with different parameters on NotifyAllDelayed but that also didn't yield any positive effects.
This is a minimal working example where NotifyAllDelayed can be uncommented and the code will run:
#include <array>
#include <functional>
#include <iostream>
#include <queue>
#include <set>
#include <utility>
// code below doesn't matter VVVVVVVVVVVVVVVV
class DelayRunner
{
public:
using func_t = std::function<void(void)>;
private:
std::queue<func_t> queue;
public:
DelayRunner() : queue{} {}
~DelayRunner() {}
void Run() {
while (queue.size()) {
queue.front()();
queue.pop();
}
}
void Add(const func_t& func) {
queue.push(func);
}
};
enum ListenerType { LISTENER_TYPE_BEGIN, SPECIFIC_USER_DATA, LISTENER_TYPE_END };
class IListener {
public:
virtual ~IListener() {}
};
template<ListenerType type> class TypeAwareListener : public IListener {
public:
static ListenerType GetListenerType() {
return type;
}
};
class ISpecificUserDataListener : public TypeAwareListener<SPECIFIC_USER_DATA>
{
public:
virtual void OnSpecificUserDataUpdated(int userID) = 0;
};
class NotificationManager
{
private:
using listener_set = std::set<IListener*>;
std::array<listener_set, LISTENER_TYPE_END> listeners;
DelayRunner* delay_runner;
public:
NotificationManager(DelayRunner* delay_runner) : listeners{}, delay_runner{ delay_runner } {}
~NotificationManager() {}
void Register(ListenerType listenerType, IListener* listener) { listeners[listenerType].insert(listener); }
void Unregister(ListenerType listenerType, IListener* listener) { listeners[listenerType].erase(listener); }
bool ExecuteForListenerTypePerEntry(ListenerType listenerType, std::function<void(IListener* listeners)> code) {
listener_set& set = listeners[listenerType];
if (set.size() == 0) {
return false;
}
for (auto& entry : set) {
code(entry);
}
return true;
}
bool ExecuteForListenerTypePerEntry(ListenerType listenerType, IListener* one_time_specific_listener, std::function<void(IListener* listeners)> code) {
listener_set& set = listeners[listenerType];
if (one_time_specific_listener != nullptr) {
code(one_time_specific_listener);
}
for (auto& entry : set) {
if ((entry != one_time_specific_listener) && (entry != nullptr)) {
code(entry);
}
}
return (set.size() > 0) || (one_time_specific_listener != nullptr);
}
//primary template
template<typename> struct extract_class_from_member_function_ptr;
template <typename A, typename B, class... _Types>
struct extract_class_from_member_function_ptr<A(B::*)(_Types...)> {
using type = B;
};
template <class _Fx, class... _Types>
bool NotifyAll(_Fx&& _Func, _Types&&... _Args) {
using T = extract_class_from_member_function_ptr<_Fx>::type;
return ExecuteForListenerTypePerEntry(T::GetListenerType(), [&](IListener* listener) {
T* casted_listener = dynamic_cast<T*>(listener);
if (casted_listener) {
std::invoke(std::forward<_Fx>(_Func), casted_listener, std::forward<_Types>(_Args)...);
}
});
}
template <typename T, class _Fx, class... _Types>
bool NotifyAll(T* one_time_specific_listener, _Fx&& _Func, _Types&&... _Args) {
using BaseT = extract_class_from_member_function_ptr<_Fx>::type;
return ExecuteForListenerTypePerEntry(BaseT::GetListenerType(), one_time_specific_listener, [&](IListener* listener) {
BaseT* casted_listener = dynamic_cast<BaseT*>(listener);
if (casted_listener) {
std::invoke(std::forward<_Fx>(_Func), casted_listener, std::forward<_Types>(_Args)...);
}
});
}
// Above code doesn't matter ^^^^^^^^^^^^^^^^
// Because it works
// Question is about this code:
template <typename... _Types>
void NotifyAllDelayed(_Types&&... _Args)
{
delay_runner->Add([=, this] { this->NotifyAll<_Types...>(std::forward<_Types>(_Args)...); });
}
// code below doesn't matter VVVVVVVVVVVVVVVV
};
class GlobalUserDataListener : public ISpecificUserDataListener {
public:
virtual void OnSpecificUserDataUpdated(int userID) override {
std::cout << "GlobalUserDataListener called with userID: " << userID << std::endl;
}
};
class SpecificCallUserDataListener : public ISpecificUserDataListener {
public:
virtual void OnSpecificUserDataUpdated(int userID) override {
std::cout << "SpecificCallUserDataListener called with userID: " << userID << std::endl;
}
};
int main()
{
GlobalUserDataListener* global_listener_ptr{ new GlobalUserDataListener{} };
SpecificCallUserDataListener* specific_listener_ptr{ new SpecificCallUserDataListener{} };
DelayRunner* delay_runner{ new DelayRunner{} };
NotificationManager* notifications{ new NotificationManager{delay_runner} };
notifications->Register(global_listener_ptr->GetListenerType(), global_listener_ptr);
notifications->NotifyAll(&ISpecificUserDataListener::OnSpecificUserDataUpdated, 42);
notifications->NotifyAll(specific_listener_ptr, &ISpecificUserDataListener::OnSpecificUserDataUpdated, 28);
delay_runner->Add([=, ¬ifications] { notifications->NotifyAll(specific_listener_ptr, &ISpecificUserDataListener::OnSpecificUserDataUpdated, 2022); });
//notifications->NotifyAllDelayed(&ISpecificUserDataListener::OnSpecificUserDataUpdated, 9);
//notifications->NotifyAllDelayed(specific_listener_ptr, &ISpecificUserDataListener::OnSpecificUserDataUpdated, 2022);
std::cout << "Tick" << std::endl;
delay_runner->Run();
delete notifications;
delete delay_runner;
delete specific_listener_ptr;
delete global_listener_ptr;
return 0;
}
CodePudding user response:
First: All identifiers starting with an underscore followed by an uppercase letter are reserved for the C implementation in all context and using them in user code causes undefined behavior.
I will still continue using these identifiers in the answer, to make the context to the question easier recognizable.
It doesn't work because operator()
of a lambda is by-default const
-qualified. As a result lvalues referring to the members of the closure object will also be const
qualified.
So e.g. the second argument 9
to NotifyAllDelayed
is a rvalue of type int
. Passing it to the function will therefore deduce the corresponding element of _Types
to int
. Inside the function naming the second element of _Args
is a lvalue expression of type int
.
However when referring to _Args
inside the lambda it refers to the corresponding captured closure members and so its second element there when named as an expression is a lvalue of type const int
due to the const
ness of operator()
.
You then try to call effectively std::forward<int>(/*lvalue of type const int*/)
. The compiler is complaining that this is not possible because you are trying to remove the const
from the type.
You would need to mark the lambda mutable
for it to compile, however it semantically doesn't make sense then either. You are copying all _Args
into the lambda. The lambda body no longer refers to the objects outside the function, but to the copies inside the closure object. These objects are always owned by the lambda, there is no need to make a forwarding decision based on what was passed to NotifyAllDelayed
. You can either decide to pass std::move(_Args)
in which case the lambda may however be called only once, or alternatively just pass _Args
which may incur extra copy operations when called, but allows the lambda to be called multiple times. A cleaner solution would be to use a struct
-based function object with operator()
overloaded for lvalue and rvalue this
to choose between these two variants.
In any case the template arguments to NotifyAll
should be deduced. Manually specifying them will break in subtle ways, for example your second overload will usually not be viable with explicit arguments, as it doesn't expect T
to be the type of the actual function parameter (instead T*
).
You can only make use of the value category of the arguments to NotifyAllDelayed
by forwarding them into the copies in the closure instead of always copying them. Doing this via a lambda instead of a struct
-based function object requires C 20's init-capture pack expansion:
delay_runner->Add([..._Args=std::forward<_Types>(_Args), this] { this->NotifyAll(/* as above */); });
Or with C 20 you can simply use std::bind_front
which does exactly all of this:
delay_runner->Add(std::bind_front([this](auto&&... args){
this->NotifyAll(std::forward<decltype(args)>(args)...);
}), std::forward<_Types>(_Args)...));
The nested lambda is only necessary to resolve the issue of multiple overloads for NotifyAll
. Unfortunately there is currently no support in the language/library to write this common wrapper lambda in a clearer way. It is not uncommon to write a macro for it.