什么时候应该在C ++ 11中使用constexpr功能?


337

在我看来,拥有“总是返回5的函数”正在破坏或削弱“调用函数”的含义。一定有原因或需要此功能,否则C ++ 11中不会存在。为什么在那儿?

// preprocessor.
#define MEANING_OF_LIFE 42

// constants:
const int MeaningOfLife = 42;

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

在我看来,如果我编写了一个返回文字值的函数,并且进行了代码审查,则有人会告诉我,我应该声明一个常量值,而不是编写return 5。


28
您是否可以定义一个返回a的递归函数constexpr?如果是这样,我可以看到用法。
ereOn 2011年

20
我认为问题应该指出“如果编译器可以自己推断出函数是否可以在编译时求值,为什么要引入一个新关键字(!)”。拥有“由关键字保证”的听起来不错,但是我想我更希望在不需要关键字的情况下尽可能保证它。
科斯

6
@Kos:可能更喜欢C ++内部知识的人可能会喜欢您的问题,但是我的问题来自一个以前写过C代码但根本不熟悉C ++ 2011关键字或C ++编译器实现细节的人。能够推理出编译器优化和常量表达式推论的问题比这个问题更高级。
沃伦·P

8
@Kos我与您的思路相同,而我的答案是,没有constexpr,您如何(轻松地)知道编译器实际上为您编译了函数评估时间?我想您可以检查程序集的输出以查看其效果,但是将编译器告诉您需要进行这种优化比较容易,并且如果由于某种原因它无法为您完成优化,则会为您提供一个不错的编译效果-错误,而不是默默地未能优化您期望的优化位置。
杰里米·弗里斯纳

3
@Kos:您可以说同样的话const。事实上,授权意图有用的!数组维数就是典型的例子。
Lightness Races in Orbit 2013年

Answers:


302

假设它做了一些复杂的事情。

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度是以弧度为单位,但这样写起来更加清楚:

const float oneeighty = DegreesToRadians( 180.0f );

这里有很多很好的信息:

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


18
很好的一点是它告诉编译器在编译时尝试计算值。我很好奇为什么在指定特定的优化后const不提供此功能?还是呢?
TamusJRoyce 2011年

11
@Tamus:通常,但不是必须的。constexpr强制编译器执行,如果无法执行,则会吐出错误。
Goz

20
我现在看到了。Sin(0.5)是另一个。这样可以很好地替换C宏。
沃伦·P

10
我可以将其视为一个新的面试问题:解释const和constexpr关键字之间的区别。
沃伦·P

2
为了自己记录这一点,我反复编写了与上述类似的代码,其中函数为“ const”而不是“ constexpr”。当我使用Clang3.3,-pedantic-errors和-std = c ++ 11时,我希望后者无法编译。它按照“ constexpr”的情况进行编译和运行。您是否认为这是clang扩展,或者自从回答了这篇文章以来,对C ++ 11规范进行了调整吗?
Arbalest

144

介绍

constexpr并不是为了告诉实现可以在需要常量表达式的上下文中评估某些东西的方式而引入的;一致的实现能够在C ++ 11之前证明这一点。

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

  • 开发人员想用这个实体表达什么?
  • 我们是否应该仅仅因为代码恰好起作用而就盲目地在常量表达式中使用代码?

没有这个世界会是constexpr什么?

假设您正在开发一个库,并且意识到您希望能够计算interval中每个整数的总和(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就无法提供这种保证。


那么,为什么需要constexpr呢?

constexpr的主要用法是声明intent

如果一个实体没有被标记为constexpr-绝不打算在常量表达式中使用它;即使是这样,我们也依靠编译器来诊断此类上下文(因为它无视我们的意图)。


25
这可能是正确的答案,因为C ++ 14和C ++ 17的最新更改允许在constexpr表达式中使用更大范围的语言。换句话说,几乎任何东西都可以注释constexpr(也许有一天它会因为这个原因而消失吗?),除非有人对何时使用constexpr或不使用该标准做出规定,否则几乎所有代码都将这样编写。
alecov '16

4
@alecov绝对不是所有内容... I/Osyscall并且dynamic memory allocation绝对不能标记为“ constexpr此外”,不是所有内容都应该constexpr
徐佳浩

1
@alecov某些函数应在运行时执行,而在编译时这样做是没有意义的。
徐佳浩

1
我也最喜欢这个答案。编译时评估是一个整洁的优化,但是您真正得到的constexpr却是某种行为的保证。就像一样const
托马什Zato -恢复莫妮卡

什么编译器允许此无constexpr版本的 int f (int n) { return n > 0 ? n + f (n-1) : n;} T arr[f(10)]; 我无法在任何地方进行编译?

91

采取std::numeric_limits<T>::max():无论出于何种原因,这都是一种方法。constexpr这将是有益的。

std::array再举一个例子:您想声明一个C数组(或一个与另一个数组一样大的数组)。目前执行此操作的方法如下:

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;
}

