0
点赞
收藏
分享

微信扫一扫

Item 15: Use constexpr whenever possible.

zhongjh 2022-02-28 阅读 34
c++

Item 15: Use constexpr whenever possible.

Effective Modern C++ Item 15 的学习和解读。

constexpr 是 C++11 中令人非常非常困惑的一个新特性。从概念上讲,它不止表明一个对象是 const 的,而且是在编译时被知道的。把它用在对象上,可以理解成 const 的加强版。把它用在函数时,将拥有完全不同的涵义。

constexpr 对象

先从 constexpr 对象开始,这个对象是 const 的,也是在编译时被知道(准确的说,应该是在翻译阶段被知道,翻译阶段包括编译和链接两个阶段)。

对象的值在编译时知道是非常有用的。它能被存储在只读内存中,尤其是对一些嵌入式系统来说,这是一个相当重要的特性。更广泛的应用包括用于指定数组的大小、整形模板参数、枚举成员值、对齐说明符等。如果你希望用变量做这些事情,那么将他们申明为 constexpr,编译器会保证它们是在编译时可以被知道的值:

int sz;                             // non-constexpr variableconstexpr auto arraySize1 = sz;     // error! sz's value not known at compilation
std::array<int, sz> data1;          // error! same problem
constexpr auto arraySize2 = 10;     // fine, 10 is a compile-time constant
std::array<int, arraySize2> data2;  // fine, arraySize2 is constexpr

但是 const 无法像 constexpr 一样保证值可以在编译时被知道,因为 const 对象不需要用一个编译时知道的值去初始化:

int sz;                            // as beforeconst auto arraySize = sz;        // fine, arraySize is const copy of sz
std::array<int, arraySize> data;  // error! arraySize's value not known at compilation

简单来说,所有的 constexpr 对象都是 const 的,但并非所有的 const 对象都是 constexpr 的。如果你希望编译器保证变量的值能够用在需要在编译时常量的上下文中时,那么你应该使用 constexpr 而不是 const。

constexpr 函数

当涉及道 constexpr 函数时,constexpr 对象的使用场景变得更加有趣。当使用编译时常量来调用 constexpr 函数时,它们产生编译时常量。当用来调用函数的值不能在运行时前得知时,它们产生运行时的值。完整的理解如下:

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

看下面的例子:

constexpr int pow(int base, int exp) noexcept // pow's a constexpr func that never throws
{// impl is below
}
constexpr auto numConds = 5;                // # of conditions
std::array<int, pow(3, numConds)> results;  // results has 3^numConds elements

pow 前面的 constexpr 意味着如果 base 和 exp 是编译时常量,pow 的返回结果可以用作编译时常量。如果 base 或 exp 不是编译时常量,pow 的结果将在运行时计算。这意味着 pow 不仅能在编译时计算std::array的大小,它也可以在运行时这么调用:

auto base = readFromDB("base");    // get these values at runtime
auto exp = readFromDB("exponent"); 
auto baseToExp = pow(base, exp);  // call pow function at runtime

当用编译时值调用 constexpr 函数,则要求该函数能够返回一个编译时值。一些限制会影响 constexpr 函数的实现,C++11 和 C++14 的限制是不同的。

C++11中,constexpr 函数只能有一条 return 语句。可以通过一些手段来扩展 constexpr 的 return 语句。一个问号表达式,一个是递归:

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

C++14 的限制则大幅减小,可以实现如下:

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 类型外的所有内置类型都是 literal 类型,用户自定义类型也可能是 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); // fine, "runs" constexpr ctor during compilation
constexpr Point p2(28.8, 5.3); // also fine

类似的,getX 和 getY 也是 constexpr 的,他们的返回值也可以被用来构造 constexpr 对象:

constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
 return { (p1.xValue() + p2.xValue()) / 2,    // call constexpr member funcs
          (p1.yValue() + p2.yValue()) / 2 }; 
}
constexpr auto mid = midpoint(p1, p2); // init constexpr object w/result of constexpr function

这意味着以前运行时能做的工作和编译时能做的工作之间的界限变得模糊了,一些以前只能在运行时执行的运算现在可以提前到编译是来执行了。提前的代码越多,软件的性能越好。当然编译时间也会相应增加。

C++11中,有两个限制阻止 Point 的成员函数 setX 和 setY 无法成为 constexpr。

  • 第一,它们修改了操作的对象。但是在C++11中,constexpr 成员函数被隐式声明为 const。
  • 第二,它们的返回值类型是 void,void 类型在C++11中不是 literal 类型。

在 C++14 中,去掉了这两个限制。因此,C++14 的 Point类中的 setX 和 setY 可以成为 constexpr:

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

这让下面的代码成为可能:

// return reflection of p with respect to the origin (C++14)
constexpr Point reflection(const Point& p) noexcept
{
  Point result; // create non-const Point
  result.setX(-p.xValue()); // set its x and y values
  result.setY(-p.yValue());
  return result; // return copy of it
}

调用代码可能是这样的:

constexpr Point p1(9.4, 27.7); 
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
// reflectedMid's value is (-19.1 -16.5) and known during compilation
constexpr auto reflectedMid = reflection(mid);  

因此,通过上面的介绍,我们了解了为什么要尽可能地使用 constexpr 了。

最后,总结下:

  • constexpr 对象是 const 的,给它初始化的值需要在编译时知道。
  • 如果使用在编译时就知道的参数来调用 constexpr 函数,它就能产生编译时的结果。
  • 相较于 non-constexpr 对象和函数,constexpr 对象很函数能被用在更广泛的上下文中。
  • constexpr 是对象接口或函数接口的一部分。
举报

相关推荐

0 条评论