C++的杂七杂八:constexpr与编译期计算

阅读: 评论:0

C++的杂七杂八:constexpr与编译期计算

C++的杂七杂八:constexpr与编译期计算

引言

社区(/)里有朋友提出了编译期分割字符串的需求,大家在群里进行了热烈的讨论,也做了许多尝试,但并没有得出确定的结果。本文作者试图对C++11/14里的新关键字constexpr进行编译期计算的总结和探讨,并在最后结合constexpr给出C++在编译期分割字符串的方法。

一、编译期计算

我们先来看一段求阶乘(factorial)的算法:

size_t factorial(size_t n) noexcept
{return (n == 0) ? 1 : n * factorial(n - 1);
}

很明显,这是一段运行期算法。程序运行的时候,传递一个值,它可以是一个变量,也可以是一个常量:

int main(void)
{std::cout << factorial(10) << std::endl;return 0;
}

如果程序仅仅像这样传递常量,我们可能会希望能够让它完全在编译的时候就把结果计算出来,那么代码改成这样或许是个不错的选择:

template <size_t N>
struct factorial
{enum : size_t { value = N * factorial<N - 1>::value };
};template <>
struct factorial<0>
{enum : size_t { value = 1 };
};int main(void)
{std::cout << factorial<10>::value << std::endl;return 0;
}

只是用起来会稍显麻烦点,但好处是运行期没有任何时间代价。

像上面这种运用模板的做法,算是最简单的模板元编程了。对C++模板来说,类型和值是同一种东西;同时,又由于C++的模板有了“Pattern Matching”(即特化和偏特化),同时又允许模板的递归结构(见上面factorial中使用factorial的情况),于是C++的模板是图灵完全的一种独立于C++的语言。理论上来说,我们可以利用它在编译期完成所有计算——前提是这些计算的输入都是literal的。

二、C++11以后的新限定符:constexpr

从C++11开始,我们有了constexpr specifier。它可以被用于变量,及函数上,像这样:

template <size_t N>
struct t_factorial_
{enum : size_t { value = N * t_factorial_<N - 1>::value };
};template <>
struct t_factorial_<0>
{enum : size_t { value = 1 };
};template <size_t N>
constexpr auto t_factorial = t_factorial_<N>::value;int main(void)
{std::cout << t_factorial<10> << std::endl;return 0;
}

当然了,上面更直接的用法是这样:

constexpr size_t c_factorial(size_t n) noexcept
{return (n == 0) ? 1 : n * c_factorial(n - 1);
}

在C++11中,constexpr还有诸多限制,但到了C++14,它似乎有点过于强大了。比如我们可以在函数中写多行语句,定义变量,甚至是循环:

// runtime version
template <typename T, size_t N>
size_t r_count(T&& v, const T(&arr)[N]) noexcept
{size_t r = 0;for (const auto& a : arr) if (v == a) ++r;return r;
}// constexpr version
template <typename T, size_t N>
constexpr size_t c_count(T&& v, const T(&arr)[N]) noexcept
{size_t r = 0;for (const auto& a : arr) if (v == a) ++r;return r;
}

就如同我们在写的只是一个普通函数,之后在函数的最前面加上constexpr它马上就可以在编译期执行了。

constexpr同样带来了强大的类型计算能力。我们简单的来看个例子,实现一个“types_insert”(Reference:C++的杂七杂八:使用模板元编程操作类型集合):

template &>
struct types {};template <typename T,  U>
constexpr auto insert(types&>) noexcept
{return types<T, U...>{};
}

可以看到,对于这种简单的类型计算,constexpr比模板元的实现要清晰很多。
不过,由于函数模板缺少偏特化,因此需要编译期分支判断的“types_assign”是没办法直接写出来的(在这里无法短路求值的std::conditional并没有什么用)。要知道,函数重载虽然强大,但仅能做编译期类型,而不是数值的Pattern Matching,这点是不如类模板的偏特化/特化的。

不过我们可以利用类模板的偏特化来模拟函数模板的偏特化:

template <int N, typename T>
struct impl_
{constexpr static auto assign(void) noexcept{return insert<T>(impl_<N - 1, T>::assign());}
};template <typename T>
struct impl_<0, T>
{constexpr static auto assign(void) noexcept{return types<>{};}
};template <int N, typename T>
constexpr auto assign(void) noexcept
{return impl_<N, T>::assign();
}

但是说实话,我并不喜欢这样,这种写法丧失了函数模板的简洁性。一般来说,大家也不会用constexpr做太复杂的类型计算,这里反而用模板元来做会更加清晰些。从上面可以看出来,在做类型计算的时候,return返回的数值并不是我们需要的,而类型结果一般会用decltype取出来。在这种情况下,不使用constexpr,仅用普通函数都是可以的。

真正让人眼前一亮的,应该还是上面c_count的写法。利用模板元做数值计算其实是它的短板。撰写复杂不说,还有不少的局限性。

比如c_count可以这样用:

template <size_t N>
struct Foo { enum : size_t { value = N }; };int main(void)
{std::cout << Foo<c_count(',', "1, 2, 3, 4, 5, 6, 7, 8, 9, 0")>::value << std::endl;return 0;
}

而模板元对string literal这类数值做计算是比较麻烦的,template non-type arguments被限制为常整数(包括枚举),或指向外部链接对象的指针(严格来说不止这

本文发布于:2024-02-02 21:57:36,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170688225646703.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:杂七杂八   constexpr
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23