【问题标题】:Does static constexpr variable inside a function make sense?函数内的静态 constexpr 变量是否有意义?
【发布时间】:2012-12-01 16:43:22
【问题描述】:

如果我在函数中有一个变量(例如,一个大数组),那么同时声明 staticconstexpr 是否有意义? constexpr 保证数组是在编译时创建的,那么static 会没用吗?

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

static 是否真的在生成代码或语义方面做了任何事情?

【问题讨论】:

    标签: c++ static c++11 constexpr


    【解决方案1】:

    简短的回答是,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-used 或声明的 static,否则编译器可以完全不包含它。这非常有用,因为它允许使用编译时临时 constexpr 数组,而不会用不必要的字节污染已编译的程序。在这种情况下,您显然不想使用static,因为static 可能会强制对象在运行时存在。

    【讨论】:

    • @AndrewLazarus,你不能从 const 对象中丢弃 const,只能从指向 Xconst X* 中丢弃。但这不是重点。关键是自动对象不能有静态地址。正如我所说,一旦编译完成,constexpr 就不再有意义,因此没有什么可以丢弃(很可能根本没有,因为该对象甚至不能保证在运行时存在。)
    • 我觉得这个答案不仅令人难以置信,而且自相矛盾。例如,您说您几乎总是想要staticconstexpr,但解释说它们是正交且独立的,做不同的事情。然后,您提到了不将两者结合起来的原因,因为它会忽略 ODR 使用(这似乎很有用)。哦,我仍然不明白为什么 static 应该与 constexpr 一起使用,因为 static 是用于运行时的东西。你从来没有解释过为什么使用 constexpr 的 static 很重要。
    • @void.pointer:你对最后一段是正确的。我改了介绍。我以为我已经解释了static constexpr 的重要性(它可以防止在每次函数调用时都必须重新创建常量数组),但我调整了一些可能使它更清晰的词。谢谢。
    • 提及编译时常量与运行时常量可能也很有用。换句话说,如果 constexpr 常量变量仅在编译时上下文中使用,并且在运行时从不需要,那么 static 没有任何意义,因为当您到达运行时时,该值已被有效地“内联” ”。但是,如果在运行时上下文中使用constexpr(换句话说,constexpr 需要隐式转换为const,并且可以使用运行时代码的物理地址),它将需要static 来确保 ODR合规性等。至少这是我的理解。
    • 我上一条评论的示例:static constexpr int foo = 100;。编译器没有理由不能在任何地方用foo 代替文字100,除非代码正在执行类似&foo 的操作。所以static on foo 在这种情况下没有用处,因为foo 在运行时不存在。再次由编译器决定。
    【解决方案2】:

    除了给出的答案,值得注意的是编译器不需要在编译时初始化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 关键字确保只执行一次初始化。

    【讨论】:

    • 如果 constexpr_short 再次初始化,我们可以不使用constexpr const short constexpr_short 给出错误吗
    • 您的constexpr const 语法没有意义,因为constexpr 已经是const,添加const 一次或多次会被编译器忽略。您正在尝试捕获错误,但这不是错误,这是大多数编译器的工作方式。
    • @metablaster 对此不确定,例如我的编译器 (GCC 10.2) 由于缺少 const 而警告 constexpr char *sectionLabel = "Name",打印出“警告:ISO C++ 禁止将字符串常量转换为'char*' [-Wwrite-strings]"。或者这是一个错误的警告?
    • @ThorbjørnLindeijer 你的编译器是正确的,但这并没有说明我的观点是错误的,因为这只适用于char,它是C++ 中的一种特殊野兽。看这个链接为什么:stackoverflow.com/questions/30561104/…
    • 最佳答案,恕我直言。谢谢
    【解决方案3】:

    不创建大型数组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]();
    }
    

    我们唯一的改变是我们做了tablestatic,但是影响是巨大的:

    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 实际上很好地优化了这样的查找表,但其他编译器却没有。 See Compiler Explorer for a live example.

    【讨论】:

      猜你喜欢
      • 2020-10-08
      • 2013-08-17
      • 2019-05-25
      • 1970-01-01
      • 2016-04-27
      • 1970-01-01
      • 2016-07-30
      • 2017-12-24
      • 2022-11-22
      相关资源
      最近更新 更多