Effective Modern C++读书笔记:条款15

Aki 发布于 2023-01-12 206 次阅读


15只要有可能使用constexpr,就使用它:

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

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

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

int sz;   //未初始化,没有确定的值,是non-constexpr变量

constexpr int ss = 10;  //是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函数时,constexpr对象的使用范围变得更加有趣。当使用编译期常量来调用这样的函数时,它们产生编译期常量。当用来调用函数的值不能在运行期前得知时,它们产生运行期的值。这听起来好像你知道它们会做什么,但是这么想是错误的。正确的观点是这样的:

  • constexpr函数能被用在要求编译期常量的上下文中,如果所有传入constexpr函数的参数都能在编译期知道,那么结果将在编译期计算出来。如果有任何一个参数的值不能在编译期知道,你的代码就被拒绝(不能在编译期执行)了。
  • 当使用一个或多个不能在编译期知道的值来调用一个constexpr函数时,它表现得就像一个正常的函数,在运行期计算它的值。这意味着你不需要两个函数来表示相同的操作,一个为编译期常量服务,一个为所有的值服务。constexpr函数把这些事都做了。

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 Point{ (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。

本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是对象接口或函数接口的一部分。