考虑以下代码:
#include <cctype>
#include <functional>
#include <iostream>
int main()
{
std::invoke(std::boolalpha, std::cout); // #1
using ctype_func = int(*)(int);
char c = std::invoke(static_cast<ctype_func>(std::tolower), 'A'); // #2
std::cout << c << "\n";
}
在此,对std::invoke
的两个调用已标记为将来参考。
预期的输出是:
a
是否保证了预期的输出?
(注意:有两个称为tolower
的函数—一个在<cctype>
中,另一个在<locale>
中。引入了显式强制转换以选择所需的重载。)
答案 0 :(得分:28)
否。
让
F
表示标准库函数([global.functions]),标准库静态成员函数或标准库函数模板的实例。 除非F
被指定为可寻址函数,否则如果C ++程序明确或隐含地尝试执行以下操作,则行为未指定(可能格式错误)形成指向F
的指针。 [注意:形成此类指针的可能方法包括应用一元&
运算符([expr.unary.op]),addressof
([specialized.addressof])或函数到指针的标准转换([conv.func])。 — 尾注] 此外,如果C ++程序试图形成对F
的引用,或者试图形成指向成员的指针,则C ++程序的行为是不确定的(可能格式不正确)标准库非静态成员函数([member.functions])或标准库成员函数模板的实例。
考虑到这一点,让我们检查对std::invoke
的两次调用。
std::invoke(std::boolalpha, std::cout);
在这里,我们正在尝试形成指向std::boolalpha
的指针。幸运的是,[fmtflags.manip]节省了一天:
本节中指定的每个功能都是指定的可寻址功能([namespace.std])。
boolalpha
是本节中指定的功能。
因此,此行格式正确,等效于:
std::cout.setf(std::ios_base::boolalpha);
那为什么呢?好吧,下面的代码是必要的:
std::cout << std::boolalpha;
std::cout << std::invoke(static_cast<ctype_func>(std::tolower), 'A') << "\n";
不幸的是,[cctype.syn]说:
标头
<cctype>
的内容和含义与C标准库标头<ctype.h>
相同。
tolower
在任何地方都没有明确指定可寻址功能。
因此,此C ++程序的行为未指定(可能格式错误),因为它试图形成指向tolower
的指针,而该指针未指定为可寻址函数。
不能保证预期的输出。 实际上,甚至不保证代码可以编译。
这也适用于成员函数。 [namespace.std]没有明确提及这一点,但是从[member.functions]可以看出,如果C ++程序尝试获取已声明的成员函数的地址,则该行为是不确定的(可能是格式错误的)在C ++标准库中。根据{{3}}:
对于C ++标准库中描述的非虚拟成员函数,实现可以声明一组不同的成员函数签名,前提是对该成员函数的任何调用都会从本文档中描述的声明集中选择一个重载。文档的行为就像选择了该重载一样。 [注意:例如,一个实现可以添加具有默认值的参数,或者用具有相同行为的两个或多个成员函数将成员函数替换为具有默认参数的成员函数,或者为成员函数名称添加其他签名。 — 尾注]
仅在唯一确定要引用哪个版本的重载的上下文中,才能使用重载函数的地址(请参见[over.over])。 [注意:由于上下文可能确定操作数是静态成员函数还是非静态成员函数,因此上下文也会影响表达式的类型是“函数指针”还是“成员函数指针” 。 — 尾注]
因此,如果程序的行为显式或隐式地尝试形成指向C ++库中成员函数的指针,则该行为是不确定的(可能是格式错误的)。
(感谢[expr.unary.op]/6指出这一点!)