本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

如果说C++11中有什么新东西能拿“最佳困惑奖”的话,那肯定是constexpr了。当把它用在对象上时,它本质上是const的加强版,但是把它用在函数上时,它将拥有不同的意义。切开“迷雾”(解开困惑)是值得的,因为当constexpr符合你想表达的情况时,你肯定会想要使用它的。

从概念上来说,constexpr表明的一个值不只是不变的,它还能在编译期被知道。但是这个概念只是故事的一部分,因为当constexpr应用在函数上时,事情变得比看上去还要微妙。为了避免毁掉后面的惊喜,现在,我只能说你不能假设constexpr函数的返回值是const的,同时你也不能假设这些值能在编译期被知道。也许最有趣的是,这些东西都是特性(是有用的)。对于constexpr函数来说,不需要产生const或能在编译期知道的返回结果是一件好事。

但是,让我们从constexpr对象开始。这些对象确实是常量,也确实能在编译期被知道。(技术上来讲,它们的值是在翻译阶段被决定的,翻译阶段包含了编译期和链接期。除非你要写一个C++的编译器或连接器,不然这都影响不到你,所以你能在编程的时候,开心地假设为constexpr对象的值是在编译期被决定的)

值能在编译器知道是很有用的。它们能代替只读内存,举个例子,尤其是对一些嵌入式系统来说,这是一个相当重要的特性。更广泛的应用就是,当C++要求一个不变的,并且在编译期能知道的整形常量表达式的时候,我们可以使用它(constexpr对象或函数)来替代。这样的场景包括了数组大小的说明,整形模板参数(包括std::array对象的长度),枚举成员的值,alignment说明符,等等。如果你想使用变量来做这些事,你肯定想要把它声明为constexpr,因为编译器会确保它是一个编译期的值:

int sz;								//non-constexpr变量

...

constexpr auto arraySize1 = sz;		//错误!sz的值不是在编译期被知道的

std::array<int, sz> data1;			//错误!同样的问题

constexpr auto arraySize2 = 10;		//对的,10是一个编译期的常量

std::array<int, arraySize2> data2;	//对的,arraySize2是一个constexpr

记住,const不能提供和constexpr一样的保证,因为const对象不需要用“在编译期就知道的”值初始化:

int sz;

...

const auto arraySize = sz;			//对的,arraySize是拷贝自sz的const变量

std::array<int, arraySize> data;	//错误!arraySize的值不能在编译期知道

简单来说,所有的constexpr对象都是const对象,但是不是所有的const对象都是constexpr对象。如果你想让编译器保证变量拥有的值能被用在那些,需要编译期常量的上下文中,那么你就应该使用constexpr而不是const。

当涉及constexpr函数时,constexpr对象的使用范围变得更加有趣。当使用编译期常量来调用这样的函数时,它们产生编译期常量。当用来调用函数的值不能在运行期前得知时,它们产生运行期的值。这听起来好像你知道它们会做什么,但是这么想是错误的。正确的观点是这样的:

  • constexpr函数能被用在要求编译期常量的上下文中,如果所有传入constexpr函数的参数都能在编译期知道,那么结果将在编译期计算出来。如果有任何一个参数的值不能在编译期知道,你的代码就被拒绝(不能在编译期执行)了。

  • 当使用一个或多个不能在编译期知道的值来调用一个constexpr函数时,它表现得就像一个正常的函数,在运行期计算它的值。这意味着你不需要两个函数来表示相同的操作,一个为编译期常量服务,一个为所有的值服务。constexpr函数把这些事都做了。

假设你需要一个数据结构来存放一个运算方式不会改变的实验结果。举个例子,在实验过程中,灯的亮度等级(有高,低,关三种状态),风扇的速度,以及温度也是这样,等等。如果这里有n种环境条件和实验有关,每种条件有三种状态,那么结果的组合数量就是3n。因此对于实验结果的所有的组合进行保存,就需要一个起码有3n的空间的数据结构。假设每个结果都是一个int,那么n就是在编译期已知的(或者说可以计算出来),std::array是一个合理的选择。但是我们需要一个方法来在编译期计算3^n。C++标准库提供了std::pow,这个函数是我们需要的数学函数,但是对于我们的目的来说,它有两点问题。第一,std::pow在浮点类型下工作,但是我们需要一个整形的结果。第二,std::pow不是constexpr(也就是,用编译期的值调用它时,它不能返回一个编译期的结果),所以我们不能用它来明确std::array的大小。

幸运的是,我们能自己写一个我们所需要的pow,我马上就会告诉你怎么实现,但是现在先让我们看一下它是怎么声明以及怎么使用的:

constexpr 									//pow是一个constexpr函数
int pow(int base, int exp) noexcept //永远不会抛出异常
{
... //实现在下面
} constexpr auto numConds = 5; //条件的数量 std::array<int, pow(3, numConds)> results; //结果有3^numConds个函数

