命名空间 std 中不可寻址函数的基本原理是什么?

What is the rationale for non-addressable functions in namespace std?

提问人:user3188445 提问时间:7/2/2022 最后编辑:Remy Lebeauuser3188445 更新时间:9/1/2023 访问量:269

问:

[namespace.std] 不允许获取命名空间中大多数函数的地址或引用。这是一个很大的陷阱,因为它似乎经常将标准库函数作为参数传递,即使这可能会停止工作,或者更糟的是,在不同的编译器上。std

据推测,这样做是为了让实现专门优化标准库。此限制使 C++ 更难使用。

您能否给出明确的示例来说明 C++ 实现如何从命名空间的这种限制中受益?std

如果这些优化如此重要,以至于需要使C++更难使用,为什么一些非系统库不需要同样的东西呢?

C++ 语言设计

评论

1赞 Brian61354270 7/2/2022
相关:是否可以在未计算的上下文中形成指向 STD 的不可寻址函数的指针?那里的答案触及了规则的一个原因。在自 C++20 以来保持或传递非可寻址函数中也提到过
9赞 HolyBlackCat 7/2/2022
这很可能与优化无关,而是与允许库静默重载或模板化某些函数有关。
5赞 Eljay 7/2/2022
传统上,一些不可寻址的函数是作为就地发出的汇编指令来实现的。一种“硬编码”内联——一种内在的。其他编译器可能会将它们实现为“适当的”函数,但是依赖它将是不可移植的。(我记得有一个编译器将其中一些实现为 C 预处理器。作为合法的解决方法,您可以将它们包装在 lambda 中,以使它们可寻址(通过 lambda)。
4赞 463035818_is_not_an_ai 7/2/2022
我曾经读过一些类似 Eljay 写的东西。用我记忆中的话来说:该标准仅指定当您调用具有某些参数的函数时会发生什么,例如 ,但这并不一定意味着存在这样的函数,它可能完全是或其他东西foo(1,1)void (int,int)void(int,int,int=0)
1赞 vnagy 7/2/2022
“更难使用”是一个非常笼统的术语,也非常主观。我理解你的好奇心,这个问题是完全合理的。但是,您认为“此限制使 C++ 更难使用”的推理是个人意见,并且基于该观点得出的问题“保证使 C++ 更难使用”没有真正的客观答案。因此,没有人真正解决你问题的那部分。

答:

10赞 Jan Schultke 9/1/2023 #1

首先,值得注意的是,这种设计并非起源于 C;这在 C++ 中是全新的。

出于相同的语法原因,允许采用库函数的地址,即使它也被定义为宏。89

89 这意味着实现必须为每个库函数提供一个实际函数,即使它也为该函数提供了一个宏。

- C89 标准,4.1.6 库函数的使用

但是,由于多种原因,这些严格的保证在 C++ 中将非常严格。 作为免责声明,我无法找到 Bjarne 本人的引述,所以我要说的一切都是社区共识和个人经验的集合。

1. 添加重载可能会破坏源兼容性

假设你有一个函数:

bool is_even(int x) { return x % 2 == 0; }

最初调用 可能是安全的,但如果除了 之外还添加了 和 的重载,那么 的用法将变得格式不正确。std::partition(begin, end, is_even)longlong longintis_even

从本质上讲,任何可寻址函数将来都不能接收额外的重载,因为它会破坏现有代码。这就是为什么 [namespace.std] 特别说“可能格式不正确”的原因。

2. 签名比 C 语言更容易更改

破坏兼容性的另一种方法是使现有函数更加通用。 例如,它允许标准库使其函数更加通用,例如转动:

// possible historical implementation in <math.h> until C++11
#define isnan(x) implementation-defined

bool isnan(long double); //possibly with overloads for float and double

并随后进入

bool isnan(std::floating_point auto);

借助函数重载和模板等功能,函数的实现可能会随着时间的推移而发生巨大变化。

当然,没有人能预见到数学库中的这些剧烈变化,但对不可寻址函数的限制使它们成为可能,而不会破坏任何符合要求的代码。

3. 函数可能没有地址

函数可能没有任何地址的原因有两个:

  • 它是一种内在功能
    • 这意味着函数调用只是告诉编译器生成一些 IR 指令,而实际函数甚至可能不存在
  • 它是一个即时函数(即 C++20)consteval

前一个原因可能是做出这一决定的重要因素。 如今,标准库中使用的任何内部函数通常都有一个函数包装器,但这在当时并不明显。inline

一个更现代的内在标准库函数的例子是,它在 MSVC STL 中被“有点内在”。 请参阅在 C++ 中提高调试性能的状态std::move

4. “函数”可以是函数对象

在 C++ 中,也可以将函数实现为函数对象,例如:

// inline has only been added in C++17, but compilers could have supported its
// functionality before that
inline const struct {
    float operator()(float x) const;
} sqrt;

如果一个函数实际上是一个函数对象,那么在获取其地址时,行为会有所不同,因为你不会得到一个函数指针。但是,调用它的行为相同(除了 ADL 不会发生)。

这是通过使函数不可寻址而实现的另一种形式的灵活性。

结论

使采用标准库函数的地址成为可能将大大降低实现者的灵活性。 几乎任何更改(例如添加重载)都会破坏兼容性,从而有效地冻结语言进度。

这在 C 中不是一个大问题,因为函数签名无论如何都会被冻结,但在 C++ 中会产生重大的负面影响。

评论

0赞 user3188445 9/19/2023
我想知道 #4 是否会变得更加普遍,因为 C++23 允许静态运算符()。