【元编程】C++ Parameter Pack 从编译期循环到编译期判断质数
写在前面
本来是想在网上找一下这个问题:
利用
C++
实现编译期确定1~100
内的质数。
但是并没有找到很好的教程,于是打算自己从编译期循环一步步做一个简单的实现。
本文将包含以下内容:
Parameter Pack
基础用法;- 利用
Parameter Pack
实现一些常用的方法; - 编译期与运行期;
- 利用
Parameter Pack
实现编译期循环; - 编译期判断质数;
C++核心特性 Parameter Pack
Parameter Pack
是C++11
引入的模板新特性,其可以用来接收任意数量(在栈内存允许的情况下)的相同或者不同类型的参数。
其基本语法如下:
template<class ...Args>
void test(Args ...args) {}
test();
test(1); // test(int);
test(1, 2, 3); // test(int, int, int);
test(0, 1.0, 'f', "OK"); // test(int, double, const char *);
上面的所有调用都是可行的,除了上述的基础用法,还可以将Parameter Pack
和普通的模板参数一同使用,不过这时需要保证Parameter Pack
是最后一个参数:
template<class T0, class ...Args>
void test(T0 t0, Args ...args) {}
// test() ill-formed t0 cannot be empty.
test(1); // T0 is int; Args... is empty.
test(0, 1, 1.0, "f"); // T0 is int, Args... are double and const char *;
Parameter Pack展开
Parameter Pack
的使用往往需要通过展开来实现。
基础展开
通过...
即可对参数类型或者参数进行展开:
// Provided calling test(int, double);
template<class ...Args>
void test(Args ...args) {
// Args... -> int, double
// args... -> arg0, arg1
std::tuple<Args...>(args...);
}
掌握了上述的展开方式之后,可以更改...
之前的部分进行不同的展开方式:
// Provided call test(int, double)
// Args*... -> int*, double*
// &args... -> &arg0, &arg1
// ++args...-> ++arg0, ++arg1
// (std::cout << args)... -> std::cout << arg0, std::cout << arg1
// std::forward<Args>(args)... -> std::forward<int>(arg0), std::forward<double>(arg1)
注意:上面的展开结尾均没有分号。
利用Parameter Pack实现一些常用的方法
利用Parameter Pack实现任意个数求最大值
这里只介绍求最大值的方法,求最小值、和、乘积等稍作修改即可实现:
// this need to be corrected.
template<class T0, class ...Args>
auto max(T0 t0, Args ...args)
{
auto res = max(args...);
return t0 > res ? t0 : res;
}
作为刚学会Parameter Pack
的你可能会写出上面的代码,上面的代码不能够生成,这是因为上述的代码在递归的最后一层会调用max()
,而我们并没有定义这个函数。
补充:上述的代码发生了递归调用,对于max(int, int, int)
上面的代码会依次调用max(int, int), max(int), max()
由于没有max()
函数所以上述的代码在实例化的时候会发生错误。
解决方法很简单既然没有max()
我们增加一个即可,但是增加的max()
返回值应该是什么呢?你可能认为是-inf
,但是这样并不具有一般性,对于重载了<
符号的类来说,如果返回一个整数可能并不能够进行比较。其实我们可以增加一个只有一个模板参数的实现:
template<class T>
T max(T t) { return t; }
这样也可行的原因是:在进行模板匹配的时候,对于单个参数的情况,不使用Parameter Pack
的匹配度更高,此时编译器会调用不使用Parameter Pack
的模板实现,因此也就不会发生调用max()
的情况了。
那么类似的,把上面的代码的max
改为min
同时将>
改成<
便可以实现多个变量中找出最小值的代码,求和、连乘等也很容易可以写出。
上面的代码除了使用递归实现,还可以通过非递归的方式实现:
template<class ...Args>
auto max(Args ...args) {
using T = typename std::common_type<Args...>::type;
T t[sizeof...(Args)] = {args...};
T res = t[0];
for (int i = 1; i < sizeof...(Args); i++) {
if (t[i] > res) {
res = t[i];
}
}
return res;
}
上面的代码中sizeof...(Args)
可以获取Args
中有多少个参数。
编译期与运行期
在通常情况下非递归的代码效率可能会比递归的代码效率更高。但是在模板展开中可以发生意外,例如这里给出递归和非递归的多个数求和实现:
template<class T>
constexpr T sum(T t) { return t; }
template<class T0, class ...Args>
constexpr auto sum(T0 t0, Args ...args)
{
return t0 + sum(args...);
}
template<class ...Args>
auto sum(Args ...args)
{
using T = typename std::common_type<Args...>::type;
T t[sizeof...(Args)] = {args...};
T sum = t[0];
for (int i = 1; i < sizeof...(Args); i++) { sum += t[i]; }
return sum;
}
如果依然按照上面实现max
的方式通过一个数组来对变量求和的话,那么非递归版的效率可能不如递归版本,这主要有两个原因:
- 对于模板函数,默认是具有
inline
属性的; - 对于递归的方法中,如果传入的参数均是编译期常量,那么编译器可能在完成编译的时候,就计算出了
max
的返回值,而在运行的时候不会进行计算。
对于编译期和运行期这里给出一个简单的例子,在我们实际开发过程中我们可能需要进行相关的变量修改,例如有一个变量表示日期,我们希望将日期更改到一小时三十分钟后(也就是九十分钟后),假设以下两种写法都是合法的:
Data date = now;
// way 1
date += 1hour + 30minutes;
// way 2
date += 90minutes;
上述代码可能看着第二种方式更加高效,只需要进行一次加法,实际上对于上面的情况编译器可以将编译期就能确定的计算给优化掉,在上面的例子中1hour + 30minutes
会被编译器计算成90minutes
后替换。但是如果上述的1hour + 30minutes
变成了两个变量相加(a+b
),那么通常情况下则不会触发优化。
编译期循环
对于编译期循环,其名字虽然叫做编译期循环但是叫做“去除循环”则更加贴切。
对于通常需要计算多个数的和,我们可以向上一节中使用循环进行计算,如果使用循环,那么这往往就和编译期计算脱离了关系,起始上面的求和代码我们可以使用C++17
的折叠表达式新特性进行编译期计算:
template<class ...Args>
auto sum(Args ...args) {
return (args + ...); // return (arg0 + arg1 + arg2 + ...);
}
上面的代码由于被展开成了多个数字相加,编译期可以在编译时进行优化直接使用结果替换。
由于上面的方法C++17
才开始支持,这里给出C++11
也支持的方法:
template<int ...args>
struct Sum {};
template<int t0>
struct Sum<t0> {
static constexpr int value = t0;
};
template<int t0, int ...args>
struct Sum<t0, args...> {
static constexpr int value = t0 + Sum<args...>::value;
};
// usage:
Sum<1, 2, 3, 4>::value;
上面的代码通常只能实现同种类型的变量求和,没有C++17
中的折叠表达式使用方便。上面的代码利用到了模板偏特化。
见识了上面的例子后,对于编译期循环,我相信大家已经有了基本的认识,即想发设发将运行时的循环通过模板递归或者折叠表达式的方式去除掉,这样就能够在编译期实现循环的效果。
更进一步的编译期循环:输出0-100的数
对于输出0~100
的数任务,即使去掉了循环,也不能够进行任何优化,依然会调用一百次std::cout
或者printf
函数,这里的编译期循环,指的是,在循环期间这些数字必须全部是constexpr
的,你可以理解成需要实现如下的代码:
std::cout << 0 << std::endl;
std::cout << 1 << std::endl;
...
std::cout << 99 << std::endl;
检验一个数是不是编译期常量可以通过检查这个数是否能够出现在模板参数的位置,例如:
int x = 10;
std::array<int, x> a;// WRONG, x is not constexpr
std::array<int, 10> b; // OK, 10 is constexpr
constexpr int y = 10;
std::array<int, y> c; // OK y is constexpr
具体地,上面所要求的编译期循环我们可以通过如下的方法实现(需要C++17
):
template<class T, T val>
struct constexprT {
static constexpr T value = val;
};
template<size_t Beg, size_t End, class Lambda>
void staticFor(Lambda lambda) {
if constexpr (Beg < End) {
lambda(constexprT<size_t, Beg>{});
staticFor<Beg + 1, End>(lambda);
}
}
// usage:
staticFor<0, 100>([] (auto i) {
std::cout << i.value << std::endl;
});
上面的constexprT
在std
中已经有对应的实现std::integral_constant
使用方法和上面的类似,通过value
访问到的值是编译期常量。在上面的代码中,我们向模板参数中传入一个匿名函数,这个匿名函数用于输出i
,经过上面的操作后我们明显的发现上面的每一个i.value
均是constexpr
符合我们的要求。
使用std::make_index_sequence
也能实现上面的效果:
// Index... -> 0, 1, 2, 3, ..., 99
template<size_t ...Index, class Lambda>
void staticForImpl(Lambda lambda, std::index_sequence<Index...>) {
(lambda(std::integral_constant<size_t, Index>{}), ...); // (lambda(0), lambda(1), ..., lambda(99));
}
template<size_t N, class Lambda>
void staticFor(Lambda lambda) {
staticFor(lambda, std::make_index_sequence<N>{});
}
// usage:
staticFor<100>([] (auto i) {
std::cout << i.value << std::endl;
});
实现编译期判断质数,并输出1-100的质数
由于使用编译期计算只能实现简单的循环操作,对于数组的访问的实现比较复杂,我们这是判断质数的方法使用非筛的方法。
我们首先要明确的是在编译期计算并输出1-100
的质数的步骤:
- 由于需要在编译期进行质数的判断,第一步需要进行编译期循环,即保证每个循环的变量是编译期常量;
- 对一个编译期常量实现编译期的质数判断;
- 输出质数;
对于上述过程的第二步判断质数,我们可以分解成如下的步骤:
- 对于数
Number
依次在编译期计算 N u m b e r ? % ? i , i = 2 , 3 , . . . , N u m b e r ? 1 Number\ \%\ i, i = 2,3,...,Number-1 Number?%?i,i=2,3,...,Number?1; - 对于上述的计算结果编译期间确定是否全部非
0
如果结果全部非0
则说明当前的数字是质数。
为此我们先实现判断多个数字是否全部是0
的代码:
template<class ...Args>
constexpr bool anyZero(Args ...args) {
return (0 || ... || (args == 0));
}
接下来我们需要计算 N u m b e r ? % ? i Number\ \%\ i Number?%?i的值,在这一步中我们需要使用到前文中提到的编译期循环:
constexpr int MAX_NUMBER = 100;
template<size_t ...Index>
constexpr bool isPrimeImpl(std::index_sequence<Index...>) {
return !anyZero(((sizeof...(Index) %
((std::integral_constant<size_t, Index>().value == 0 ||
std::integral_constant<size_t, Index>().value == 1) ?
MAX_NUMBER + 1 :
std::integral_constant<size_t, Index>().value)))...);
}
template <int Number>
constexpr bool isPrime()
{
if (Number == 0 || Number == 1) { return false; }
return isPrimeImpl(std::make_index_sequence<Number>{});
}
// usage:
isPrime<10>(); // false;
isPrime<7>(); // true;
由于我们不需要计算除以0
和1
的余数我们将0
和1
进行特殊处理,将其变成MAX_NUMBER+1
这样就相当于跳过了0
和1
,同时我们需要对0
和1
进行特殊判断。
有了上面的isPrime
方法,我们只需要在调用一次staticFor
在匿名函数中调用isPrime
进行判断即可:
template<size_t ...Index, class Lambda>
void staticForImpl(Lambda lambda, std::index_sequence<Index...>) {
bool isPrime[sizeof...(Index)] = {(lambda(constexprT<size_t, Index>{}))...};
for (int i = 0; i < sizeof...(Index); i++) {
if (isPrime[i]) {
std::cout << i << " ";
}
}
std::cout << std::endl;
}
template<size_t N, class Lambda>
void staticFor(Lambda lambda) {
staticForImpl(lambda, std::make_index_sequence<N>{});
}
staticFor<MAX_NUMBER>([](auto i) {
return isPrime<i.value>();
});
参考
parameter pack cppreference
C++ 变长模板参数与折叠表达式教学 bilibili
std::get cppreference
std::integral_constant cppreference
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!