First off, sorry for unclear question title, feel free to edit if you think of a better way to state it.
I have a class:
template <typename ...Arguments>
class CSignal
{
template <typename ...ActualArguments>
void invoke(ActualArguments&&... args) const {}
};
And another one, this is what I have a problem with:
class SomeClass
{
template<typename ...Arguments>
void invokeQueued(CSignal<Arguments...>& signal, const Arguments&... args)
{
m_queue.emplace_back([=](){signal.invoke(args...);});
}
std::deque<std::function<void (void)>> m_queue;
};
Problem:
CSignal<float> signal;
int i = 0;
SomeClass().invokeQueued(signal, i);
Error:
template parameter 'Arguments' is ambiguous
could be 'float'
or 'int'
Possible naive solution
template<typename ...FormalArguments, typename ...ActualArguments>
void invokeQueued(CSignal<FormalArguments...>& signal, const ActualArguments&... args)
{
m_queue.emplace_back([=](){signal.invoke(args...);});
}
is not acceptable in this specific case, because I need to capture the arguments by value (copy them into the lambda), and the conversion from ActualArguments
to FormalArguments
must occur at the time invokeQueued
is called, not when the lambda is called.
If I could typedef
the arguments pack in the CSignal
class, I would do:
template<typename ...FormalArguments>
void invokeQueued(CSignal<FormalArguments...>& signal, const CSignal<FormalArguments...>::argument_types&... args)
{
m_queue.emplace_back([=](){signal.invoke(args...);});
}
But it doesn't seem possible. Solutions?
The error you encounter is raised because the types of arguments differ while a compiler has only one type template parameter for each such a pair: from the CSignal
's signature it sees float
, while from the deduced type of the second argument it sees int
, and both have to be matched against a single element of the Arguments
pack. This is where the ambiguity originates from.
To work around that, you can exclude one of the parameters from template argument deduction by introducing a non-deduced context, like with the below identity trick:
template <typename T> struct identity { using type = T; };
template <typename T> using identity_t = typename identity<T>::type;
class SomeClass
{
public:
template <typename... Arguments>
void invokeQueued(CSignal<Arguments...>& signal,
const identity_t<Arguments>&... args)
// ~~~~~~~~~^
{
m_queue.emplace_back([=](){signal.invoke(args...);});
}
std::deque<std::function<void(void)>> m_queue;
};
The compiler will not attempt to deduce any template parameter that is part of nested name specifier syntax, and this is basically what identity
does - it introduces the identity<T>::type
syntax, so that T
is left to a scope resolution operator, yet it still can be used intact in the function declaration.
Alternatively, you may store decayed copies of arguments converted to proper types at the time of capturing them in a lambda expression (C++14):
#include <utility>
#include <type_traits>
#include <cstddef>
class SomeClass
{
public:
template <typename... FormalArguments, typename... ActualArguments>
void invokeQueued(CSignal<FormalArguments...>& signal, ActualArguments&&... args)
{
invokeQueued(signal, std::index_sequence_for<ActualArguments...>{}, std::forward<ActualArguments>(args)...);
}
template <typename... FormalArguments, typename... ActualArguments, std::size_t... Is>
void invokeQueued(CSignal<FormalArguments...>& signal, std::index_sequence<Is...>, ActualArguments&&... args)
{
m_queue.emplace_back(
[signal, t = std::tuple<std::decay_t<FormalArguments>...>(std::forward<ActualArguments>(args)...)]
(){signal.invoke(std::get<Is>(t)...);});
}
std::deque<std::function<void(void)>> m_queue;
};