回忆一下,pow前面的constexpr不是说pow返回一个const值,它意味着如果base和exp是编译期常量,pow的返回结果能被视为编译期常量。如果base和/或exp不是编译期常量,pow的结果将在运行期计算。这意味着pow不只能在编译阶段计算std::array的大小,它也可以在运行期的时候这么调用:

auto base = readFromDB("base");		//运行期得到这些值
auto exp = readFromDB("exponent"); auto baseToExp = pow(base, exp); //在运行期调用pow

因为当用编译期的值调用constexpr函数时,必须能返回一个编译期的结果,所以有些限制被强加在constexpr函数的实现上。C++11和C++14有不同的限制。

在c++11中,constexpr函数只能包含一条简单的语句:一个return语句。实际上,限制没听起来这么大,因为两个技巧可以用来扩张constexpr函数的表达式,并且这将超过你的想象。第一,条件表达式 “?:”能用来替换if-else语句,然后第二,递归能用来替换循环。因此pow被实现成这样:

constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

这确实可以工作,但是很难想象,除了写函数的人,还有谁会觉得这个函数写得很优雅。在C++14中,constexpr函数的限制大幅度变小了,所以这让下面的实现成为了可能:

constexpr int pow(int base, int exp) noexcept			//C++14
{
auto result = 1;
for(int i - 0; i < exp; ++i) result *= base; return result;
}

constexpr函数由于限制,只能接受和返回literal类型(本质上来说就是,这个类型的值能在编译期决定)。在C++11中,除了void的所有built-in类型都是literal类型,user-defined类型也可能是literal类型。因为构造函数和其他函数也可能是constexpr:

class Point{
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{} constexpr double xValue() const noexcept { return x;}
constexpr double yValue() const noexcept { return y;} void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; } private:
double x, y;
};

这里,Point的构造函数被声明为constexpr,因为如果传入的参数能在编译期知道,则被构造的Point的成员变量的值也能在编译期知道。因此,Point也能被初始化为constexpr:

constexpr Point p1(9.4, 27.7);			//对的,在编译期“执行”constexpr构造函数	

constexpr Point p2(28.8, 5.3);			//也是对的

同样地,getter(xValue和yValue)也能是constexpr,因为如果用一个在编译期就知道的Point对象调用它们(比如,一个constexpr Point对象),则成员变量x和y的值都能在编译期知道。这使得一个constexpr函数能调用Point的getter,然后用这个函数的返回值来初始化一个constexpr对象。

constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
return { (p1.xValue() + p2.xValue()) / 2, //调用constexpr成员函数
(p1.yValue() + p2.yValue()) / 2}; //并通过初始化列表产生一个
//新的临时Point对象
} constexpr auto mid = midpoint(p1, p2); //用constexpr函数的返回值
//来初始化一个constexpr对象

这是很激动人心的,它意味着,虽然mid对象的初始化需要调用构造函数,getter函数和一个non-member函数,但是它还是能在read-only内存中创建!这意味着,你能使用一个表达式(比如mid.xValue() * 10)来明确模板的参数,或者明确enum成员的值。它意味着以前运行期能做的工作和编译期能做的工作之间的界限变得模糊了,一些以前只能在运行期执行的运算现在可以移到编译期来执行了。移动的代码越多,软件跑得越快。(当然编译时间也会增加。)

在C++11中,有两个限制阻止Point的成员函数setX和setY被声明为constexpr。第一,它们改动了它们操作的对象,但是在C++11中,constexpr成员函数被隐式声明为const。第二,它们的返回值类型是void,void类型在C++11中不是literal类型。在C++14中,两个限制都被移除了,所以C++14的Point,能把它的setter也声明为constexpr:

class Point{
public:
... constexpr void setX(double newX) noexcept //C++14
{ x = newX; } constexpr void setY(double newY) noexcept
{ y = newY;} ...
};

这使得我们能写出这样的函数:

constexpr Point reflection(const Point& p) noexcept
{
Point result; //创建一个non-constPoint result.setX(-p.xValue()); //设置它的x和y
result.setY(-p.yValue()); return result; //返回一个result的拷贝
}

客户代码看起来像这样:

constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2); constexpr auto reflectedMid = //reflectedMid的值是(-19.1 -16.5)
reflection(mid); //并且是在编译期知道的

本Item的建议是,只要有可能就使用constexpr,并且现在我希望你能知道这是为什么:比起non-constexpr对象和non-constexpr函数,constexpr对象和constexpr函数都能被用在更广泛的上下文中(一些只能使用常量表达式的地方)。通过“只要有可能就使用constexpr”,你能让你的对象和函数的使用范围最大化。

