0

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.

TwistedBlizzard
  • 931
  • 2
  • 9
  • 3
    *"Nor do I want to have the template parameters specified above the class like"* - However that's exactly what you'll have to do at some point. – user7860670 Jul 20 '22 at 05:46
  • So do you want to write your own replacement for std::function? – n. m. could be an AI Jul 20 '22 at 06:07
  • 1
    @n.1.8e9-where's-my-sharem. Not really, I just want to know the way std::thread stores its arguments through its constructor. I'm confused how it stores its arguments in tuples, because how do you declare a tuple without specifying exactly what is going to be stored inside? – TwistedBlizzard Jul 20 '22 at 06:20
  • `std::function` (as well as `std::any` and similar) does something, that is called *type erasing*. You may want to search for that term if you know how exactly it is done. Basically you need to allocate memory for the arguments you want to store, and then save the type ids or something similar to identify the types later on. – Jakob Stark Jul 20 '22 at 06:30
  • @beangod why do you think `std::thread` stores the arguments into a `std::tuple`? – Jakob Stark Jul 20 '22 at 06:31
  • @beangod if you specify `struct S { template S(Args...args){} };` you can call it as you like. Now if you're asking how it's possible to store the tuple afterwards: type erasure is the answer, see https://www.youtube.com/watch?v=xJSKk_q25oQ – alagner Jul 20 '22 at 06:40
  • But I don't really know if the point of this question is "how templated constructor works" or maybe "type erasure in std::function and the like" or "how is tuple unpacked into and passed into system-specific thread API", can you please specify that? – alagner Jul 20 '22 at 06:42
  • @alagner I'm sorry for the way I wrote this question. I updated it to clarify. I think I've definitely had some misconceptions about std::thread. – TwistedBlizzard Jul 20 '22 at 07:14
  • How exactly do you want to use the stored arguments later? – HolyBlackCat Jul 20 '22 at 07:14
  • @HolyBlackCat The stored arguments could be used for anything. I'm only curious on how you would store the arguments and then somehow be able to retrieve them. – TwistedBlizzard Jul 20 '22 at 07:21
  • Thing is, you can't use them "for anything". You have to know their uses in advance. E.g. the answer below uses them to call a function, and doesn't let you use them for anything else. Any solution would be like this. – HolyBlackCat Jul 20 '22 at 07:24

2 Answers2

1

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;
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
  • This desperately needs perfect forwarding. – HolyBlackCat Jul 20 '22 at 07:15
  • @HolyBlackCat I explictly didn't since I need copies anyway. – Pepijn Kramer Jul 20 '22 at 07:30
  • I don't think you do. You need either copies or moves. – HolyBlackCat Jul 20 '22 at 09:25
  • The intention is to pass temporaries without copying and then let the lambda capture do the copying (to avoid doing it twice). – Pepijn Kramer Jul 20 '22 at 09:49
  • Passing by reference is ok, but you don't have copy while capturing. You should either copy or move, depending on what kind of reference you got. In other words, `[...args = std::forward(args)]`. Same for the function itself, it also should be taken by a forwarding reference and captured with forwarding. – HolyBlackCat Jul 20 '22 at 18:51
  • @HolyBlackCat Thanks that was what I intended. Used std::forward of packs like that a lot, just not in a lambda context yet. End result should be this : // @HolyBlackCat A thanks, that's indeed what I missed :) So it will become : `FnWrapper(fn_t&& fn, args_t&&... args) : m_fn{ [fn_copy=std::forward(fn), ...args_copy = std::forward(args)] { fn_copy(args_copy...); } }` then. – Pepijn Kramer Jul 21 '22 at 04:04
  • Looks good to me. Consider editing the answer. – HolyBlackCat Jul 21 '22 at 06:47
0

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.

alagner
  • 3,448
  • 1
  • 13
  • 25