C++mem_fn成员函数转函数对象

2026-04-11 06:15:33 1992阅读 0评论

mem_fn:把成员函数“拎出来”用的轻量工具

写 C++ 时,你有没有遇到过这种场景:想把某个类的成员函数传给 std::for_eachstd::transform 或者 std::thread,却发现编译器死活不认——报错说“不能把 void (A::*)() 转成可调用对象”?
这时候翻文档,可能看到 std::mem_fn,但点进去一看,只有几行模板声明和一句模糊的“生成函数对象”,接着就没了下文。它不像 std::bind 那样热闹,也不像 lambda 那样直观,于是很多人干脆绕开它,要么手写 lambda,要么硬套 std::bind。其实,mem_fn 不是过时的摆设,而是一个精准、轻量、无捕获开销的成员函数适配器——只是它太安静,容易被忽略。

std::mem_fn 的核心作用,是把一个成员函数指针(比如 &MyClass::process转换为一个普通函数对象,这个对象接受第一个实参作为 this 指针(或引用),后续参数按原顺序传递。它不绑定任何值,不保存状态,不做类型擦除,甚至连 std::function 都不依赖。它干的只有一件事:解包调用约定

举个实在的例子。假设你有这样一个类:

struct Logger {
    void log(const std::string& msg) { std::cout << "[LOG] " << msg << "\n"; }
    void warn(int code, const std::string& msg) { std::cout << "[WARN " << code << "] " << msg << "\n"; }
};

现在你想批量调用 log 方法,比如对一个 vector<Logger> 中每个对象都执行一次:

std::vector<Logger> logs(3);
std::for_each(logs.begin(), logs.end(), std::mem_fn(&Logger::log));
// ❌ 编译失败:log 需要一个 string 参数,但 mem_fn(&Logger::log) 只接受一个参数(this)

这里踩了第一个坑:std::mem_fn(&Logger::log) 生成的函数对象,签名是 void(Logger&),不是 void(Logger&, const std::string&)。它只负责“把对象送进去”,不负责补全参数。所以正确写法是:

std::for_each(logs.begin(), logs.end(),
    [msg = "startup complete"](Logger& l) { l.log(msg); });
// ✅ 用 lambda 更直接

mem_fn 还有什么用?它的真正主场,是需要延迟绑定 this,但参数已知、且不想引入闭包开销的场合。比如,你有一个 vector<std::unique_ptr<Logger>>,想统一调用 warn,但警告码和消息是运行时确定的:

auto warn_fn = std::mem_fn(&Logger::warn);
int code = 404;
std::string msg = "not found";

// 现在你可以安全地把它存起来、传给别的函数,甚至放进容器
std::vector<std::function<void(Logger&)>> handlers;
handlers.push_back([=](Logger& l) { l.warn(code, msg); }); // 有捕获,有开销
// vs
handlers.push_back([=, fn = warn_fn](Logger& l) { fn(l, code, msg); }); // 仍带捕获

等等——这好像也没省事?别急。关键在组合使用mem_fn 最自然的搭档,其实是 std::bind 的“半绑定”模式:

auto warn_404 = std::bind(std::mem_fn(&Logger::warn), _1, 404, "not found");
// 现在 warn_404 是一个接受单个 Logger& 的可调用对象
std::for_each(ptr_logs.begin(), ptr_logs.end(), warn_404);

注意:这里 std::mem_fn 并非必须,std::bind(&Logger::warn, _1, 404, "not found") 同样可行。但 mem_fn 的优势在于——它明确剥离了成员函数的调用语义,让 bind 的意图更清晰;更重要的是,在 C++20 后,mem_fn 支持 SFINAE 友好检测,而裸指针直接传给 bind 在某些老编译器上会触发硬错误

还有一个常被忽视的细节:mem_fnconst 成员函数、volatile 限定符、甚至引用限定符(&&)都严格保留。比如:

struct Data {
    int value() const& { return 42; }
};
auto get_val = std::mem_fn(&Data::value);
Data d;
static_assert(std::is_same_v<decltype(get_val(d)), int>);
// 如果你传的是临时对象:get_val(Data{}) —— 编译失败,因为 value() 是 const& 限定

这意味着,mem_fn 不是粗暴“抹平”语义,而是忠实地复刻原函数的约束。你在用它的时候,等于在代码里悄悄加了一层类型契约。

那么,什么时候该选 mem_fn?三个信号很明确:

  • 你需要把成员函数指针转成可存储、可传递的函数对象,且不希望产生 lambda 捕获或 std::function 的动态分配开销
  • 你正在写泛型代码,需要 SFINAE 或 concepts 判断某个类型是否支持某成员函数调用(mem_fn 的返回类型可推导,比裸指针更友好);
  • 你在封装一个通用的“调用器”工具,比如日志分发器、事件回调注册表,要求调用签名干净、零成本抽象。

最后提醒一句:C++20 引入了 std::ranges 和更强大的 std::bind_frontmem_fn 的出场机会确实变少了。但它没被淘汰——就像螺丝刀不会因为电钻普及就被扔进垃圾桶。当你需要一把不带电池、不占空间、拧紧即走的工具时,mem_fn 依然在工具箱最顺手的位置。

它不炫技,不抢镜,只在你需要它精准咬合的那一瞬间,稳稳地转一下。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1992人围观)

还没有评论,来说两句吧...

目录[+]