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

本文详细介绍了C++11/14的constexpr关键字,探讨了其在编译期计算和类型计算中的应用,包括实现编译期字符串split功能。通过实例展示了constexpr如何简化代码并提高性能,同时讨论了其在不同编译器下的表现和限制。
摘要由CSDN通过智能技术生成

引言

社区(http://purecpp.org/)里有朋友提出了编译期分割字符串的需求,大家在群里进行了热烈的讨论,也做了许多尝试,但并没有得出确定的结果。本文作者试图对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 <typename...>
struct types {};

template <typename T, typename... U>
constexpr auto insert(types<U...>) 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被限制为常整数(包括枚举),或指向外部链接对象的指针(严格来说不止这

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐门教主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值