1
+1可以很好地使用模板,但是在没有constexpr的情况下可以完全相同地工作,不是吗?
科斯

21
@Kos:否。它将返回运行时值。 constexpr强制编译器使函数返回编译时值(如果可以)。
deft_code 2011年

14
@Kos:没有constexpr函数,无论函数调用的结果是否为编译时常量,都不能在数组大小声明中用作模板参数,也不能将其用作模板参数。这两个基本上是唯一的用例,constexpr但是至少模板参数用例是一种重要的用例。
Konrad Rudolph

2
“无论出于何种原因,这都是一种方法”:原因是C ++ 03中只有编译时间整数,而没有其他编译时间类型,因此只有一种方法可用于C ++ 11之前的任意类型。
塞巴斯蒂安·马赫

5
@LwCui不,不是“好的”:默认情况下,GCC在某些方面是松懈的。使用该-pedantic选项,它将被标记为错误。
康拉德·鲁道夫

19

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;

12
您能否说明“极端细微的链接器错误”?或至少提供澄清的指针?
enobayram

4
@enobayram,三元运算符获取操作数的地址。从代码中看不出来。一切编译正常,但链接失败,因为的地址four无法解析。我必须真正挖掘出谁在使用我的static const变量地址。
deft_code

23
“这很明显是有原因的”:最明显的原因是分号,对吗?
TonyK

4
“极端细微的链接器错误”使我完全困惑。既不four也不five在范围内。
史蒂文·卢

3
另请参见新enum class类型,它修复了一些枚举问题。
ninMonkey 2013年

14

根据我的阅读,对constexpr的需求来自元编程中的一个问题。特性类可能具有表示为函数的常量,请考虑:numeric_limits :: max()。使用constexpr,这些类型的函数可用于元编程,或用作数组边界等。

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

编辑:

反复研究SO之后,似乎其他人提出了一些 有关constexprs可能实现的示例


“要成为接口的一部分,您必须成为一个函数”?
Daniel Earwicker 2011年

现在,我已经看到了这样做的用处,我对C ++ 0x感到更加兴奋。似乎是经过深思熟虑的事情。我知道他们一定是。那些语言标准的超级极客很少做随机的事情。
沃伦·P

我对lambda,线程模型,initializer_list,rvalue引用,可变参数模板,新的绑定重载感到更加兴奋……还有很多值得期待的地方。
卢克

1
哦,是的,但我已经了解其他几种语言中的lambda / closures。 constexpr在具有强大的编译时表达式评估系统的编译器中更有用。C ++在该领域确实没有同行。(这是对C ++ 11,恕我直言的强烈赞誉)
Warren P

11

摘自Stroustrup在“ 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
该示例也可以在Stroustrup的论文《基础设施软件开发》中找到
Matthieu Poullet

clang-3.3:错误:constexpr函数的返回类型'Value <Second>'不是文字类型
Mitja

很好,但是谁将文字放在这样的代码中。如果要编写交互式计算器,让编译器为您“检查单位”将很有意义。
bobobobo

5
@bobobobo,或者如果您正在为“火星气候轨道器”编写导航软件,也许是:)
杰里米·弗里斯纳

