何时以及为什么要在constexpr中使用static
When and why would you use static with constexpr?
作为免责声明,我在提出这个问题之前做了我的研究。我发现了一个类似的问题,但答案感觉有点"稻草人",并没有真正回答我个人的问题。我也参考了我方便的cppreference页面,但大多数时候它并没有提供一个非常"简化"的解释。
基本上我仍然在提高constexpr
,但目前我的理解是,它需要表达式在编译时进行评估。因为它们可能只在编译时存在,所以它们在运行时不会真正拥有内存地址。所以当我看到人们使用static constexpr
(比如在课堂上)时,我很困惑……static
在这里是多余的,因为它只对运行时上下文有用。
我在"constexpr
不允许除了编译时表达式之外的任何东西"语句中看到了矛盾(特别是在SO这里)。然而,Bjarne Stroustrup页面上的一篇文章通过各种示例解释了事实上constexpr
确实要求在编译时对表达式求值。如果没有,将生成编译器错误。
我的前一段似乎有点偏离主题,但这是一个必要的基线,以理解为什么static
可以或应该与constexpr
一起使用。不幸的是,这个基线有很多相互矛盾的信息。
谁能帮我把所有这些信息整合成纯粹的事实,用例子和有意义的概念?除了理解constexpr
的真正行为之外,为什么要使用static
呢?如果它们可以一起使用,static constexpr
在哪些范围/场景下是有意义的?
函数级静态变量有一个显著的区别,这与lambda-capture有关:
void odr_use(int const&);
int main() {
int non_static = 42;
static int is_static = 42;
[]{
odr_use(non_static); // error
odr_use(is_static); // OK
}();
}
允许在lambdas中使用函数局部静态变量而不捕获它们。这与constexpr
没有任何关系——然而,强制捕获constexpr
变量通常没有什么意义。因此,static
+constexpr
在从lambdas访问常量时提供了一些安慰,考虑:
#include <string_view>
int main()
{
constexpr std::string_view x = "foo";
[]{ x.data(); }; // error: odr-use of non-captured variable
}
在这个例子中,字符串视图和它的内容是常量。然而,使用成员函数会触发odr-use,这需要我们捕获变量。或者,使用static
+constexpr
。
多用途意思是"根据单一定义规则使用",它可以归结为"是该操作所需对象的地址"。对于成员函数,需要该地址来形成this
-指针。
下面你会发现概念上的差异,这也解释了上面提到的效果。
constexpr变量不是编译时值
一个值是不可变的,不占用存储空间(没有地址),然而,声明为constexpr
的对象可以是可变的,并且确实占用存储空间(在as-if规则下)。
可变性
大多数声明为constexpr
的对象都是不可变的,但是可以定义一个(部分)可变的constexpr
对象,如下:
struct S {
mutable int m;
};
int main() {
constexpr S s{42};
int arr[s.m]; // error: s.m is not a constant expression
s.m = 21; // ok, assigning to a mutable member of a const object
}
<<h3>存储/h3>在as-if规则下,编译器可以选择不分配任何存储空间来存储声明为constexpr
的对象的值。类似地,它也可以对非constexpr变量进行这样的优化。但是,考虑一下我们需要将对象的地址传递给非内联函数的情况;例如:
struct data {
int i;
double d;
// some more members
};
int my_algorithm(data const*, int);
int main() {
constexpr data precomputed = /*...*/;
int const i = /*run-time value*/;
my_algorithm(&precomputed, i);
}
编译器需要为precomputed
分配存储空间,以便将其地址传递给一些非内联函数。编译器可以为precomputed
和i
连续分配存储空间;可以想象这种情况可能会影响性能(见下文)。
Standardese
变量可以是对象或引用[basic]/6。让我们关注对象。
像constexpr int a = 42;
这样的声明在语法上是简单声明;它由decl- specific -seqinit-declarator-list;
组成从(dcl。Dcl]/9,我们可以得出结论(但不是严格地),这样的声明声明了一个对象。具体来说,我们可以(严格地)断定它是一个对象声明,但这包括引用声明。参见是否可以有void
类型的变量的讨论。
对象声明中的constexpr
暗示对象的类型是const
[dcl.constexpr]/9。对象是存储[intro.object]/1的一个区域。我们可以从[引言]中推断。[对象]/6和[介绍]。Memory]/1表示每个对象都有一个地址。注意,我们可能不能直接获取这个地址,例如,如果对象是通过右值引用的。(甚至还有不是对象的前值,比如字面量42
。)两个完全不同的对象必须有不同的地址[introobject]/6.
从这里,我们可以得出结论,声明为constexpr
的对象必须具有相对于的唯一地址任何其他(完整)对象
进一步,我们可以得出结论,声明constexpr int a = 42;
声明了一个具有唯一地址的对象。
static和constexpr
我认为唯一有趣的问题是"按功能static
",
void foo() {
static constexpr int i = 42;
}
据我所知——但这似乎仍然不完全清楚——编译器可能在运行时计算constexpr
变量的初始化项。但这似乎是病态的;让我们假设没有这样做,也就是说,它会在编译时预先计算初始化式。
static constexpr
局部变量的初始化是在静态初始化期间完成的。这必须在任何动态初始化[basic.start.init]/2之前执行。虽然不能保证,但我们可以假设这不会造成运行时/加载时成本。此外,由于常量初始化没有并发问题,我认为我们可以安全地假设这不需要线程安全的运行时检查static
变量是否已经初始化。(查看clang和gcc的源代码应该会对这些问题有所启发。)
对于非静态局部变量的初始化,在某些情况下,编译器无法在常量初始化期间初始化变量:
void non_inlined_function(int const*);
void recurse(int const i) {
constexpr int c = 42;
// a different address is guaranteed for `c` for each recursion step
non_inlined_function(&c);
if(i > 0) recurse(i-1);
}
int main() {
int i;
std::cin >> i;
recurse(i);
}
结论看起来,在某些极端情况下,我们可以从static constexpr
变量的静态存储持续时间中获益。然而,我们可能会失去这个局部变量的局部性,如"storage"一节所示。这个答案。直到我看到一个基准,表明这是一个真实的效果,我将假定这是无关的。
如果static
对constexpr
对象只有这两种效果,我会默认使用static
:我们通常不需要保证constexpr
对象的唯一地址。
对于可变constexpr
对象(包含mutable
成员的类类型),本地static
和非静态constexpr
对象之间的语义明显不同。同样,如果地址本身的值是相关的(例如,用于哈希映射查找)。
仅供示例。社区维基。
static
== per-function(静态存储时长)
声明为constexpr
的对象和其他对象一样有地址。如果出于某种原因,使用了对象的地址,编译器可能必须为它分配存储空间:
constexpr int expensive_computation(int n); // defined elsewhere
void foo(int const p = 3) {
constexpr static int bar = expensive_computation(42);
std::cout << static_cast<void const*>(&bar) << "n";
if(p) foo(p-1);
}
变量的地址对于所有调用都是相同的;每个函数调用都不需要堆栈空间。比较:
void foo(int const p = 3) {
constexpr int bar = expensive_computation(42);
std::cout << static_cast<void const*>(&bar) << "n";
if(p) foo(p-1);
}
这里,对于foo
的每次(递归)调用,地址将是不同的。
这很重要,例如,如果对象很大(例如数组),并且我们需要在需要常量表达式(需要编译时常量)并且需要获取其地址的上下文中使用它。
注意,由于地址必须不同,对象可能在运行时被初始化;例如,如果递归深度取决于运行时参数。初始化器仍然可以预先计算,但是对于每个递归步骤,结果可能必须复制到新的内存区域中。在这种情况下,constexpr
只保证初始化式可以在编译时求值,并且初始化可以在编译时对该类型的变量执行。
static
== per-class
template<int N>
struct foo
{
static constexpr int n = N;
};
与往常一样:为foo
的每个模板特化(实例化)声明一个变量,例如foo<1>
,foo<42>
,foo<1729>
。如果你想公开非类型模板形参,你可以使用静态数据成员。它可以是constexpr
,这样其他人就可以从编译时已知的值中受益。
static
==内部联动
// namespace-scope
static constexpr int x = 42;
相当冗余;默认情况下,constexpr
变量具有内部链接。在这种情况下,我看不出有任何理由使用static
。
我使用static constexpr
作为未命名枚举的替代,在我不知道确切的类型定义,但希望查询有关类型的一些信息(通常在编译时)的地方。
编译时未命名枚举还有一些额外的好处。更容易调试(值在调试器中显示为"正常"变量)。此外,您可以使用任何可以constexpr构造的类型(不仅仅是数字),而不仅仅是带有enum的数字。
例子:
template<size_t item_count, size_t item_size> struct item_information
{
static constexpr size_t count_ = item_count;
static constexpr size_t size_ = item_size;
};
现在,您可以在编译时访问这些变量:
using t = item_information <5, 10>;
constexpr size_t total = t::count_ * t::size_;
选择:
template<size_t item_count, size_t item_size> struct item_information
{
enum { count_ = item_count };
enum { size_ = item_size };
};
template<size_t item_count, size_t item_size> struct item_information
{
static const size_t count_ = item_count;
static const size_t size_ = item_size;
};
替代方法不具有静态constexpr的所有优点——您可以保证编译时处理、类型安全以及(潜在的)更低的内存使用(constexpr变量不需要占用内存,除非可能,它们是有效的硬编码)。
除非你开始获取constexpr变量的地址(即使你仍然这样做),否则你的类不会像标准静态const那样增加大小。
- lambda参数转换为constexpr技巧,然后获取带链接的数组
- 多成员Constexpr结构初始化
- 条件constexpr函数
- 为什么即使使用-cudart-static进行编译,库用户仍然需要链接到cuda运行时
- constexpr 函数中的非文字(通过 std::is_constant_evaluated)
- Visual C++ constexpr Hints
- 如何确认我的constexpr表达式实际上已经在编译时执行
- 为什么constexpr的性能比正常表达式差
- 是否可以使用if constexpr删除控制流语句
- 要与"if constexpr"一起使用的编译时消息(在预处理器之后)
- "static initialization order fiasco"是 constexpr 变量的问题吗?
- constexpr static std::array<const char *,5> 无法使用 MSVC2013 进行编译
- google-test and static constexpr member
- visual c++ static polymorphism (CRTP) 在评估"静态 constexpr&
- Static constexpr of set
- c++14 static constexpr auto with odr usage
- 继承自 std::true_type vs static constexpr const bool 成员
- 何时以及为什么要在constexpr中使用static
- ' static constexpr auto '数据成员初始化为未命名的枚举
- 使用static、const、constexpr的全局声明/初始化