记住,constexpr是对象接口或函数接口的一部分,constexpr宣称“我能被用在任何需要常量表达式的地方”。如果你声明一个对象或函数为constexpr,客户就有可能使用在这些上下文中(要求常量表达式的地方)。如果你之后觉得对于constexpr的使用是错误的,然后移除了constexpr,这会造成很大范围的客户代码无法编译。(由于调试的原因,增加一个I/O操作到我们的constexpr函数中也会导致同样的问题,因为I/O语句一般不允许在constexpr中使用)“只要有可能就使用constexpr”中的“只要有可能”是说:需要你保证你愿意长时间保持这些对象和函数是constexpr。

你要记住的事
  • constexpr对象是const,对它进行初始化的值需要在编译期知道。
  • 如果使用在编译期就知道的参数来调用constexpr函数,它就能产生编译期的结果。
  • 比起non-constexpr对象和函数,constexpr对象很函数能被用在更广泛的上下文中。
  • constexpr是对象接口或函数接口的一部分。

Item 15: 只要有可能,就使用constexpr的更多相关文章

  1. 读书笔记 effective c++ Item 15 在资源管理类中提供对原生(raw)资源的访问

    1.为什么需要访问资源管理类中的原生资源  资源管理类是很奇妙的.它们是防止资源泄漏的堡垒,没有资源泄漏发生是设计良好的系统的一个基本特征.在一个完美的世界中,你需要依赖这样的类来同资源进行交互,绝不 ...

  2. Effective C++ Item 15 Provide access to raw resources in resource-managing classes

    In last two item, I talk about resource-managing using RAII, now comes to the practical part. Often, ...

  3. item 10: 比起unscoped enum更偏爱scoped enum

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 一般情况下,在花括号中声明一个name(包括变量名,函数名),这个 ...

  4. Android 高级UI设计笔记03:使用ListView实现左右滑动删除Item

    1. 这里就是实现一个很简单的功能,使用ListView实现左右滑动删除Item: (1)当我们在ListView的某个Item,向左滑动显示一个删除按钮,用户点击按钮,即可以删除该项item,并且有 ...

  5. 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

    1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...

  6. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  7. 读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

    智能指针的行为像是指针,但是没有提供加的功能.例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删除堆上的资源.STL容器中的迭代器基本上都是 ...

  8. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计类

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  9. 自己定义滑动删除item的ListView。

    首先继承创建继承ListView和实现OnTouchListener,OnGestureListener的类. 会使用到AbsList中的pointToPosition(int x, int y)方法 ...

随机推荐

  1. .Net4.0 任务(Task)

    任务(Task)是一个管理并行工作单元的轻量级对象.它通过使用CLR的线程池来避免启动专用线程,可以更有效率的利用线程池.System.Threading.Tasks 命名空间下任务相关类一览: 类 ...

  2. Linux显示包含全部的文件系统

    Linux显示包含全部的文件系统 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ df -a 文件系统 1K-blocks 已用 可用 已用% 挂载点 /dev ...

  3. 前端框架Vue入门

    1.Vue简介 Vue是一套构建用户界面的渐进性框架.Vue采用自底向上增量开发的设计,其关注点在图层,与angular的区别就在这里,它关注的是图层,而angular注释的是数据. 2.与React ...

  4. box-sizing -- 盒模型

    项目开发中,在浏览同事的代码,发现他经常用一个属性--box-sizing,很好奇是什么,于是乎,上网查阅资料学了起来. 首先我们先复习一下盒模型的组成:一个div通常由 content(内容)+ma ...

  5. freemarker中的left_pad和right_pad(十五)

    freemarker中的left_pad和right_pad 1.简易说明 (1)left_pad 距左边 (2)right_pad 距右边 (3)当仅仅只有一个参数时,插入的是空白:当有两个参数时, ...

  6. RobotFramework下的http接口自动化Set Request Header 关键字的使用

    Set Request Header 关键字用来设置http请求时的请求头部信息. 该关键字接收两个参数,[ header_name | header_value ] 示例1:设置http请求时的Re ...

  7. es6学习笔记--解构赋值

    昨天学习了es6语法中解构赋值,解构赋值在声明中和函数传参提高了灵活性和便捷性,值得掌握该语法. 概念: ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构.   数组的解构 ...

  8. 【POJ2387】Til the Cows Come Home (最短路)

    题面 Bessie is out in the field and wants to get back to the barn to get as much sleep as possible bef ...

  9. Spring Boot初探之restful服务发布

    一.背景 Spring boot是集服务发布.数据库管理.日志管理等于一身的服务开发框架:是微服务开发的全能小帮手.这章讲述一下如何使用spring boot发布restful服务接口. 二.搭建基础 ...

  10. Pluto - iOS 上一个高性能的排版渲染引擎

    WeTest 导读 Pluto 是 iOS 上的一个排版渲染引擎,通过 JSON/JS 文件可以很方便地描述界面元素,开发效率很高,并且在流畅度,内存等方便有保证.pluto.oa.com 上有更多详 ...