简体   繁体   中英

Variadic members in non-template class

I'm trying to write a class Invocation which has a templated constructor:

template<typename F>
class Invocation {
public:
    template<typename... Args>
    Invocation(F&& f, Args&&... args)
    { /* store f and args somewhere for later use */ }

    ...
};

Normally I would parameterize the Invocation class itself with both F and Args... , but in this case I need a uniform type for a given F , so I'm trying to find a way to store args... of any types inside a Invocation<F> , and to incur as little performance hit as possible. (This might not be the best design, but it can be an interesting exercise.)

One thought is to use virtual functions:

template<typename F>
class ArgsBase {
public:
    // discard return value
    virtual void invoke(F&& f) = 0;
};

template<typename F, typename... Ts>
class Args : public ArgsBase<F> {
public:
    Args(Ts&&... args) : args_(std::forward<Ts>(args)...) {}

    void invoke(F&& f) override
    {
        /* somehow call f with args_ (something like std::apply) */
        ...
    }

private:
    std::tuple<Ts&&...> args_;
};

And then in the Invocation<F> class, we can for example have an std::unique_ptr<ArgsBase<F>> member, which points to an Args<F, Ts...> object created in the Invocation<F> ctor. And we can call its invoke virtual method when needed.

This is just one random idea I came up with. Is there any other way to achieve this? Ideally without the overhead of virtual functions or anything like that?

UPDATE: Thanks to the comments/answers that suggest using std::function or lambdas. I should've made it clear that I'm actually interested in a more general case, ie, the variadic stuff might not be arguments to a callable. It can be just anything that I want to store in a class whose type is not parameterized by the types of these stuff.

If you're trying to save a function call with its parameters for later invocation, you could use lambdas packaged in std::function objects:

template<typename F, typename ... Args>
std::function<void()> createInvocation(F f, const Args& ... args)
{
    return [f,args...]() { f(args...); };
}

Then you could use it like this:

void myFunc(int a, int b)
{
    std::cout << "Invoked: " << a + b << std::endl;
}

int main() {

    auto invocation = createInvocation(myFunc, 1, 2);

    invocation();

    return 0;

}

UPDATE: If you wanted to create a generic non-templated container type, you can wrap a tuple into a type that itself derives from a non-templated type. The main problem then is accessing the underlying data. This can be solved by creating a static function dispatch table that for a given tuple type, redirects queries so that std::get , which requires a compile-time constant index template parameter, can instead be invoked with a dynamically provided function parameter. Here is an implementation that achieves this:

class GenericTupleContainer
{
public:

    virtual const void* getItemAtIndex(size_t index) = 0;
};

template<typename ... T>
class TupleContainer : public GenericTupleContainer
{
public:

    TupleContainer(T&& ... args)
        : data(std::forward<T>(args)...)
        {}

    const void* getItemAtIndex(size_t index) override
    {
        if(index >= sizeof...(T))
            throw std::runtime_error("Invalid index");

        return dispatchTable[index](data);
    }

private:

    template<size_t index>
    static const void* getItemAtIdx(const std::tuple<T...>& data)
    {
        return &std::get<index>(data);
    }

    using GetterFn = const void*(*)(const std::tuple<T...>&);

    static GetterFn* initDispatchTable()
    {
        static GetterFn dispatchTable[sizeof...(T)];
        populateDispatchTable<sizeof...(T)>(dispatchTable, std::integral_constant<bool, sizeof...(T) == 0>());
        return dispatchTable;
    }

    static GetterFn* dispatchTable;

    template<size_t idx>
    static void populateDispatchTable(GetterFn* table, std::false_type);

    template<size_t idx>
    static void populateDispatchTable(GetterFn* table, std::true_type) 
    {
        //terminating call - do nothing
    }

    std::tuple<T...> data;
};

template<typename ... T>
typename TupleContainer<T...>::GetterFn* TupleContainer<T...>::dispatchTable = TupleContainer<T...>::initDispatchTable();

template<typename ... T>
template<size_t idx>
void TupleContainer<T...>::populateDispatchTable(GetterFn* table, std::false_type)
{
    table[idx-1] = &TupleContainer<T...>::template getItemAtIdx<idx-1>;
    populateDispatchTable<idx-1>(table, std::integral_constant<bool, idx-1 == 0>() );
}

template<typename ... T>
auto createTupleContainer(T&& ... args)
{
    return new TupleContainer<T...>(std::forward<T>(args)...);
}

Then you can use the above as follows:

int main() {

    GenericTupleContainer* data = createTupleContainer(1, 2.0, "Hello");

    std::cout << *(static_cast<const int*>(data->getItemAtIndex(0))) << std::endl;
    std::cout << *(static_cast<const double*>(data->getItemAtIndex(1))) << std::endl;
    std::cout << (static_cast<const char*>(data->getItemAtIndex(2))) << std::endl;

    return 0;

}

As you can see from the above usage, you've achieved the aim of wrapping an arbitrary templated tuple into a non-templated type, in such a way that you can access the component members with a normal (function) index parameter instead of a template one. Now the return type of such a getter has to be universal, so I've chosen to use void* here, which is not ideal. But you can develop this idea to make this container give more useful information about the types of its data tuple members. Also, note that this does use a virtual function. With some further work you can get rid of this as well, although you won't be able to get rid of at least one function pointer lookup (ie the lookup in the dispatch table) - this is the price paid for gaining the flexibility of being able to use a runtime value to index into the tuple.

As mentioned in comment, I wouldn't worry about storing arguments by value. The compiler's copy-elision can be generous.

Particularly if you offer the class an r-value invoke:

#include <tuple>

template<typename F>
class ArgsBase {
public:
    // discard return value
    virtual void invoke(F&& f) const & = 0;
    virtual void invoke(F&& f) && = 0;
};

template<typename F, class... FunctionArgs>
class Args : public ArgsBase<F> {
public:
  template<class...Ts>
    Args(Ts&&... args) : args_(std::forward<Ts>(args)...) {}

    template<std::size_t...Is, class Tuple>
      static void invoke_impl(F& f, std::index_sequence<Is...>, Tuple&& t)
    {
      f(std::get<Is>(std::forward<Tuple>(t))...);
    }


    void invoke(F&& f) const & override
    {
      invoke_impl(f, 
                  std::make_index_sequence<std::tuple_size<tuple_type>::value>(), 
                  args_); 
        /* somehow call f with args_ (something like std::apply) */
    }

    void invoke(F&& f) && override
    {
      invoke_impl(f, 
                  std::make_index_sequence<std::tuple_size<tuple_type>::value>(), 
                  std::move(args_)); 
        /* somehow call f with args_ (something like std::apply) */
    }

private:
  using tuple_type = std::tuple<FunctionArgs...>;
    tuple_type args_;
};

template<class Callable, class...MyArgs>
  auto later(MyArgs&&...args) {
  return Args<Callable, std::decay_t<MyArgs>...>(std::forward<MyArgs>(args)...);
}

void foo(const std::string&, std::string)
{
}

int main()
{
  auto l = later<decltype(&foo)>(std::string("hello"), std::string("world"));
  l.invoke(foo);
  std::move(l).invoke(foo);
}

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