两个Lambda的调用运算符地址是否必然不同?有无合规唯一ID方案?
为任意可调用对象生成唯一ID的C++17方案问题
背景与现有方案
我们需要给任意可调用对象分配类似std::type_index的唯一ID,现有方案是利用函数地址作为ID:对于Lambda或仿函数X,取X::operator()的地址。但该方案存在未定义行为(UB)——需要将函数指针通过reinterpret_cast转换为std::uintptr_t类型。
测试现象
测试发现:
- GCC、Clang及MSVC Debug模式下,两个Lambda的
operator()地址均不同; - MSVC开启
/O2优化的Release构建中,两个空Lambda的operator()地址完全相同。
复现代码
#include <cstdint> #include <iostream> template <typename Lambda> std::uintptr_t address_of_call_operator(Lambda&&){ return []<typename Op>(Op const& pointer_to_op){ return reinterpret_cast<std::uintptr_t&>(const_cast<Op&>(pointer_to_op)); }(&Lambda::operator()); } int main() { std::cout << std::hex << address_of_call_operator([]{}) << "\n"; std::cout << std::hex << address_of_call_operator([]{}) << "\n"; }
编译输出
- MSVC Debug编译(
cl main.cpp /std:c++20):
7ff64cec1230 7ff64cec1240
- MSVC Release编译(
cl main.cpp /std:c++20 /O2):
7ff790a32b80 7ff790a32b80
问题
- 既然通常可依赖两个Lambda拥有不同类型,能否依赖其调用运算符地址必然不同?
- 有无不依赖UB、兼容主流平台的函数(指针)唯一ID获取方案?
解答
问题1:不能依赖调用运算符地址唯一
虽然每个Lambda都是独立的类型,但C++标准允许编译器对语义等价的函数进行合并(函数折叠优化)。空Lambda的operator()无任何副作用,语义完全等价,MSVC的/O2优化就触发了该行为——将两个空Lambda的调用运算符合并为同一个函数实例,因此地址相同。
标准并未要求不同类型的非虚成员函数必须拥有不同地址,所以依赖调用运算符地址唯一是不可靠的。
问题2:不依赖UB的兼容方案
方案1:利用std::type_index结合类型信息
通过获取可调用对象operator()的类型,用std::type_index生成唯一ID,完全符合标准:
#include <typeindex> #include <iostream> #include <utility> template<typename Callable> std::type_index get_callable_id(Callable&&) { using DecayedCallable = std::decay_t<Callable>; using OpType = decltype(&DecayedCallable::operator()); return std::type_index(typeid(OpType)); } // 普通函数指针重载 template<typename Ret, typename... Args> std::type_index get_callable_id(Ret(*)(Args...)) { return std::type_index(typeid(Ret(*)(Args...))); } // 成员函数指针重载 template<typename Ret, typename Class, typename... Args> std::type_index get_callable_id(Ret(Class::*)(Args...)) { return std::type_index(typeid(Ret(Class::*)(Args...))); } int main() { std::cout << get_callable_id([]{}) << "\n"; std::cout << get_callable_id([]{}) << "\n"; // 不同ID std::cout << get_callable_id([]() mutable {}) << "\n"; // 新的唯一ID }
每个Lambda的operator()类型是唯一的,因此生成的std::type_index必然唯一。
方案2:静态局部变量地址作为ID
利用模板实例化的特性,每个模板实例对应独立的静态局部变量,其地址可作为唯一ID,不存在UB(对象指针转std::uintptr_t是标准允许的):
#include <cstdint> #include <iostream> #include <utility> template<typename Callable> std::uintptr_t get_callable_id(Callable&&) { static char unique_tag; return reinterpret_cast<std::uintptr_t>(&unique_tag); } // 普通函数指针重载 template<typename Ret, typename... Args> std::uintptr_t get_callable_id(Ret(*)(Args...)) { static char unique_tag; return reinterpret_cast<std::uintptr_t>(&unique_tag); } int main() { std::cout << std::hex << get_callable_id([]{}) << "\n"; std::cout << std::hex << get_callable_id([]{}) << "\n"; // 不同地址 }
不同的可调用类型会触发模板的不同实例化,对应静态变量地址唯一。
方案3:std::function类型擦除获取ID
适用于需要统一ID类型的场景,通过std::function的目标类型信息生成ID:
#include <functional> #include <typeindex> #include <iostream> #include <utility> template<typename Callable> std::type_index get_callable_id(Callable&& c) { std::function f(std::forward<Callable>(c)); return std::type_index(typeid(*f.target<void>())); } int main() { std::cout << get_callable_id([]{}) << "\n"; std::cout << get_callable_id([]{}) << "\n"; }
std::function的target<void>()会返回指向内部可调用对象的指针,其类型信息唯一,因此ID可靠。
内容的提问来源于stack exchange,提问作者joergbrech
相关产品推荐
相关产品推荐

