3

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?

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
Violet Giraffe
  • 32,368
  • 48
  • 194
  • 335
  • 1
    Why must it happen when `invokeQueue` is called? – Barry Dec 20 '14 at 21:19
  • @Barry: because the functor will be saved and called later, by which time the original arguments will be long gone out of scope. Example: `CSignal signal; SomeClass().invokeQueued(signal, "123")`. Conversion from `const char[4]` to `std::string` exists and it must occur right now, while the `char*` pointer is valid. – Violet Giraffe Dec 20 '14 at 21:21

2 Answers2

8

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.

DEMO


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;
};

DEMO 2

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
  • That works! At least it compiles, hopefully it does copy the arguments, will check that later. I don't have a slightest understanding of the `using` syntax, though :( – Violet Giraffe Dec 20 '14 at 21:29
  • Convention is to reverse the roles of `identity` and `identity_t`, as the standard uses the `_t`-extension for the version which does not need the `typename `something`::type`. – Daniel Frey Dec 20 '14 at 21:43
  • @DanielFrey yes it is, but I like the opposite approach – Piotr Skotnicki Dec 20 '14 at 21:44
  • @PiotrS FWIW, I feel the same. – Daniel Frey Dec 20 '14 at 21:50
  • The `identity_t` struct also seems to work with `typedef T type` instead of `using type = T`. Is there any difference between the two declarations in this context? – Violet Giraffe Dec 21 '14 at 08:23
  • @VioletGiraffe no difference between those two. Also that identity alias for identity_t is just for convenience. – Piotr Skotnicki Dec 21 '14 at 08:40
  • @PiotrS.: I think I get it now, thank you. It's just a way to create a usable alias for otherwise inaccessible `Arguments` type. And `identity` is as close as we can get to a templated `typedef`. – Violet Giraffe Dec 21 '14 at 08:43
1

Use the identity trick to create a non-deduced context for Arguments&&....

template <typename T>
class Identity
{
public:
using type = T;
};

template<typename ...Arguments>
void invokeQueued(CSignal<Arguments...>& signal, const typename Identity<Arguments>::type &... args)
{
    m_queue.emplace_back([=](){signal.invoke(args...);});
}
Pradhan
  • 16,391
  • 3
  • 44
  • 59