附录一:高级constexpr
在C++
中,编译时和运行时之间的边界是模糊的,这在C++14
中引入泛化常量表达式时更是如此。 然而,能够操纵异构对象就意味着要能深刻理解边界的含义,让代码按自己的意图来运行。 本节的目标是使用constexpr
来设置一些东西; 以了解哪些问题可以解决,哪些不能。 本节涵盖了关于常量表达式的高级概念; 只有对constexpr
有很好理解的读者才应该尝试阅读。
Constexpr stripping
让我们开始一个具有挑战性的问题。 下面的代码可编译吗?
template <typename T>
void f(T t) {
static_assert(t == 1, "");
}
constexpr int one = 1;
f(one);
答案是不能,由Clang
给出的错误就像:
error: static_assert expression is not an integral constant expression
static_assert(t == 1, "");
^~~~~~
对出错的解释是,在f
的函数体内,t
不是常数表达式,因此不能用作static_assert
的操作数。 原因是这样的函数根本不能由编译器生成。 要理解这个问题,考虑当我们使用具体类型实例化f
模板时发生了什么:
// Here, the compiler should generate the code for f<int> and store the
// address of that code into fptr.
void (*fptr)(int) = f<int>;
显然,编译器不能生成f<int>
的代码,如果t!= 1
,它应该触发一个static_assert
,因为我们还没有指定t
的值。 更糟的是,生成的函数应该适用于常量和非常量表达式:
void (*fptr)(int) = f<int>; // assume this was possible
int i = ...; // user input
fptr(i);
显然,不能生成fptr
的代码,因为它需要能够对运行时值进行static_assert
,这是没有意义的。 此外,注意,无论你是否使用constexpr
函数都没关系; 使f constexpr
只声明f
的结果是一个常量表达式,只要它的参数是一个常量表达式,但它仍然不能让你知道你是否使用f
的body
中的常量表达式调用。 换句话说,我们想要的是:
template <typename T>
void f(constexpr T t) {
static_assert(t == 1, "");
}
constexpr int one = 1;
f(one);
在这个假设情况下,编译器将知道t
是来自f
的主体的常量表达式,并且可以使static_asser
t起作用。 然而,当前语言还不constexpr
参数,并且添加它们将带来非常具有挑战性的设计和实现问题。 这个小实验的结论是参数传递剥离了constexpr-ness。 现在可能不清楚的是这种剥离的后果,接下来解释。
Constexpr保存
参数不是常量表达式意味着我们不能将其用作非类型模板参数,数组绑定,static_assert
或需要常量表达式的任何其他地方。 此外,这意味着函数的返回类型不能取决于参数的值,如果你想以这样的形式得到一个新类型:
template <int i>
struct foo { };
auto f(int i) -> foo<i>; // obviously won't work
显然,这行不通。事实上,函数的返回类型只能取决于它的参数的类型,而constexpr
不能改变这个事实。 但根据函数的参数返回具有不同类型的对象对我们至关重要,因为我们对操作异构对象感兴趣。 例如,一个函数可能希望在一种情况下返回类型T
的对象,在另一种情况下返回类型U
的对象; 从以上分析来看,我们现在知道这些“情况”将必须依赖于参数类型编码的信息,而不是它们的值。
为了通过参数传递来保留constexpr
,我们必须将constexpr
值编码为一个类型,然后将一个不一定是该类型的constexpr
对象传递给函数。 该函数必须是模板,然后可以访问在该类型内编码的constexpr
值。
TODO: 改进这个解释,并谈论包装成类型的非整数常量表达式。
副作用
让我提一个棘手的问题。 以下代码是否有效?
template <typename T>
constexpr int f(T& n) { return 1; }
int n = 0;
constexpr int i = f(n);
答案是肯定的,但原因可能不明显。 这里发生的是,我们有一个非常量的值n
和一个constexpr
函数f
和它的引用参数。 大多数人认为它不应该工作的原因是n
不是constexpr
。 但是,我们不在f
内部做任何事情,所以没有什么实质的理由解释它不应该工作! 这有点像在内部的一个constexpr
函数:
constexpr int sqrt(int i) {
if (i < 0) throw "i should be non-negative";
return ...;
}
constexpr int two = sqrt(4); // ok: did not attempt to throw
constexpr int error = sqrt(-4); // error: can't throw in a constant expression
只要throw
出现的代码路径不被执行,调用的结果可以是常量表达式。 同样,我们可以在f
中做任何我们想要的事,只要我们不执行需要访问它的参数n
的代码,因为这不是一个常量表达式:
template <typename T>
constexpr int f(T& n, bool touch_n) {
if (touch_n) n + 1;
return 1;
}
int n = 0;
constexpr int i = f(n, false); // ok
constexpr int j = f(n, true); // error
Clang
给出的第二次调用的错误是:
error: constexpr variable 'j' must be initialized by a constant expression
constexpr int j = f(n, true); // error
^ ~~~~~~~~~~
note: read of non-const variable 'n' is not allowed in a constant expression
if (touch_n) n + 1;
^
让我们现在停下来看看它的游戏规则,并考虑一个更微妙的例子。 以下代码是否有效?
template <typename T>
constexpr int f(T n) { return 1; }
int n = 0;
constexpr int i = f(n);
与我们的初始场景唯一的区别是,f
现在的参数按值而不是按引用传递。 然而,这与上一个函数有所不同。 事实上,我们现在要求编译器创建一个n
的副本,并将此副本传递给f
。 然而,n
不是constexpr
,所以它的值只在运行时知道。 编译器要怎么操作编译一个变量的副本(在编译时),但该变量的值只在运行时才知道? 当然,它不能。 事实上,Clang
给出的错误信息对于发生了什么很清楚:
error: constexpr variable 'i' must be initialized by a constant expression
constexpr int i = f(n);
^ ~~~~
note: read of non-const variable 'n' is not allowed in a constant expression
constexpr int i = f(n);
^
TODO: 解释在常量表达式中不会出现副作用,即使它们产生的表达式不被访问。