我应该声明一个常量而不是编写一个constexpr函数吗?

392
我觉得拥有一个“总是返回5的函数”会破坏或淡化“调用函数”的意义。C++11中肯定有某种原因或需求才会加入这个功能。为什么会有这个功能呢?
// preprocessor.
#define MEANING_OF_LIFE 42

// constants:
const int MeaningOfLife = 42;

// constexpr-function:
constexpr int MeaningOfLife () { return 42; }

我觉得如果我写了一个返回字面值的函数,并且在代码审查中遇到了这个问题,有人会告诉我,我应该声明一个常量值,而不是写 return 5。

29
你能定义一个返回constexpr的递归函数吗?如果可以的话,我可以看到一个用法。 - ereOn
29
我认为这个问题应该表述为“如果编译器可以自行推断函数是否可以在编译时评估,为什么需要引入一个新的关键字(!)”。用“关键字保证”听起来很好,但我认为我更喜欢在可能的情况下无需关键字就能保证。请注意,我的翻译不会改变原文的含义。 - Kos
7
@Kos:对于更加熟悉C++内部机制的人而言,你的问题可能更受欢迎。但我的问题来自一个曾经写过C代码,却不熟悉C++ 2011关键字及其编译器实现细节的角度。能够理解编译器优化和常量表达式推导是一个更高级别的问题。 - Warren P
15
我认为和你想的一样,我的答案是:如果没有constexpr,你怎么(轻松地)知道编译器是否真正对函数进行了编译期求值?我猜你可以检查汇编输出来查看它的处理方式,但是更容易的方法是告诉编译器你需要这种优化。如果由于某些原因它不能为你完成优化,它会给你一个漂亮的编译错误信息,而不是在你期望优化的地方默默地失败。 - Jeremy Friesner
5
你可以同样用“const”这个关键词来表达同样的意思。实际上,“强制语义”是有用的!数组维度是一个典型的例子。 - Lightness Races in Orbit
显示剩余3条评论
15个回答

353

假设它执行的操作稍微复杂一些。

constexpr int MeaningOfLife ( int a, int b ) { return a * b; }

const int meaningOfLife = MeaningOfLife( 6, 7 );

现在你有了一个可以被计算为常量的东西,同时保持良好的可读性,并允许比仅将常量设置为数字更复杂的处理。

它基本上提供了一种良好的维护性,因为它变得更加明显你正在做什么。例如,考虑max(a, b)

template< typename Type > constexpr Type max( Type a, Type b ) { return a < b ? b : a; }

这是一个相当简单的选择,但这意味着如果您使用常量值调用max函数,它将在编译时显式计算而不是运行时。

另一个很好的例子是DegreesToRadians函数。每个人都发现度数比弧度更容易理解。虽然您可能知道180度等于3.14159265(圆周率)弧度,但以下写法更清晰:

const float oneeighty = DegreesToRadians( 180.0f );

这里有很多好的信息:

http://en.cppreference.com/w/cpp/language/constexpr


21
当使用"constexpr"时,它会告诉编译器尝试在编译时计算值,这是一个非常好的点。我很好奇为什么在指定特定优化时,"const"没有提供这个功能?或者说有吗? - TamusJRoyce
12
通常情况下,它会这样做,但并不一定要这样。使用constexpr会强制编译器执行,并在无法执行时返回错误。 - Goz
22
我现在明白了。Sin(0.5)是另一个选项。这个方法很好地替代了C语言的宏定义。 - Warren P
13
我可以看到这可能是一个新的面试问题:解释 const 和 constexpr 关键字的区别。 - Warren P
3
为了记录这一点,我按照上面的方式编写了类似的代码,并将函数从“constexpr”更改为“const”。由于我正在使用Clang3.3、-pedantic-errors和-std=c++11,我希望后者不会编译。但实际上,它与“constexpr”一样编译并运行了。你认为这是Clang的扩展还是C++11规范自此帖子被回答以来已经进行了调整? - Arbalest
显示剩余3条评论

202

介绍

constexpr并不是作为一种告诉实现程序可以在需要常量表达式的上下文中进行评估的方式而引入的;符合规范的实现在C++11之前就已经能够证明这一点。

实现无法证明的是某段代码的意图

  • 开发人员想要用这个实体表达什么?
  • 我们是否应该盲目地允许代码在常量表达式中使用,仅仅因为它恰好可以工作?

没有constexpr,世界会是什么样子?

假设你正在开发一个库,并意识到你想要能够计算区间(0,N]中每个整数的总和。

