【问题标题】:why is there a "never use non-literal type" rule in constexpr functions?为什么在 constexpr 函数中有“从不使用非文字类型”规则?
【发布时间】:2020-07-07 19:44:55
【问题描述】:

取以下法码:

bool bar();

template <class T>
constexpr bool foo(T t) {
  if (t>0) {
    return true;
  }
  return bar();
}


int main() {
  //constexpr bool cb1 = foo(-1); // error as expected  because it would attempt to call bar()
  constexpr bool cb2 = foo(1); // ok
}

https://godbolt.org/z/UWt_3A

因此,只要我们没有在编译时评估上下文中遇到非 constexpr 代码路径,我们的 constexpr 就已经形成良好的格式。整洁!

但是,如果我应用相同的实际概念,但碰巧在条件代码路径中包含非文字类型,例如 std::string,那么标准会说不:

#include <string>

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
  if (t>0) {
    return true;
  }
  std::string s = "abc";
  return bar(s);
}


int main() {
  //constexpr bool cb1 = foo(-1); // error as expected
  constexpr bool cb2 = foo(1); // this is also an error now :(
}

https://godbolt.org/z/iHThCq

这背后的原理是什么?为什么使用 std::string 是非法的,即使它实际上从未被构造(或销毁)?

额外问题:那么为什么以下合法:https://godbolt.org/z/L3np-u(上面略有变化,没有定义 std::string)?!

【问题讨论】:

  • 是的,规则确实存在,even in the latest standard draft。对我来说,这似乎也有点苛刻。怀着极大的好奇心追随这件事。
  • fwiw,我不认为这是一个基本限制(也许编译器难以检查非文字变量是否需要实例化)。似乎可以删除此约束。
  • 一点也不苛刻,它很容易修复,将你的非文字变量包装在 lambda 中并调用它。
  • 如果您将 if 检查本身设为 constexpr,一种方法可以使其在 GCC 下编译,但这并不能回答问题。
  • 我猜它有一些“历史”原因。您可能会问,为什么也不允许使用“goto”。这在编译时是可能的。但这会导致额外的实现开销...在 c++11 中只允许一个返回...

标签: c++ constexpr


【解决方案1】:

我只是在这里猜测,但这可能是因为 std::string s = "abc" 是一个自动变量并在函数开始时分配到堆栈中(即使尚未构造)违反了 constexpr 规则?

如果我将代码更改为:

using namespace std::string_literals;

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    else {
        //std::string ss = "abc"s;
        return bar("abc"s);
    }
    return false;
}

因为它编译时不需要分配任何东西。

我在这里解释我的推理(以及对 cme​​ts 的回应),因为我需要比评论更多的空间。

正如@StoryTeller-UnslanderMonica 所说,“猜测是回答问题的糟糕基础”。

绝对是的。这就是为什么我开始这么说的原因:我猜。这是有原因的。

我不喜欢正常猜测,但我觉得这很有趣,想想想是否有人说我错了(我已经准备好接受了。)

但言归正传,文字类型变量通常存储在一些只读内存数据段中(除非它们是数字,否则可以直接转换为 ASM MOV/... 指令),而不是堆栈。

如果声明为自动(存储在堆栈中):

存储时间

程序中的所有对象都具有以下存储期限之一:

自动存储期限。对象的存储在封闭代码块的开头分配,并在结尾处释放。除了声明为 static、extern 或 thread_local 的对象外,所有本地对象都有此存储持续时间。

(强调我的。)

因此,即使在if 之后声明,存储空间也已分配并且在任何情况下都应该被释放(在 OP 显示的示例中。)

事实上,如果这样做:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    const std::string ss = "abc"s;
    return bar(ss);
}

错误是:

main.cc:15:16: error: call to non-‘constexpr’ function ‘std::__cxx11::basic_string<char> std::literals::string_literals::operator""s(const char*, std::size_t)’

为什么? 我猜因为自动,“对象的存储是在封闭代码块的开头分配的”(函数的开头),无论执行代码路径如何.

而且,如果你声明它constexpr,它会引入析构函数:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    constexpr std::string ss = "abc"s;
    return bar(ss);
}

错误:

main.cc:19:32: error: temporary of non-literal type ‘const string’ {aka ‘const std::__cxx11::basic_string<char>’} in a constant expression
     constexpr std::string ss = "abc"s;
                                ^~~~~~
In file included from /usr/include/c++/8/string:52,
                 from main.cc:2:
/usr/include/c++/8/bits/basic_string.h:77:11: note: ‘std::__cxx11::basic_string<char>’ is not literal because:
     class basic_string
           ^~~~~~~~~~~~
/usr/include/c++/8/bits/basic_string.h:77:11: note:   ‘std::__cxx11::basic_string<char>’ has a non-trivial destructor
main.cc: In instantiation of ‘constexpr bool foo(T) [with T = int]’:
main.cc:25:29:   required from here
main.cc:19:27: error: the type ‘const string’ {aka ‘const std::__cxx11::basic_string<char>’} of ‘constexpr’ variable ‘ss’ is not literal
     constexpr std::string ss = "abc"s;

我认为关键是:‘std::__cxx11::basic_string&lt;char&gt;’ has a non-trivial destructor

