为何函数指针与std::function对引用类型的处理行为存在差异?
场景说明
函数指针与std::function对象在以下场景中行为表现不同:
- 实际函数定义以值作为参数
- 函数指针(或
std::function对象)的定义指定以引用作为参数
测试代码
#include <cstdint> #include <format> #include <functional> #include <iostream> void Foo(std::uintptr_t value, std::uintptr_t& reference){ std::cout << std::format("value={}(0x{:x}), reference={}", value, value, reference) << std::endl; std::cout << std::format("&value={}, &reference={}", (void*)&value, (void*)&reference) << std::endl; std::cout << std::endl; } int main(int argc, char* argv[]){ std::uintptr_t value = 1234; std::cout << std::format("&value={}", (void*)&value) << std::endl; std::cout << std::endl; std::cout << "[function pointer]" << std::endl; auto function_pointer = (void(*)(std::uintptr_t&, std::uintptr_t&))(&Foo); function_pointer(value, value); std::cout << "[function object]" << std::endl; std::function<void(std::uintptr_t&, std::uintptr_t&)> function_object = Foo; function_object(value, value); return 0; }
实际输出
&value=0xc26ab6f770
[function pointer]
value=835014031216(0xc26ab6f770), reference=1234
&value=0xc26ab6f6f0, &reference=0xc26ab6f770[function object]
value=1234(0x4d2), reference=1234
&value=0xc26ab6f6f0, &reference=0xc26ab6f770
预期输出
&value=0xc26ab6f770
[function pointer]
value=835014031216(0xc26ab6f770), reference=1234
&value=0xc26ab6f6f0, &reference=0xc26ab6f770[function object]
value=835014031216(0xc26ab6f770), reference=1234
&value=0xc26ab6f6f0, &reference=0xc26ab6f770
核心疑问
为何会出现这种差异?std::function的结果是否属于定义行为?
补充说明
之前的问题描述不够准确,具体场景是:
在代码中使用std::function,并通过模板将其类型设定为std::function<ReturnType(ArgTypes&...)>。由于std::function的参数被指定为引用类型,原本认为若将接收非引用参数的函数存入该std::function对象,程序将无法正常运行。但实际测试时,存入接收非引用参数的函数后,MSVC++编译器未给出任何警告,且程序运行正常。
因此核心疑问是:
为何实际函数与std::function对象的签名不匹配,代码却能无警告编译并正常运行?
个人理解:引用类型在内部是通过传递值的地址而非值本身来实现的,因此认为若std::function的参数类型指定为引用类型,但实际函数并非如此,程序会因传递值的地址而非值本身而无法正常运行。
1. 函数指针强制转换的问题
你对函数指针的强制转换属于未定义行为。Foo的签名是void(std::uintptr_t, std::uintptr_t&),但你强行将其转换为void(*)(std::uintptr_t&, std::uintptr_t&)类型的指针,调用时会违反函数调用的ABI约定:第一个参数原本应该传值,现在却传了引用(本质是指针),所以Foo的第一个形参value会接收到value变量的地址值,这就是你看到value=835014031216(即变量value的地址)的原因。这种转换和调用完全不符合C++标准,结果不可预测。
2. std::function的适配逻辑
std::function的设计目标之一就是适配兼容的可调用对象,它会自动处理参数类型的兼容转换,只要调用时的参数能被转换为目标函数的参数类型即可。
当你把Foo(签名void(std::uintptr_t, std::uintptr_t&))赋值给std::function<void(std::uintptr_t&, std::uintptr_t&)>时,std::function会生成一个适配层:
- 对于第一个参数,
std::function接收的是std::uintptr_t&,但Foo需要的是std::uintptr_t,适配层会自动解引用这个引用,把引用指向的值传递给Foo的第一个形参,因此Foo的value会拿到1234,而非变量地址。 - 第二个参数两者都是引用类型,直接传递即可,所以行为和预期一致。
这种适配是符合C++标准的定义行为,std::function会通过类型擦除和包装器处理参数的兼容转换,只要参数之间存在隐式转换(这里引用到值的转换是合法的,因为可以从引用获取值),就可以正常适配。
3. 为何编译器无警告?
MSVC未给出警告,是因为std::function的适配是标准允许的合法转换,编译器认为这是符合预期的代码。如果想要检测这种签名不匹配的情况,可以启用更严格的编译警告(比如MSVC的/W4或/Wall),或者使用模板元编程在编译期检查可调用对象的签名是否完全匹配。
内容的提问来源于stack exchange,提问作者Sylvia

