Home > OS >  How to Store Variadic Template Arguments Passed into Constructor and then Save them for Later Use?
How to Store Variadic Template Arguments Passed into Constructor and then Save them for Later Use?

Time:07-20

I am curious how one would go about storing a parameter pack passed into a function and storing the values for later use.

For instance:

class Storage {
public:
   template<typename... Args>
   Storage(Args... args) {
     //store args somehow
   }
 }

Basically I am trying to make a class like tuple, but where you don't have to specify what types the tuple will hold, you just pass in the values through the constructor.

So for instance instead of doing something like this:

std::tuple<int, std::string> t = std::make_tuple(5, "s");

You could do this:

Storage storage(5, "s");

And this way you could any Storage objects in the same vector or list. And then in the storage class there would be some method like std::get that would return a given index of an element we passed in.

CodePudding user response:

Since run will return void, I assume all the functions you need to wrap can be functions that return void too. In that case you can do it like this (and let lambda capture do the storing for you):

#include <iostream>
#include <functional>
#include <string>
#include <utility>

class FnWrapper
{
public:
    template<typename fn_t, typename... args_t>
    FnWrapper(fn_t fn, args_t&&... args) :
        m_fn{ [=] { fn(args...); } }
    {
    }

    void run()
    {
        m_fn();
    }

private:
    std::function<void()> m_fn;
};

void foo(const std::string& b)
{
    std::cout << b;
}

int main()
{
    std::string hello{ "Hello World!" };
    FnWrapper wrapper{ foo, hello };
    wrapper.run();
    return 0;
}

CodePudding user response:

OK, what you're asking is type erasure. Typical way of implementing it is via a virtual function inherited by a class template.

Live demo here: https://godbolt.org/z/fddfTEe5M I stripped all the forwards, references and other boilerplate for brevity. It is not meant to be production code by any means.

#include<memory>
#include <iostream>
#include <stdexcept>

struct Fn
{
    Fn() = default;

    template<typename F, typename...Arguments>
    Fn(F f, Arguments...arguments)
    {
        callable = 
            std::make_unique<CallableImpl<F, Arguments...>>(f, arguments...);
    }


    void operator()()
    {
        callable 
            ? callable->call()
            : throw std::runtime_error("empty function");
    }

    struct Callable
    {
        virtual void call() =0;
        virtual ~Callable() = default;
    };

    template<typename T, typename...Args_> 
    struct CallableImpl : Callable
    {
        CallableImpl(T f, Args_...args)
        : theCallable(f)
        , theArgs(std::make_tuple(args...))
        {}

        T theCallable;
        std::tuple<Args_...> theArgs;
        void call() override
        {
            std::apply(theCallable, theArgs);
        }
    };

    std::unique_ptr<Callable> callable{};
};

void f(int a)
{
    std::cout << a << '\n';
}

int main(int, char*[])
{
    Fn fx{f, 3};
    fx();
    char x = 'q';
    Fn flambda( [x](){std::cerr << x << '\n';} );
    flambda();

}

The "meat" of it lies here:

    struct Callable
    {
        virtual void call() =0;
        virtual ~Callable() = default;
    };

    template<typename T, typename...Args_> 
    struct CallableImpl : Callable
    {
        CallableImpl(T f, Args_...args)
        : theCallable(f)
        , theArgs(std::make_tuple(args...))
        {}

        T theCallable;
        std::tuple<Args_...> theArgs;
        void call() override
        {
            std::apply(theCallable, theArgs);
        }
    };

Callable is just the interface to access the object. Enough to store a pointer to it and access desired methods. The actual storage happens in its derived classes:template<typename T, typename...Args_> struct CallableImpl : Callable. Note the tuple there. T is for storing the actual object, whatever it is. Note that it has to implement some for of compile-time interface, in C terms referred to as a concept. In that case, it has to be callable with a given set of arguments. Thus it has to be known upfront.

The outer structure holds the unique_ptr to Callable but is able to instantiate the interface thanks to the templated constructor:

    template<typename F, typename...Arguments>
    Fn(F f, Arguments...arguments)
    {
        callable = 
            std::make_unique<CallableImpl<F, Arguments...>>(f, arguments...);
    }

What is the main advantage of it? When done properly, it has value semantics. Effectively, it can be used to represent a sort of polymorphism without derivation, note T doesn't have to have a common base class, it just has to be callable in one way or another; this can be used for addition, subtraction, printing, whatever.

As for the main drawbacks: a virtual function call (CallableImpl stored as Callable) which may hinder performance. Also, getting back the original type is difficult, if not nearly impossible.

  • Related