函数内的静态constexpr变量有意义吗?

如果我在一个函数(比如一个大数组)中有一个变量,将它同时声明为staticconstexpr是否有意义?constexpr保证数组是在编译时创建的,所以static没有用吗?

void f() {
static constexpr int x [] = {
// a few thousand elements
};
// do something with the array
}

在生成的代码或语义方面,static实际上做了任何事情吗?

145023 次浏览

简单的回答是,static不仅有用,而且总是被需要。

首先,注意staticconstexpr是完全独立的。static定义了对象在执行期间的生存期;constexpr指定对象应该在编译期间可用。编译和执行在时间和空间上都是不连贯的。因此,一旦程序被编译,constexpr就不再相关了。

每个声明的constexpr变量都隐式地是const,但是conststatic几乎是正交的(除了与static const整数的交互作用)。

C++对象模型(§1.9)要求除位字段之外的所有对象占用至少一个字节的内存并具有地址;此外,在给定时刻,程序中可观察到的所有此类对象必须具有不同的地址(第6段)。这并不完全要求编译器为每次调用具有局部非静态const数组的函数在堆栈上创建一个新数组,因为编译器可以在as-if原则中寻求保护,只要它能证明不能观察到其他此类对象。

不幸的是,这并不容易证明,除非函数很简单(例如,它不调用任何其他在转换单元中不可见的函数体),因为数组从定义上讲或多或少都是地址。因此,在大多数情况下,非静态const(expr)数组必须在每次调用时在堆栈上重新创建,这就无法在编译时计算它。

另一方面,局部static const对象由所有观察者共享,而且即使定义它的函数从未被调用,它也可以被初始化。所以以上都不适用,编译器不仅可以自由生成它的单个实例;可以在只读存储中生成它的单个实例。

所以在你的例子中你一定要使用static constexpr

然而,有一种情况下你不希望使用static constexpr。除非constexpr声明的对象是ODR-usedstatic,否则编译器完全可以不包含它。这非常有用,因为它允许使用编译时临时constexpr数组,而不会用不必要的字节污染已编译的程序。在这种情况下,显然不希望使用static,因为static可能会强制对象在运行时存在。

除了给出的答案,值得注意的是,编译器不需要在编译时初始化constexpr变量,因为要知道constexprstatic constexpr之间的区别是,要使用static constexpr,你必须确保变量只初始化一次。

下面的代码演示了constexpr变量如何被初始化多次(尽管值相同),而static constexpr肯定只初始化一次。

此外,代码还比较了constexprconst结合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 -O3bar()编译为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实际上很好地优化了此类查找表,但其他编译器做不到。有关现场示例,请参阅编译器资源管理器