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.