static
如果我在函数中有一个变量(例如,一个大数组),那么同时声明它是否有意义constexpr
?constexpr
保证数组是在编译时创建的,所以static
没用吗?
void f() {
static constexpr int x [] = {
// a few thousand elements
};
// do something with the array
}
static
实际上在生成的代码或语义方面做了什么吗?
简短的回答是,它不仅static
有用,而且总是很受欢迎。
首先,请注意static
和constexpr
是完全相互独立的。static
定义对象在执行期间的生命周期;constexpr
指定对象在编译期间应该可用。编译和执行在时间和空间上都是不相交和不连续的。所以一旦程序编译完成,constexpr
就不再相关了。
每个声明的变量constexpr
都是隐式的,const
但几乎是正交的(除了与整数的交互。)const
static
static const
对象模型(第C++
1.9 节)要求除位域之外的所有对象都至少占用一个字节的内存并具有地址;此外,在给定时刻在程序中可观察到的所有此类对象都必须具有不同的地址(第 6 段)。这并不完全要求编译器在每次调用具有本地非静态 const 数组的函数时在堆栈上创建一个新数组,因为编译器可以在as-if
原则上寻求庇护,只要它可以证明没有其他这样的对象可以观察到的。
不幸的是,这并不容易证明,除非该函数是微不足道的(例如,它不调用其主体在翻译单元中不可见的任何其他函数),因为根据定义,数组或多或少是地址。因此,在大多数情况下,每次调用时都必须在堆栈上重新创建非静态const(expr)
数组,这违背了能够在编译时计算它的意义。
另一方面,本地static const
对象由所有观察者共享,而且即使定义它的函数从未被调用,它也可能被初始化。所以以上都不适用,编译器不仅可以免费生成它的单个实例;在只读存储中生成它的单个实例是免费的。
所以你绝对应该static constexpr
在你的例子中使用。
但是,在一种情况下您不想使用static constexpr
. 除非constexpr
声明的对象是ODR-used或 declared static
,否则编译器可以完全不包含它。这非常有用,因为它允许使用编译时临时constexpr
数组,而不会用不必要的字节污染已编译的程序。在这种情况下,您显然不想使用static
,因为static
可能会强制对象在运行时存在。
除了给出的答案,值得注意的是编译器不需要在编译时初始化变量,知道和constexpr
之间的区别在于使用你确保变量只初始化一次。constexpr
static constexpr
static constexpr
以下代码演示了如何constexpr
多次初始化变量(尽管具有相同的值),而static constexpr
肯定只初始化一次。
此外,代码比较了constexpr
反对const
与结合的优势static
。
#include <iostream>
#include <string>
#include <cassert>
#include <sstream>
const short const_short = 0;
constexpr short constexpr_short = 0;
// print only last 3 address value numbers
const short addr_offset = 3;
// This function will print name, value and address for given parameter
void print_properties(std::string ref_name, const short* param, short offset)
{
// determine initial size of strings
std::string title = "value \\ address of ";
const size_t ref_size = ref_name.size();
const size_t title_size = title.size();
assert(title_size > ref_size);
// create title (resize)
title.append(ref_name);
title.append(" is ");
title.append(title_size - ref_size, ' ');
// extract last 'offset' values from address
std::stringstream addr;
addr << param;
const std::string addr_str = addr.str();
const size_t addr_size = addr_str.size();
assert(addr_size - offset > 0);
// print title / ref value / address at offset
std::cout << title << *param << " " << addr_str.substr(addr_size - offset) << std::endl;
}
// here we test initialization of const variable (runtime)
void const_value(const short counter)
{
static short temp = const_short;
const short const_var = ++temp;
print_properties("const", &const_var, addr_offset);
if (counter)
const_value(counter - 1);
}
// here we test initialization of static variable (runtime)
void static_value(const short counter)
{
static short temp = const_short;
static short static_var = ++temp;
print_properties("static", &static_var, addr_offset);
if (counter)
static_value(counter - 1);
}
// here we test initialization of static const variable (runtime)
void static_const_value(const short counter)
{
static short temp = const_short;
static const short static_var = ++temp;
print_properties("static const", &static_var, addr_offset);
if (counter)
static_const_value(counter - 1);
}
// here we test initialization of constexpr variable (compile time)
void constexpr_value(const short counter)
{
constexpr short constexpr_var = constexpr_short;
print_properties("constexpr", &constexpr_var, addr_offset);
if (counter)
constexpr_value(counter - 1);
}
// here we test initialization of static constexpr variable (compile time)
void static_constexpr_value(const short counter)
{
static constexpr short static_constexpr_var = constexpr_short;
print_properties("static constexpr", &static_constexpr_var, addr_offset);
if (counter)
static_constexpr_value(counter - 1);
}
// final test call this method from main()
void test_static_const()
{
constexpr short counter = 2;
const_value(counter);
std::cout << std::endl;
static_value(counter);
std::cout << std::endl;
static_const_value(counter);
std::cout << std::endl;
constexpr_value(counter);
std::cout << std::endl;
static_constexpr_value(counter);
std::cout << std::endl;
}
可能的程序输出:
value \ address of const is 1 564
value \ address of const is 2 3D4
value \ address of const is 3 244
value \ address of static is 1 C58
value \ address of static is 1 C58
value \ address of static is 1 C58
value \ address of static const is 1 C64
value \ address of static const is 1 C64
value \ address of static const is 1 C64
value \ address of constexpr is 0 564
value \ address of constexpr is 0 3D4
value \ address of constexpr is 0 244
value \ address of static constexpr is 0 EA0
value \ address of static constexpr is 0 EA0
value \ address of static constexpr is 0 EA0
如您所见,您自己constexpr
被多次初始化(地址不一样),而static
关键字确保只执行一次初始化。
不制作大型数组static
,即使它们constexpr
会对性能产生巨大影响,并可能导致许多错过的优化。它可能会使您的代码减慢几个数量级。您的变量仍然是本地的,编译器可能决定在运行时初始化它们,而不是将它们作为数据存储在可执行文件中。
考虑以下示例:
template <int N>
void foo();
void bar(int n)
{
// array of four function pointers to void(void)
constexpr void(*table[])(void) {
&foo<0>,
&foo<1>,
&foo<2>,
&foo<3>
};
// look up function pointer and call it
table[n]();
}
您可能希望gcc-10 -O3
将 a 编译bar()
为jmp
它从表中获取的地址,但事实并非如此:
bar(int):
mov eax, OFFSET FLAT:_Z3fooILi0EEvv
movsx rdi, edi
movq xmm0, rax
mov eax, OFFSET FLAT:_Z3fooILi2EEvv
movhps xmm0, QWORD PTR .LC0[rip]
movaps XMMWORD PTR [rsp-40], xmm0
movq xmm0, rax
movhps xmm0, QWORD PTR .LC1[rip]
movaps XMMWORD PTR [rsp-24], xmm0
jmp [QWORD PTR [rsp-40+rdi*8]]
.LC0:
.quad void foo<1>()
.LC1:
.quad void foo<3>()
这是因为 GCC 决定不存储table
在可执行文件的数据部分,而是在每次函数运行时使用其内容初始化一个局部变量。事实上,如果我们去掉constexpr
这里,编译后的二进制文件是 100% 相同的。
这很容易比以下代码慢 10 倍:
template <int N>
void foo();
void bar(int n)
{
static constexpr void(*table[])(void) {
&foo<0>,
&foo<1>,
&foo<2>,
&foo<3>
};
table[n]();
}
我们唯一的改变是我们已经做出了改变table
static
,但影响是巨大的:
bar(int):
movsx rdi, edi
jmp [QWORD PTR bar(int)::table[0+rdi*8]]
bar(int)::table:
.quad void foo<0>()
.quad void foo<1>()
.quad void foo<2>()
.quad void foo<3>()
总之,永远不要让您的查找表成为局部变量,即使它们是constexpr
. Clang 实际上很好地优化了此类查找表,但其他编译器却没有。请参阅编译器资源管理器以获取实时示例。