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

284

如果我有一个函数内的变量(比如一个大数组),是否有意义同时声明它为staticconstexprconstexpr保证数组在编译时创建,那么static就无用了吗?

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

static在生成的代码或语义方面是否起作用?

3个回答

352
简而言之,static不仅有用,而且几乎总是想要的。需要注意的是,staticconstexpr完全独立。 static 定义对象在执行期间的生命周期; constexpr指定在编译期间该对象应该可用。编译和执行在时间和空间上是分离和离散的。因此,一旦程序被编译,constexpr将不再相关。每个声明为 constexpr的变量都隐式地是const, 但 const static几乎是正交的(除了与static const整数交互的情况)。本地static const对象由所有观察者共享,并且即使未调用定义它的函数也可能被初始化。 因此,编译器可以自由地生成它的一个实例并存储在只读存储器中。

因此,在您的示例中,您应该绝对使用static constexpr

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


2
@AndrewLazarus,您无法从const对象中去除const,只能从指向Xconst X*中去除。但这不是重点;关键是自动对象不能具有静态地址。正如我所说,一旦编译完成,constexpr就不再有意义了,因此没有什么可以去除的(可能根本没有任何东西,因为该对象甚至不能保证在运行时存在)。 - rici
43
我感觉这个回答既令人困惑,又自相矛盾。例如,你说你几乎总是想要“static”和“constexpr”,但解释它们是正交和独立的,执行不同的功能。然后你提到一个不要将两者合并的原因,因为这会忽略ODR使用(这似乎很有用)。哦,我仍然不明白为什么应该在constexpr中使用静态,因为静态是用于运行时的东西。你从未解释过为什么在constexpr中使用静态很重要。 - void.pointer
3
@void.pointer: 你对最后一段的看法是正确的。我改了引言。我认为我已经解释了 static constexpr 的重要性(它可以防止常量数组在每次函数调用时都需要被重新创建),但是我修改了一些措辞,以便更加清晰明了。谢谢。 - rici
17
提到编译时常量和运行时常量可能也会有所帮助。换句话说,如果一个constexpr常量变量仅在编译时上下文中使用并且在运行时不需要,那么使用static就没有意义,因为到达运行时时,该值已经被有效地“内联”。然而,如果constexpr在运行时上下文中使用(换句话说,constexpr需要隐式转换为const,并且在运行时代码中可用于物理地址),则需要static以确保ODR兼容性等。这至少是我的理解。 - void.pointer
9
我上一条评论的例子是:static constexpr int foo = 100;。除非代码像&foo这样做了某些特殊处理,否则编译器可以在使用foo处替换为字面值100。因此,在这种情况下,foo上的static没有什么用处,因为foo在运行时并不存在。这完全取决于编译器。 - void.pointer
显示剩余13条评论

29

除了给出的答案之外,值得注意的是编译器不一定需要在编译时初始化 constexpr 变量,知道 constexprstatic constexpr 的区别在于使用 static constexpr 你可以确保变量只被初始化一次。

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

此外,该代码比较了 constexprconststatic 结合使用的优势。

#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 关键字确保仅执行一次初始化。


我们能否不使用 constexpr const short constexpr_short 来防止再次初始化 constexpr_short 时出错? - akhileshzmishra
你的 constexpr const 语法没有意义,因为 constexpr 已经是 const,添加一次或多次 const 都会被编译器忽略。你试图捕捉一个错误,但这不是一个错误,这是大多数编译器的工作方式。 - metablaster
@metablaster 我不确定这一点,例如我的编译器(GCC 10.2)由于缺少const而警告constexpr char *sectionLabel = "Name",打印出“warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]”。或者这是一个错误的警告? - Thorbjørn Lindeijer
1
@ThorbjørnLindeijer,您的编译器是正确的,但这并不意味着我的观点是错误的,因为这仅适用于C++中的特殊字符char。请查看此链接以了解原因: https://dev59.com/ZV0a5IYBdhLWcg3wPGiQ - metablaster
最佳答案,依我之见。谢谢。 - Ícaro Pires
5
并不是 char 是一种“特殊的野兽”。constexpr char* 只是将 const 应用于指针本身,而不是指针类型。这是 char * constconst char* 的区别。这种语法上的差异适用于任何指针类型。 - Matthew M.

26

即使在数组被constexpr修饰时,不将大数组定义为static也可能会对性能产生巨大影响,并导致许多优化被错过。这可能会使您的代码速度慢上几个数量级。您的变量仍然是局部的,编译器可能会决定在运行时初始化它们,而不是将它们存储为可执行文件中的数据。

考虑以下示例:

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,编译后的二进制文件完全相同。

这段代码很容易比下面的代码慢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实际上可以很好地优化这样的查找表,但其他编译器则不行。请参见Compiler Explorer的实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接