作为免责声明,我在询问前已对此进行过研究。我找到了a similar SO question但是那里的答案感觉有点“稻草人”,并没有真正回答我个人的问题。我也提到了我的方便cppreference page,但这并没有提供对事情的非常“愚蠢”的解释。
基本上我仍然在constexpr
上升,但目前我的理解是它需要在编译时评估表达式。由于它们可能仅在编译时存在,因此它们在运行时不会真正具有内存地址。所以当我看到人们使用static constexpr
时(例如在类中)会让我感到困惑...... static
在这里会是多余的,因为这只对运行时上下文有用。
我看到“constexpr
中除了编译时表达式之外不允许任何其他内容”的声明(特别是在SO处)。但是,an article from Bjarne Stroustrup's page在各种示例中解释了实际上constexpr
需要在编译时评估表达式。如果不是,则应生成编译器错误。
我之前的段落似乎有点偏离主题,但它是理解为什么static
可以或应该与constexpr
一起使用所必需的基线。不幸的是,这个基线有很多矛盾的信息。
任何人都可以帮助我将所有这些信息整合到纯粹的事实中,并提供有意义的示例和概念吗?基本上在了解constexpr
真实行为的方式的同时,为什么要使用static
呢?通过static constexpr
的范围/场景是否有意义,如果它们可以一起使用?
答案 0 :(得分:14)
值是不可变的,不占用存储空间(没有地址),
但是声明为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
}
编译器可以在as-if规则下选择 not 分配任何存储来存储声明为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
分配存储空间;
可以想象这可能会影响性能的情况(见下文)。
变量是对象或引用 [basic] / 6 。 让我们关注对象。
像constexpr int a = 42;
这样的声明具有明显的简单声明;
它由 decl-specifier-seq init-declarator-list ;
从[dcl.dcl] / 9,我们可以得出结论(但并不严格)这样的声明声明了一个对象。
具体来说,我们可以(严格地)得出结论,它是对象声明,
但这包括引用的声明。
另见whether or not we can have variables of type void
的讨论。
对象声明中的constexpr
意味着对象的类型为const
[dcl.constexpr] / 9 。
对象是存储区域 [intro.object] / 1 。
我们可以从[intro.object] / 6和[intro.memory] / 1推断出每个对象都有一个地址。
请注意,我们可能无法直接使用此地址,例如如果通过prvalue引用对象。
(甚至有prvalues不是对象,例如文字42
。)
两个不同的完整对象必须具有不同的地址 [intro.object] / 6 。
从这一点开始,我们可以得出结论,声明为constexpr
的对象必须具有唯一的地址
任何其他(完整)对象。
此外,我们可以得出结论,声明constexpr int a = 42;
声明了一个具有唯一地址的对象。
恕我直言唯一有趣的问题是“按功能static
”,àla
void foo() {
static constexpr int i = 42;
}
据我所知 - 但this seems still not entirely clear - 编译器可能在运行时计算constexpr
变量的初始值设定项。
但这似乎是病态的;我们假设不那样做,
即它在编译时预先计算初始化程序。
在{em>静态初始化期间完成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
变量的静态存储持续时间中受益。
但是,我们可能会丢失此局部变量的局部性,如本答案的“存储”部分所示。
直到我看到一个基准测试表明这是一个真正的效果,
我会假设这不相关。
如果static
对constexpr
对象只有这两种效果,
我默认使用static
:
我们通常不需要保证constexpr
个对象的唯一地址。
对于可变constexpr
个对象(具有mutable
成员的类类型),
本地static
和非静态constexpr
对象之间存在明显不同的语义。
类似地,如果地址本身的值是相关的(例如,对于散列图查找)。
答案 1 :(得分:6)
仅限示例。社区维基。
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
的每次(递归)调用的地址都将不同。
例如,如果对象很大(例如数组)并且我们需要在需要常量表达式的上下文中使用它(需要编译时常量)并且我们需要获取其地址。 / p>
请注意,由于地址必须不同,对象可能会在运行时初始化 ;例如,如果递归深度取决于运行时参数。初始化程序仍然可以预先计算,但结果可能必须复制到每个递归步骤的新内存区域。
在这种情况下,constexpr
仅保证在编译时可以 进行评估,并且在编译时可以 执行初始化那种类型。
static
==每班template<int N>
struct foo
{
static constexpr int n = N;
};
与always一样:为foo
的每个模板特化(实例化)声明一个变量,例如foo<1>
,foo<42>
,foo<1729>
。如果要公开非类型模板参数,可以使用例如静态数据成员。它可以是constexpr
,以便其他人可以从编译时已知的值中受益。
static
==内部联系// namespace-scope
static constexpr int x = 42;
非常多余; constexpr
个变量默认具有内部链接。
在这种情况下,我认为目前没有任何理由使用static
。
答案 2 :(得分:1)
我使用static constexpr
作为未命名枚举的替代品,在我不知道确切类型定义的地方,但想要查询有关该类型的一些信息(通常在编译时)时间)。 强>
编译时未命名的枚举还有一些额外的好处。调试更容易(值在调试器中显示为&#34;普通&#34;变量。此外,您可以使用任何可以构造constexpr的类型(不仅仅是数字),而不仅仅是带有枚举的数字。< / p>
示例:
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那样的大小增加。