int f (int n) {
  return n > 0 ? n + f (n-1) : n;
}

意图的缺失

如果在编译时已知传递的参数,编译器可以轻松证明上述函数在常量表达式中是可调用的;但是你并没有声明这个意图 - 它只是碰巧是这样的情况。

现在有人来读你的函数,进行与编译器相同的分析;"哦,这个函数可以在常量表达式中使用!",并编写了以下代码片段。

T arr[f(10)]; // freakin' magic

优化
作为一个库开发者,你决定当调用f时应该缓存结果;谁会想要一遍又一遍地计算相同的值集合呢?
int func (int n) { 
  static std::map<int, int> _cached;

  if (_cached.find (n) == _cached.end ()) 
    _cached[n] = n > 0 ? n + func (n-1) : n;

  return _cached[n];
}

结果
通过引入优化,你刚刚破坏了每个在需要常量表达式的上下文中使用你的函数的情况。
你从未承诺该函数可在常量表达式中使用,而没有constexpr,就无法提供这样的承诺。
所以,我们为什么需要constexprconstexpr的主要用途是声明意图
如果一个实体没有被标记为constexpr - 那就意味着它从未被设计为在常量表达式中使用;即使它被使用了,我们仍然依赖编译器来诊断这样的上下文(因为它会忽略我们的意图)。

32
这可能是正确的答案,因为C++14和C++17的最近变化允许在“constexpr”表达式中使用更广泛的语言范围。换句话说,几乎所有东西都可以被注释为“constexpr”(也许有一天它会因此消失?),除非有一个标准来决定何时使用“constexpr”,否则几乎所有代码都将以这种方式编写。 - alecov
6
不是所有的东西都可以被标记为“constexpr”,比如“输入/输出”、“系统调用”和“动态内存分配”。此外,并不是所有的东西都应该被标记为“constexpr”。 - JiaHao Xu
1
@alecov 有些函数是在运行时执行的,而在编译时执行这些函数是没有意义的。 - JiaHao Xu
3
我也最喜欢这个答案。编译时求值是一个很好的优化,但实际上通过constexpr您获得的是一种行为保证,就像const一样。 - Tomáš Zato
1
哪个编译器允许这个没有constexpr的版本 int f (int n) { return n > 0 ? n + f (n-1) : n;} T arr[f(10)];?我无法在任何地方编译它? - Jer
显示剩余5条评论

95

考虑到某些原因,std::numeric_limits<T>::max()是一种方法。在这里使用constexpr会很有益。

另一个例子:您想声明一个与另一个数组大小相同的C数组(或std::array)。目前的做法如下:

int x[10];
int y[sizeof x / sizeof x[0]];

但是,能够写成以下这样不是更好吗:
int y[size_of(x)];

由于使用 constexpr,您可以:

template <typename T, size_t N>
constexpr size_t size_of(T (&)[N]) {
    return N;
}

23
@Kos: 不行,它会返回一个运行时的值。constexpr 会强制编译器尽可能让函数返回一个编译时的值(如果可以的话)。 - deft_code
16
没有使用 constexpr,它无法用作数组大小声明,也无法作为模板参数,无论函数调用的结果是否为编译时常量。这两种情况基本上是 constexpr 的唯一用例,但至少模板参数的用例相当重要。 - Konrad Rudolph
2
由于C++03中只有编译时整数,而没有其他编译时类型,因此在C++11之前,只有方法可以适用于任意类型,这就是为什么这是一种方法的原因。 - Sebastian Mach
6
不,这并不是“可以”的意思:GCC在某些方面默认情况下宽松一些。使用“-pedantic”选项,它就会被标记为错误。 - Konrad Rudolph
2
@SexyBeast 不确定你的意思?int size 在编译时已知,常量10在编译时已知,因此数组大小也在编译时已知,在运行时没有任何“调用”。 - paulm
显示剩余8条评论

20

constexpr函数非常好用,是C++的一个很棒的补充。然而,你说得对,它所解决的大多数问题都可以通过宏来不太优雅地解决。

然而,constexpr的一个用途在C++03中没有等价物,那就是有类型的常量。

// This is bad for obvious reasons.
#define ONE 1;

// This works most of the time but isn't fully typed.
enum { TWO = 2 };

// This doesn't compile
enum { pi = 3.1415f };

// This is a file local lvalue masquerading as a global
// rvalue.  It works most of the time.  But May subtly break
// with static initialization order issues, eg pi = 0 for some files.
static const float pi = 3.1415f;

