我写了(在c ++ 11中)一个可变参数模板constexpr
函数,它计算参数类型的max sizeof,例如:
maxSizeof<int, char, MyType>()
这工作正常。然后我想有一个可变参数模板类,其中一个字段是一个大小等于maxSizeof()的数组。这也应该正常工作:
template <typename... TypesT>
class Myclass {
uint8_t field[maxSizeOf<TypesT...>()]
}
但我还需要Myclass
为每个参数类型声明方法。我通过以下方式使用CRTP:
template <typename... TypesT>
class Myclass;
template <>
class Myclass {
uint8_t field[maxSizeOf<TypesT...>()] // (1) Couldn't do it here as there are no `TypesT`
}
template <typename FirstT, typename... OtherT>
class Myclass<FirstT, OtherT...> : public Myclass<OtherT...> {
public:
virtual void doSomething(FirstT object) = 0;
private:
uint8_t field[maxSizeOf<FirstT, OtherT...>()] // (2) Shouldn't do it here as it will create field for all of the "middle" classes
}
问题是如何实现方法的声明,同时具有适当大小的数组字段。 (1)和(2)因评论中说明的原因不起作用。
像大多数软件工程问题一样,这可以通过添加更多层的indirection[1]来解决:
template <typename... TypesT>
class MyclassFunctions;
template <>
class MyclassFunctions
{};
template <typename FirstT, typename... OtherT>
class MyclassFunctions<FirstT, OtherT> : public MyClassFunctions<OtherT...>
{
public:
virtual void doSomething(FirstT object) = 0;
};
template <typename... TypesT>
class Myclass : public MyclassFunctions<TypesT...>
{
uint8_t field[maxSizeOf<TypesT...>()]
};
这里有两个问题。首先是让它编译。第二是能够打电话给doSomething
。
第一个问题的简单解决方案是:
template <class T>
class MyClassFunction {
public:
virtual void doSomething(T object) = 0;
};
template <typename... TypesT>
class MyClass : public MyClassFunction<TypesT>...
{
uint8_t field[maxSizeOf<TypesT...>()];
};
这样做的缺点是调用doSomething
很难做到:
prog.cpp:33:59: error: request for member ‘doSomething’ is ambiguous using foo = decltype( ((MyClass<int, char>*)nullptr)->doSomething(7) ); ^~~~~~~~~~~
我们需要一些using
s来解决这个问题。在c++17,我们可以这样做:
template <typename... TypesT>
class MyClass : public MyClassFunction<TypesT>...
{
uint8_t field[maxSizeOf<TypesT...>()];
public:
using MyClassFunction<TypesT>::doSomething...;
};
但这在c++11中不可用。
要解决这个问题,我们必须进行基于树的继承。最简单的基于树的是线性的:
template<class...Ts>
struct MyClassFunctions {};
template<class T0, class T1, class...Ts>
struct MyClassFunctions<T0, T1, Ts...>:
MyClassFunction<T0>, MyClassFunctions<T1, Ts...>
{
using MyClassFunction<T0>::doSomething;
using MyClassFunctions<T1, Ts...>::doSomething;
};
template<class T0>
struct MyClassFunctions:MyClassFunction<T0> {};
template <typename... TypesT>
class MyClass : public MyClassFunctions<TypesT...>
{
uint8_t field[maxSizeOf<TypesT...>()];
};
这具有创建O(n ^ 2)总类型名称长度的缺点,这可能导致长类型列表的问题。在中等长度,你得到内存膨胀和编译时间减慢,长时间你会让编译器崩溃。
为了解决这个问题,你可以构建一个继承的二叉树。诀窍是能够使用对数深度模板递归将...
包分成两半。完成后,代码变为:
template<class T0, class T1, class...Ts>
struct MyClassFunctions<T0, T1, Ts...>:
left_half< MyClassFunctions, T0, T1, Ts... >,
right_half< MyClassFunctions, T0, T1, Ts... >
{
using left_half< MyClassFunctions, T0, T1, Ts... >::doSomething;
using right_half< MyClassFunctions, T0, T1, Ts... >::doSomething;
};
但是,如果你传递了几十种类型,这种努力是值得的。
左/右半看起来像这样:
template<template<class...>class Z, class...Ts>
using left_half = /* todo */;
template<template<class...>class Z, class...Ts>
using right_half = /* todo */;
在todo中有一些疯狂的元编程。
您可以使用索引技巧和std::tuple
的机制来拆分这些列表(在c++11 log-depth index generation需要一些努力)。或者您可以在类型列表上进行指数分割。
写
template<class...Ts>
struct pack {};
template<std::size_t N, class Pack>
struct split/* {
using lhs = // part before N
using rhs = // part after N
};
分裂一个类型列表,第一个N
在左边。它可以递归写入:
template<std::size_t N, class...Ts>
struct split<N, pack<Ts...>> {
private:
using half_split = split<N/2, pack<Ts...>>;
using second_half_split = split<N-N/2, typename half_split::rhs>;
public:
using lhs = concat< typename half_split::lhs, typename second_half_split::lhs >;
using rhs = typename second_half_split::rhs;
};
template<class...Ts>
struct split<0, pack<Ts...>> {
using lhs=pack<>;
using rhs=pack<Ts...>;
};
template<class T0, class...Ts>
struct split<1, pack<T0, Ts...>> {
using lhs=pack<T0>;
using rhs=pack<Ts...>;
};
这需要concat<pack, pack>
做明显的事情。
现在你需要apply<template, pack>
然后写left_half
和right_half
。
总结Angew的回答,以及Yakk指出的可访问性问题,你可以应用Angew工程原理:
template <typename... TypesT>
class MyclassFunctions;
template <>
class MyclassFunctions<>
{};
template <typename FirstT>
class MyclassFunctions<FirstT>
{
public:
virtual void doSomething(FirstT object){};
};
template <typename FirstT, typename... OtherT>
class MyclassFunctions<FirstT, OtherT...> : public MyclassFunctions<OtherT...>
{
public:
using MyclassFunctions<OtherT...>::doSomething;
virtual void doSomething(FirstT object){};
};
template <typename... TypesT>
class Myclass : public MyclassFunctions<TypesT...>
{
int field[sizeof...(TypesT)];
};
一种方法是利用函数式编程语言用户熟知的旧技巧:对部分结果进行递归,例如:累积的最大值作为第一个模板参数。然而,为了保持Myclass<Ts...>
签名,我们需要增加一个间接级别,正如Angew建议的那样:
template<std::size_t curMax, typename... Ts>
struct MyclassImpl
{
std::uint8_t field[curMax];
};
template<std::size_t curMax, typename T1, typename... Ts >
struct MyclassImpl<curMax, T1, Ts...> : MyclassImpl<(curMax > sizeof(T1) ? curMax : sizeof(T1)), Ts...>
{
virtual void doSomething(T1) = 0;
};
template<typename... Ts>
using Myclass = MyclassImpl<0u, Ts...>;
让我们添加一个原始测试用例(你也可以在这里使用maxSizeof
):
struct TestStruct{ char c[100]; };
using TestMC = Myclass<int, double, TestStruct, short>;
static_assert( sizeof(TestMC::field) == sizeof(TestStruct), "Field size incorrect" );
虽然这并不比Angew的版本好(除了不需要maxSizeof
),但它仍然是指出这种有用的递归模式的好机会。大多数但不仅仅是在像Haskell这样的纯函数式编程语言中,它对于启用尾调用优化非常有用,例如:
-- slowMax "tail call unfriendly"
-- (optional) signature
slowMax :: Ord a => [a] -> a
slowMax [x] = x
slowMax (x:xs) = let m = slowMax xs in if x > m then x else m
-- fastMax "tail call friendly"
fastMax :: Ord a => [a] -> a
fastMax (x:xs) = fastMaxImpl x xs where
fastMaxImpl m [] = m
fastMaxImpl m (x:xs) = fastMaxImpl (if x > m then x else m) xs
(在我使用-O2
进行的简单测试中,fastMax
的速度提高了3.5倍。)