You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在延迟调用模板中条件转换std::forward参数为char*(SFINAE)

问题分析

实现类成员函数延迟调用工具NotifyAll/NotifyAllNow时,遇到char*参数(如std::string::c_str())的生命周期问题:原字符串内存释放后,延迟调用触发时会出现未定义行为。虽然设计了LifeExtender类持有字符串副本以延长生命周期,但在std::invoke环节无法自动将LifeExtender对象转换为char*,同时还要保证其他参数的正常转发。尝试基于std::enable_if的identity函数编译失败,临时方案存在过多拷贝问题,且不能修改目标成员函数的char*参数类型。

解决方案

核心思路是通过两个辅助逻辑实现:

  1. 自动包装char*参数:在创建延迟调用对象时,自动将const char*类型的参数包装为LifeExtender,其他参数保持原类型并完美转发。
  2. 参数转发时的类型适配:在触发调用时,对LifeExtender类型的参数自动转换为const char*,其他参数直接转发。

1. 实现LifeExtender类

首先定义持有字符串副本的辅助类,提供到const char*的隐式转换:

#include <string>
#include <functional>
#include <tuple>
#include <type_traits>
#include <cstdio>

class LifeExtender {
private:
    std::string str_;
public:
    explicit LifeExtender(const char* s) : str_(s) {}
    // 隐式转换为const char*,供目标函数调用时使用
    operator const char*() const noexcept {
        return str_.c_str();
    }
};

2. 参数包装与转发的辅助函数

  • wrap_arg:自动识别const char*参数并包装为LifeExtender,其他参数直接转发。
  • forward_arg:调用时将LifeExtender转换为const char*,其他参数保持完美转发。
// 判断是否为const char*类型(忽略cv限定)
template <typename T>
struct is_const_char_ptr : std::is_same<std::remove_cv_t<T>, const char*> {};

// 包装参数:const char*转为LifeExtender,其他类型直接转发
template <typename T>
auto wrap_arg(T&& arg) {
    if constexpr (is_const_char_ptr<std::decay_t<T>>::value) {
        return LifeExtender(std::forward<T>(arg));
    } else {
        return std::forward<T>(arg);
    }
}

// 转发参数:LifeExtender转为const char*,其他类型完美转发
template <typename T>
std::enable_if_t<!std::is_same_v<std::decay_t<T>, LifeExtender>, T&&>
forward_arg(T&& arg) noexcept {
    return std::forward<T>(arg);
}

inline const char* forward_arg(LifeExtender&& le) noexcept {
    return static_cast<const char*>(le);
}

inline const char* forward_arg(const LifeExtender& le) noexcept {
    return static_cast<const char*>(le);
}

3. 延迟调用对象的实现

重新实现DelayedCall模板类,利用上述辅助函数处理参数:

template <typename Class, typename... Args>
class DelayedCall {
private:
    Class* obj_;
    void (Class::*func_)(Args...);
    // 保存包装后的参数tuple
    std::tuple<decltype(wrap_arg(std::declval<Args>()))...> args_;

    // 递归展开tuple并调用目标函数
    template <std::size_t... I>
    void invoke_impl(std::index_sequence<I...>) {
        (obj_->*func_)(forward_arg(std::get<I>(std::move(args_)))...);
    }

public:
    // 构造函数:接收对象指针、成员函数指针和原始参数,自动包装参数
    DelayedCall(Class* obj, void (Class::*func)(Args...), Args&&... args)
        : obj_(obj), func_(func), args_(wrap_arg(std::forward<Args>(args))...) {}

    // 触发延迟调用
    void operator()() {
        invoke_impl(std::make_index_sequence<sizeof...(Args)>{});
    }
};

// NotifyAll函数:创建延迟调用对象
template <typename Class, typename... Args>
auto NotifyAll(Class* obj, void (Class::*func)(Args...), Args&&... args) {
    return DelayedCall<Class, Args...>(obj, func, std::forward<Args>(args)...);
}

4. 测试示例

class TestClass {
public:
    void print(const char* msg, int num) {
        printf("Msg: %s, Num: %d\n", msg, num);
    }
};

int main() {
    TestClass obj;
    // 传入临时std::string的c_str(),原字符串会立即销毁
    auto delayed = NotifyAll(&obj, &TestClass::print, std::string("Hello World").c_str(), 42);
    // 触发延迟调用,此时LifeExtender仍持有字符串副本
    delayed();
    return 0;
}
方案优势
  • 自动处理生命周期:无需手动传入LifeExtender,所有const char*参数都会被自动包装,避免内存访问问题。
  • 完美转发:非char*参数通过std::forward实现完美转发,避免不必要的拷贝。
  • 无侵入性:不需要修改目标成员函数的参数类型,完全兼容原有接口。

内容的提问来源于stack exchange,提问作者Gizmo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 01:02:32