为什么函数指针可以是`constexpr`?

31
编译器如何在程序执行之前知道平方根在内存中的位置?我原以为每次程序执行时地址都会不同,但这段代码可以运行:
constexpr double(*fp)(double) = &sqrt;
cout << fp(5.0);

是因为地址相对于内存中的另一个地址吗?我认为不是,因为fp的值很大:0x720E1B94。


链接器完成这项工作,而不是编译器。链接器找出事物的位置。 - Ed Heal
@Coolwater 函数指针在调用之间不会改变,为什么要改变呢? - W.F.
@W.F. 当一个可执行文件被运行时,它的内容会被放置在内存中。为什么下一次运行程序时,完全相同的内存空间会再次可用呢? - Coolwater
@Coolwater 方法调用并非全部都被内联,可以参考调用栈的概念。 - W.F.
2
@SHH:除非每次运行程序都重新编译,否则这种差异并不重要。 - Benjamin Lindley
显示剩余10条评论
4个回答

31

在编译时,编译器不知道sqrt的地址。但是,在编译时,你不能使用constexpr函数指针进行任何操作来访问该指针的地址。因此,在编译时的函数指针可以被视为不透明值。

由于在初始化后无法更改constexpr变量,因此每个constexpr函数指针都可以简化为特定函数的位置。

如果你做了这样的事情:

using fptr = float(*)(float);

constexpr fptr get_func(int x)
{
  return x == 3 ? &sqrtf : &sinf;
}

constexpr fptr ptr = get_func(12);
编译器可以准确地检测出针对特定编译时值函数get_func将返回哪个函数。因此get_func(12)会简化为&sinf。所以,无论&sinf将编译成什么,get_func(12)也会编译成相同的内容。

1
这并没有解释编译器如何知道&sinf的值是什么。只有@hyde的回答解释了那个。 - fishinear
4
编译器并不需要知道实际的数值,它只需要知道这是一个在执行期间无法更改的常量值。这使得它可以直接引用所生成的可执行文件中的sqrtfsinf符号:它允许消除分支和通过指针调用的常量参数。 - Giel
@Giel 没错。编译器不需要知道它,但最终的代码肯定需要知道它,这就是OP的问题。答案当然是符号的值在链接期间确定和填充,这在这个答案中没有解释。 - fishinear

17

地址值是由链接器分配的,因此编译器不知道确切的地址值。

cout << fp(5.0); 

这种方法之所以有效,是因为它在精确地址被解析后在运行时进行评估。

通常情况下,您不能使用constexpr指针的实际值(地址),因为它在编译时未知。

Bjarne Stroustrup的C++程序设计语言第四版提到:

10.4.5 Address Constant Expressions

The address of a statically allocated object (§6.4.2), such as a global variable, is a constant. However, its value is assigned by the linker, rather than the compiler, so the compiler cannot know the value of such an address constant. That limits the range of constant expressions of pointer and reference type. For example:

   constexpr const char∗ p1 = "asdf";
   constexpr const char∗ p2 = p1;     // OK 
   constexpr const char∗ p2 = p1+2;   // error : the compiler does not know the value of p1 
   constexpr char c = p1[2];          // OK, c==’d’; the compiler knows the value pointed to by p1

4
据我所知,constexpr const char* p3 = p1 + 2; 是正确的。p1 + 2 是一个代表具有静态存储期子对象地址的值,是有效的常量表达式;在这种情况下,不需要完整对象。你不能将其用作非类型模板参数,但这是另一回事。我无法在标准中找到任何使这样的构造成为非法的规定;这适用于C++11及更高版本。我测试过的所有编译器都接受它。另请参见此答案 - bogdan

9
编译器如何在程序执行前知道平方根在内存中的位置?
工具链决定函数放置的位置。
地址是否相对于内存中的另一个地址?
如果生成的程序是可重定位或位置无关的,则是这种情况。如果程序都不是,那么地址甚至可以是绝对的。
为什么下一次运行程序时会有完全相同的内存空间可用?
因为内存空间是虚拟的。

3
空间是虚拟的,但如今通常也会使用地址空间布局随机化(ASLR)。 - Ruslan

7

很简单。

考虑编译器如何知道在这段代码中要调用的地址:

puts("hey!");

编译器不知道puts的位置,也不会为其添加运行时查找(尽管这实际上是类的虚方法所需要做的)。运行时动态库的不同版本可能存在(即使是相同的库文件,地址空间布局随机化也可能存在),因此构建工具链链接器也不知道它。
因此,由动态链接器在编译后的二进制程序启动时修复地址。这被称为重定位
您的constexpr也会发生完全相同的事情:编译器添加了使用该地址的代码中的每个位置到重定位表,然后每次程序启动时动态链接器都会完成其工作。

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