// This is a true constant rvalue
constexpr float pi = 3.1415f;

// Haven't you always wanted to do this?
// constexpr std::string awesome = "oh yeah!!!";
// UPDATE: sadly std::string lacks a constexpr ctor

struct A
{
   static const int four = 4;
   static const int five = 5;
   constexpr int six = 6;
};

int main()
{
   &A::four; // linker error
   &A::six; // compiler error

   // EXTREMELY subtle linker error
   int i = rand()? A::four: A::five;
   // It not safe use static const class variables with the ternary operator!
}

//Adding this to any cpp file would fix the linker error.
//int A::four;
//int A::six;

13
你能否说明一下那个“非常微妙的链接错误”是什么?或者至少提供一个澄清的指针? - enobayram
4
三元运算符会使用操作数的地址,代码中并不明显。虽然所有内容都能编译通过,但链接失败是因为无法解析“four”的地址。我得花费很多时间才能确定是谁正在获取我的“static const”变量的地址。 - deft_code
26
这很糟糕的原因显而易见,最显而易见的原因是分号,对吧? - TonyK
4
“极其微妙的链接器错误”让我完全困惑。无论是four还是five都不在作用域内。 - Steven Lu
3
请参考新的 enum class 类型,它解决了一些枚举类型的问题。 - ninMonkey
显示剩余15条评论

15

据我所读,需要constexpr的原因是元编程中的一个问题。Trait类可以将常数表示为函数,比如numeric_limits::max()。有了constexpr,这些类型的函数可以用于元编程或作为数组边界等。

另一个我能想到的例子是,在类接口中,您可能希望派生类型为某些操作定义自己的常量。

编辑:

在SO上翻阅后,看起来其他人已经提出了一些可能使用constexpr实现的示例例子


成为接口的一部分,你必须是一个函数。 - Daniel Earwicker
现在我能看到它的用处,我对C++ 0x更加兴奋了。它似乎是一个经过深思熟虑的东西。我知道他们一定会这样做。那些语言标准的超级极客很少做随意的事情。 - Warren P
我对lambda表达式、线程模型、initializer_list、右值引用、可变参数模板、新的绑定重载等方面更感兴趣...有很多值得期待的东西。 - luke
1
哦,是的,但我已经在其他几种语言中理解了lambda/closure。constexpr在具有强大的编译时表达式求值系统的编译器中更具体地有用。在这个领域,C++确实没有对手。(在我看来,这是对C++11的高度赞扬) - Warren P

11

来自斯特劳斯特鲁普在“Going Native 2012”演讲中的内容:

template<int M, int K, int S> struct Unit { // a unit in the MKS system
       enum { m=M, kg=K, s=S };
};

template<typename Unit> // a magnitude with a unit 
struct Value {
       double val;   // the magnitude 
       explicit Value(double d) : val(d) {} // construct a Value from a double 
};

using Speed = Value<Unit<1,0,-1>>;  // meters/second type
using Acceleration = Value<Unit<1,0,-2>>;  // meters/second/second type
using Second = Unit<0,0,1>;  // unit: sec
using Second2 = Unit<0,0,2>; // unit: second*second 

constexpr Value<Second> operator"" s(long double d)
   // a f-p literal suffixed by ‘s’
{
  return Value<Second> (d);  
}   

constexpr Value<Second2> operator"" s2(long double d)
  // a f-p literal  suffixed by ‘s2’ 
{
  return Value<Second2> (d); 
}

Speed sp1 = 100m/9.8s; // very fast for a human 
Speed sp2 = 100m/9.8s2; // error (m/s2 is acceleration)  
Speed sp3 = 100/9.8s; // error (speed is m/s and 100 has no unit) 
Acceleration acc = sp1/0.5s; // too fast for a human

2
这个例子也可以在斯特鲁斯特鲁普的论文《基础设施软件开发》中找到。 - Matthieu Poullet
clang-3.3:错误:constexpr函数的返回类型“Value <Second>”不是字面类型。 - Mitja
这很好,但是谁会在代码中放置字面量呢?如果你正在编写交互式计算器,让编译器“检查你的单位”是有意义的。 - bobobobo
7
如果你在为火星气候轨道器编写导航软件,也许会这样做 :) - Jeremy Friesner
1
为了使其编译通过 - 1. 在字面量后缀中使用下划线。2. 添加运算符""_m以表示100_m。3. 使用100.0_m,或添加一个接受无符号长整型的重载函数。4. 声明Value构造函数为constexpr。5. 添加相应的Value类运算符/,如下所示:constexpr auto operator / (const Value<Y>& other) const { return Value<Unit<TheUnit::m - Value<Y>::TheUnit::m, TheUnit::kg - Value<Y>::TheUnit::kg, TheUnit::s - Value<Y>::TheUnit::s>>(val / other.val); }。其中TheUnit是在Value类内部添加的Unit类型定义。 - 0kcats

