我正在尝试编写一个具有模板化构造函数的类Invocation
:
template<typename F>
class Invocation {
public:
template<typename... Args>
Invocation(F&& f, Args&&... args)
{ /* store f and args somewhere for later use */ }
...
};
通常我会用F
和Args...
参数化Invocation
类本身,但在这种情况下,我需要给定F
的统一类型,所以我试图找到一种方法在Invocation<F>
中存储任何类型的args...
,并尽可能减少性能影响。(这可能不是最好的设计,但它可能是一个有趣的练习。
一个想法是使用虚函数:
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_;
};
然后在 Invocation<F>
类中,例如,我们可以有一个 std::unique_ptr<ArgsBase<F>>
成员,它指向在 Invocation<F>
ctor 中创建的Args<F, Ts...>
对象。我们可以在需要时将其invoke
虚拟方法调用。
这只是我想出的一个随机想法。还有其他方法可以实现这一点吗?理想情况下,没有虚拟功能或类似内容的开销?
更新:感谢建议使用 std::function
或 lambda 的评论/答案。我应该明确表示,我实际上对更一般的情况感兴趣,即可变参数的东西可能不是可调用的参数。它可以是我想存储在类中的任何内容,其类型不是由这些东西的类型参数化的。
如评论中所述,我不会担心按值存储参数。编译器的复制省略可能很慷慨。
特别是如果您为类提供 r 值调用:
#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);
}
如果您尝试保存函数调用及其参数以供以后调用,则可以使用打包在std::function
对象中的 lambda:
template<typename F, typename ... Args>
std::function<void()> createInvocation(F f, const Args& ... args)
{
return [f,args...]() { f(args...); };
}
然后你可以像这样使用它:
void myFunc(int a, int b)
{
std::cout << "Invoked: " << a + b << std::endl;
}
int main() {
auto invocation = createInvocation(myFunc, 1, 2);
invocation();
return 0;
}
更新:如果要创建泛型非模板化容器类型,可以将元组包装到本身派生自非模板化类型的类型中。然后,主要问题是访问基础数据。这可以通过创建一个静态函数调度表来解决,对于给定的元组类型,重定向查询,以便可以使用动态提供的函数参数调用需要编译时常量索引模板参数的std::get
。下面是实现此目的的实现:
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)...);
}
然后你可以按如下方式使用上述内容:
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;
}
从上面的用法中可以看出,您已经实现了将任意模板化元组包装为非模板化类型的目标,这样您就可以使用普通(函数)索引参数而不是模板参数访问组件成员。现在这种getter的返回类型必须是通用的,所以我选择在这里使用void*
,这并不理想。但是你可以发展这个想法,使这个容器提供有关其数据元组成员类型的更多有用信息。另外,请注意,这确实使用虚拟函数。通过一些进一步的工作,你也可以摆脱它,尽管你将无法摆脱至少一个函数指针查找(即调度表中的查找) - 这是获得能够使用运行时值索引到元组的灵活性所付出的代价。