因此在执行代码路径之前考虑了对析构函数的理论调用。

为什么?

因为“对象的存储空间是在封闭代码块的开头分配的”。

以下内容:

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    }
    return bar("abc"s);
}

创建一个临时的:

main.cc:19:15: error: call to non-‘constexpr’ function ‘bool bar(std::__cxx11::string)’
     return bar("abc"s);

但是

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    } else {
        return bar("abc"s);
    }
    return false;
}

仅在执行路径转到else 时创建临时(不是这种情况。)

如前所述,这是一个猜测,但我认为是一个基于的猜测,而不仅仅是盲目尝试。

再次,我确信这取决于编译器的实现。我绝不是 C++ 标准专家,但我无法在任何文档中找到这个明确的案例。

我已经在gdb中运行了程序,看是否进入foo函数:

bool bar(std::string);

template <class T>
constexpr bool foo(T t) {
    if (t>0) {
        return true;
    } else {
        //std::string ss = "abc"s;
        return bar("abc"s);
    }
    return false;
}

int main() {
    //constexpr bool cb1 = foo(-1); // error as expected
    constexpr bool cb2 = foo(1); // this is also an error now :(

    cout << "Bool: " << cb2 << endl;
    
    return 0;
}

它在没有定义bar 的情况下链接...

manuel@desktop:~/projects$ g++ -Wall -Wextra -g main.cc -o main --std=gnu++2a -Wpedantic && time ./main
Bool: 1

real    0m0,002s
user    0m0,000s
sys 0m0,002s
manuel@desktop:~/projects$ gdb ./main
GNU gdb (Debian 8.2.1-2+b3) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./main...done.
(gdb) b main
Breakpoint 1 at 0x117d: file main.cc, line 27.
(gdb) r
Starting program: /home/manuel/projects/main 

Breakpoint 1, main () at main.cc:27
27      constexpr bool cb2 = foo(1); // this is also an error now :(
(gdb) s
29      cout << "Bool: " << cb2 << endl;
(gdb) s
Bool: 1
31      return 0;
(gdb) s
32  }
(gdb) q
A debugging session is active.

    Inferior 1 [process 18799] will be killed.

Quit anyway? (y or n) y

【讨论】:

  • 猜测是回答问题的糟糕基础。根据您的推理,文字类型变量也将位于“堆栈”上。但是这样的变量是允许的。那么“堆栈”怎么可能是解释呢?
  • 为什么要删除 else 块?当字符串变量位于该块中时,也会发生相同的错误。我想您必须这样做才能使您的解释看起来合理。存储时间与它无关。
  • 虽然是一个合理的论点,但我没有接受这一点,因为证明并不可靠:else { return bar("abc"s); } 在没有 else 的情况下编译得很好 - 即不需要嵌套范围。请参阅我原始问题末尾的“为什么以下合法”示例。
  • @Nick 如果 t>0 则不分配字符串,因此没有构造函数、析构函数甚至函数调用,因此没有内存预留。如果您将其更改为:std::string a ("abc"); return bar(a); 将会分配(即使没有函数调用)并且编译将失败。
  • 无论有没有else,它的行为方式完全相同,有或没有任何组合的局部命名变量。你在“为什么?”之后的例子在函数范围内使用bar("abc") not 无法编译。我不知道你从哪个编译器得到这个。再次查看我的最后一个神螺栓链接。
【解决方案2】:

因为您的第二个代码违反了以下规则:

constexpr 函数的定义应满足以下要求:

  • 其函数体应为 = delete、= default 或 不包含的复合语句
  • 非文字类型变量的定义或静态或线程存储持续时间或未执行初始化的变量。

由于在您的第二个代码中,您定义了一个类似std::string s = "abc"; 的变量,其中std::string 类型是非文字类型,因此违反了我引用的规则。所以,你的第二个代码格式不正确。

更新:

为什么规则必须存在?因为以下规则:

对象声明中使用的 constexpr 说明符将对象声明为 const。这样的对象应具有文字类型并应被初始化。在任何 constexpr 变量声明中,初始化的完整表达式应为常量表达式

由于这条规则,初始化的完整表达式包括析构函数的调用:

一个完整的表达式是

  • 临时对象以外的对象的生命周期结束时生成的析构函数调用,或

因为std::string s = "abc";是一个具有自动持续时间存储的变量,它不是一个临时对象,因此它的析构函数的调用包含在初始化的完整表达式中。因为非文字类型的析构函数不会是 constexpr 函数,所以这就是为什么这个规则是必要的。

【讨论】:

  • 这是问题的前提。问题是为什么该规则存在:)
  • OP 知道。正如他们所说的“那么标准说不”。问题是 为什么 是那里的规则,即使没有对 std::string 进行评估。
  • @Quentin 我不明白你的意思?只要你定义了一个非文字类型的变量,它就会违反规则,因此无论表达式是否被评估,你的代码都会导致格式错误。
  • @StoryTeller-UnslanderMonica 看到这个outcome,clang 会好的。为什么?因为clang遵守标准规定的规则。我想。
  • @Manuel - 不,它没有。对象的生命周期在其初始化完成时开始。这是规范定义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
  • 2013-12-24
  • 2021-09-01
  • 2018-07-10
相关资源
最近更新 更多