9

另一个尚未提到的用途是constexpr构造函数。这允许创建编译时常量,它们不需要在运行时初始化。

const std::complex<double> meaning_of_imagination(0, 42); 

配合用户定义字面值,你就可以完全支持字面值用户定义类了。
3.14D + 42_i;

8

我刚开始将一个项目转换成c++11,并且遇到了一个非常适合使用constexpr的情况,它可以清理掉执行同一操作的替代方法。这里关键的一点是:只有在函数被声明为constexpr时才能将其放入数组大小声明中。在我所涉及的代码区域中,我可以看到很多情况下都会非常有用。

constexpr size_t GetMaxIPV4StringLength()
{
    return ( sizeof( "255.255.255.255" ) );
}

void SomeIPFunction()
{
    char szIPAddress[ GetMaxIPV4StringLength() ];
    SomeIPGetFunction( szIPAddress );
}

5
这句话也可以这样写:const size_t MaxIPV4StringLength = sizeof("255.255.255.255"); - Superfly Jon
static inline constexpr const auto 可能更好。 - JiaHao Xu
1
@JiaHaoXu:constexpr 意味着 constinlinestatic 不被隐含,因此添加它会改变可见性。 - ShadowRanger

7

以前元编程有一个模式:

template<unsigned T>
struct Fact {
    enum Enum {
        VALUE = Fact<T-1>*T;
    };
};

template<>
struct Fact<1u> {
    enum Enum {
        VALUE = 1;
    };
};

// Fact<10>::VALUE is known be a compile-time constant

我相信,constexpr 的引入使您能够编写此类构造,而无需使用模板和特化、SFINAE 等奇怪的构造,但与您编写运行时函数完全相同,只是保证结果将在编译时决定。

但是请注意:

int fact(unsigned n) {
    if (n==1) return 1;
    return fact(n-1)*n;
}

int main() {
    return fact(10);
}

使用 g++ -O3 编译此代码,您将看到 fact(10) 确实在编译时被计算!

一个支持VLA的编译器(即以C99模式运行的C编译器或具有C99扩展的C++编译器)甚至可以让您执行以下操作:

int main() {
    int tab[fact(10)];
    int tab2[std::max(20,30)];
}

但目前它并不是标准的C++ - constexpr似乎是解决这个问题的一种方式(即使在没有VLA的情况下,也是如此)。而且还存在需要将“正式”的常量表达式作为模板参数的问题。


事实函数在编译时不会被评估。它需要是constexpr,并且必须只有一个返回语句。 - Sumant
1
@Sumant:你说得对,它不必在编译时进行评估,但实际上确实是这样的!我指的是编译器中真正发生的事情。在最近的GCC上编译它,查看生成的汇编代码,并自行检查,如果你不相信我的话! - Kos
尝试添加std::array<int, fact(2)>,你会发现fact()没有在编译时评估。这只是GCC优化器做得很好。 - user283145
2
这就是我说的... 我真的那么不清楚吗?请看最后一段。 - Kos

4
所有其他答案都很棒,我只想举一个关于constexpr的很酷的例子,这个例子非常惊人。See-Phit(https://github.com/rep-movsd/see-phit/blob/master/seephit.h) 是一个在编译时进行HTML解析和模板引擎处理的程序。这意味着你可以将HTML放入其中并输出一棵树,然后对其进行操作。在编译时完成解析可以为您提供一些额外的性能优势。
从Github页面的示例中可以看到:
#include <iostream>
#include "seephit.h"
using namespace std;



int main()
{
  constexpr auto parser =
    R"*(
    <span >
    <p  color="red" height='10' >{{name}} is a {{profession}} in {{city}}</p  >
    </span>
    )*"_html;

  spt::tree spt_tree(parser);

  spt::template_dict dct;
  dct["name"] = "Mary";
  dct["profession"] = "doctor";
  dct["city"] = "London";

  spt_tree.root.render(cerr, dct);
  cerr << endl;

  dct["city"] = "New York";
  dct["name"] = "John";
  dct["profession"] = "janitor";

  spt_tree.root.render(cerr, dct);
  cerr << endl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接