1
要使其编译-1.在文字后缀中使用下划线。2.将运算符“” _m添加为100_m。3.使用100.0_m,或添加一个接受unsigned long long的重载。4.声明值构造函数constexpr。5.像这样将对应的运算符/添加到Value类:constexpr自动运算符/((const Value <Y>&other)const {返回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的typedef。
0kcats

8

constexpr构造函数是另一个用途(尚未提及)。这样就可以创建在运行时不必初始化的编译时间常数。

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

将其与用户定义的文字配对,即可完全支持文字的用户定义类。

3.14D + 42_i;

6

元编程曾经有一个模式:

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,并且只能有一个return语句。
Sumant 2011年

1
@Sumant:您是对的,它不必在编译时进行评估,但是它是正确的!我指的是编译器中真正发生的事情。在最近的GCC上编译它,查看生成的asm,如果您不相信我,请自己检查!
科斯,

尝试添加std::array<int, fact(2)>,您会发现在编译时未评估fact()。只是GCC优化器做得很好。

1
那就是我说的...我真的不清楚吗?参见最后一段
Kos 2012年

5

刚刚开始将项目切换到c ++ 11,并且遇到了constexpr的一个非常好的情况,它清除了执行相同操作的替代方法。这里的关键点是,只有在将函数声明为constexpr时,才能将其放入数组大小的声明中。在许多情况下,我可以看到我在涉及的代码领域中前进非常有用。

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

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

4
同样可以这样写:const size_t MaxIPV4StringLength = sizeof(“ 255.255.255.255”);
Superfly Jon

static inline constexpr const auto可能更好。
徐佳浩

3

所有其他答案都很棒,我只想举一个很酷的例子,说明您可以用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;
}

1

您的基本示例与常量本身的说法相同。为什么使用

static const int x = 5;
int arr[x];

过度

int arr[5];

因为它更易于维护。与现有的元编程技术相比,使用constexpr的写入和读取速度要快得多。


0

它可以启用一些新的优化。 const传统上是对类型系统的提示,并且不能用于优化(例如,const成员函数const_cast无论如何const都可以合法地修改对象,因此不能被信任进行优化)。

constexpr表示表达式确实是常量,前提是该函数的输入为const。考虑:

class MyInterface {
public:
    int GetNumber() const = 0;
};

如果这是在其他模块中公开的,则编译器无法相信GetNumber()每次调用时都不会返回不同的值-即使连续两次,也不会在它们之间进行任何非const调用-因为const也可能已经在实现中抛弃了。(显然,任何这样做的程序员都应该受到枪击,但是语言允许这样做,因此编译器必须遵守规则。)

新增constexpr

class MyInterface {
public:
    constexpr int GetNumber() const = 0;
};

编译器现在可以在GetNumber()缓存返回值的地方进行优化,并消除对的其他调用GetNumber(),因为这constexpr是对返回值不会改变的有力保证。


实际上const 可以用于优化... 即使在IIRC 之后,修改值定义的const也是未定义的行为const_cast。我希望它对于const成员函数是一致的,但是我需要用标准检查一下。这意味着编译器可以在那里安全地进行优化。
科斯

1
@Warren:优化是否实际完成并不重要,只是允许这样做。@Kos:这是一个鲜为人知的精妙之处,如果原来的对象是声明为const(int xconst int x),则可以安全通过修改它const_cast的指针/引用它-ing走常量。否则,const_cast将始终调用未定义的行为,并且将是无用的:)在这种情况下,编译器没有有关原始对象的常量性的信息,因此无法分辨。
AshleysBrain 2011年

@Kos我不认为const_cast是这里唯一的问题。允许const方法读取甚至修改全局变量。相反,来自另一个线程的人也可以在两次调用之间修改const对象。
enobayram 2013年

1
“ = 0”在此处无效,应将其删除。我会自己做,但是我不确定这是否符合SO协议。
KnowItAllWannabe 2014年

这两个示例均无效:第一个(int GetNumber() const = 0;)应该将GetNumber()方法声明为virtual。第二个(constexpr int GetNumber() const = 0;)无效,因为纯说明符(= 0)表示该方法是虚拟的,但是constexpr的方法不能是虚拟的(ref:en.cppreference.com/w/cpp/language/constexpr
stj 2016年

-1

何时使用constexpr

  1. 只要有编译时间常数。

虽然我同意您的意见,但这个答案并未说明为什么 constexpr应该优先于预处理器宏或const
Sneftel

-3

对于类似的东西很有用

// constants:
const int MeaningOfLife = 42;

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

int some_arr[MeaningOfLife()];

用特质类之类将其绑定在一起,它将变得非常有用。


4
在您的示例中,它比纯常量具有零优势,因此它并不能真正回答问题。
jalf

这是一个人为的示例,想象一下MeaningOfLife()是否从其他地方获取值,例如另一个函数或#define或series therof。您可能不知道它返回什么,它可能是库代码。在其他示例中,设想一个具有constexpr size()方法的不可变容器。您现在可以执行int arr [container.size()];
plivesey 2011年

2
@plivesey您可以再用一个更好的例子来编辑答案。
Mukesh
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.