Modern C++中lambda表达式的陷阱

Modern C++中lambda表达式的陷阱

lambda表达式给stl带来了无与伦比的便利,尤其对像std::for_each这种使用函数指针的场合更是方便,但却是写的爽快,维护的蛋疼,前几天还遇到了一个陷阱,这里特意记录一下

陷阱1:默认引用捕获可能带来的悬挂引用问题

在捕获参数时喜欢使用[&]来一次捕获包括this在内的所有内容,此方法非常方便,但在遇到局部变量时,引用捕获却是非常容易出现问题,尤其用在事件系统,信号槽系统里时。

一个简单的lambda程序如下:

#include <iostream>
#include <functional>
using namespace std;

typedef std::function<void()> FP;
void run_fun_ptr(FP fp);
FP get_fun_ptr();
FP get_fun_ptr_ref();
int main()
{
    run_fun_ptr(get_fun_ptr());
    run_fun_ptr(get_fun_ptr_ref());
    return 0;
}

void run_fun_ptr(FP fp)
{
    if(fp)
    {
        fp();
    }
}

FP get_fun_ptr()
{
    int a = 2;
    return [=](){cout << "= a:"<<a << endl;};
}

FP get_fun_ptr_ref()
{
    int a = 2;
    return [&](){cout << "& a:"<<a << endl;};
}

结果输出:

= a:2
& a:4200153

这里get_fun_ptr正常输出,因为使用的是=号捕获,但get_fun_ptr_ref使用的是引用捕获,就会出现未定义的行为,因为捕获了一个临时变量,引用实际可以看成指针,在get_fun_ptr_ref之后,get_fun_ptr_refint a = 2;的临时变量会释放(出栈),此时指针就不知道指的是什么东西了。

在有事件循环系统时,最典型的就是ui程序,若lambda的触发是依据某个事件,如一个鼠标对按钮的点击,但lambda却引用捕获了一个局部变量,在创建时变量存在,但在触发点击时,变量很有可能已经销毁了,这时就会有未定义错误发生。

如下例子是SA的一个生成最近打开文件菜单项目的例子,作用就是把记录最近打开的文件路径生成一系列菜单项目,在第二个lambda表达式中,若用默认引用捕获,会把QAction* act作为引用捕获,在此函数结束后,将变成悬空引用

    std::for_each(m_recentOpenFiles.begin(),m_recentOpenFiles.end(),[&](const QString& strPath){
        QAction* act = new QAction(strPath,this);
        connect(act,&QAction::triggered,this,[this,act](bool on){
            Q_UNUSED(on);
            this->openFile(act->text());
        });
        ui->menuRecentOpenFile->addAction(act);
    });

陷阱2:捕获this陷阱

后来在网上看到这篇文章Effective Modern C++ 条款31 对于lambda表达式,避免使用默认捕获模式

看来这是Modern C++的新坑,还好Effective系列把这些都点明了,文章除了上面说的捕获悬挂引用情况,还有一种情况会导致问题,就是lambda使用当前类外的变量时要异常小心其捕获的this指针,如lambda使用了全局变量,或者lambda所在类以外生命周期比这个类长的变量

#include <iostream>
#include <functional>
#include <vector>
#include <memory>
#include <algorithm>
typedef std::function<void()> FP;

class Point
{
public:
    Point(int x,int y):m_x(x),m_y(y)
    {

    }
    void print()
    {
        s_print_history.push_back([=](){std::cout << "(X:" << m_x << ",Y:" << m_y <<")" << std::endl;});
        std::cout << "(X:" << m_x << ",Y:" << m_y <<")" << std::endl;
    }

    static void print_history()
    {
        std::for_each(s_print_history.begin(),s_print_history.end(),[](FP p){
            if(p)
                p();
        });
    }

private:
    int m_x;
    int m_y;
    typedef std::function<void(void)> FP;
    static std::vector<FP> s_print_history;
};
std::vector<FP> Point::s_print_history = std::vector<FP>();

int main()
{
    std::unique_ptr<Point> p;
    p.reset(new Point(1,1));p->print();
    p.reset(new Point(2,2));p->print();
    p.reset(new Point(3,3));p->print();
    Point::print_history();
    return 0;
}

输出结果

(X:1,Y:1)
(X:2,Y:2)
(X:3,Y:3)
print history:
(X:3,Y:3)
(X:2,Y:2)
(X:3,Y:3)

这个历史输出明显不是正确的结果,这个历史已经是一个未定义的行为,别的编译器输出的结果和我这里编译的结果应该是不一样的,这里就是this的捕获陷阱

s_print_history.push_back([=](){std::cout << "(X:" << m_x << ",Y:" << m_y <<")" << std::endl;});

这句lambda通过默认值捕获,其实只是捕获了this指针,在lambda里使用m_x,相当于this->m_x。在this销毁后在调用这个lambda,这时候的this就不知指到哪里了。

由于lambda里有比创建这个lambda的this生命周期更长的变量,一般是引入这个类的其他类型变量或者是静态变量和全局变量,一旦涉及到这三种东西,不建议用lambda,但任性要用,需要做一个中转,上述打印代码应该改为:

void print()
{
    int x = m_x;
    int y = m_y;
    s_print_history.push_back([x,y](){std::cout << "(X:" << x << ",Y:" << y <<")" << std::endl;});
    std::cout << "(X:" << m_x << ",Y:" << m_y <<")" << std::endl;
}

这时,会把x,y通过传值捕获,lambda里没有保存this指针信息,避免隐藏this的影响。

具体建议大家看看这篇文章Effective Modern C++ 条款31 对于lambda表达式,避免使用默认捕获模式

总结

  • 引用捕获陷阱:引用捕获[&]别使用局部变量

  • this陷阱:lambda里避免有全局变量或静态变量或者比当前类生命周期更长的变量

  • 尽量避免使用复杂的lambda

本文源码请见:
https://github.com/czyt1988/czyBlog/tree/master/tech/lambda_ref_trap

<think>好的,我现在需要回答用户关于如何在C++17中以简洁的方式将lambda表达式作为函数参数传递的问题。用户已经提供了一些引用内容,我需要结合这些内容和之前的回答来给出准确的信息。 首先,回顾之前的回答,用户已经了解了一些C++11、C++14中的lambda表达式作为参数的简化方法,比如自动类型推导、泛型lambda和模板参数。现在用户特别询问C++17中的简洁写法,我需要找出C++17特有的相关特性。 C++17引入了几个新特性,比如类模板参数推导(CTAD)、结构化绑定、constexpr lambda等,但这里需要关注的是与lambda作为参数传递相关的改进。根据之前的引用,用户提到的引用4中提到了递归lambda的写法,使用std::function,这在C++11中已经存在,但C++17可能有其他优化。 另外,用户之前的问题涉及如何简化lambda作为参数的传递,可能涉及到模板推导、自动类型、以及如何避免冗长的std::function声明。在C++17中,模板参数推导可能更加灵活,允许更简洁的模板函数参数声明。 需要确认的是,C++17是否允许更简洁的模板函数定义,比如使用auto作为参数类型,或者进一步简化lambda捕获和参数列表。同,用户提供的引用3提到lambda在函数内部使用的便利性,可能需要在C++17中如何更好地应用。 此外,可能需要考虑C++17中的constexpr lambda,虽然这更多与编译计算相关,但可能影响lambda作为参数的使用方式。另外,类模板参数推导可能简化某些模板的使用,比如std::function的构造,从而让传递lambda更简洁。 总结可能的C++17相关简洁写法包括: 1. 使用模板参数推导,无需显式指定模板类型。 2. 利用constexpr lambda在编译期的优化。 3. 结合auto参数和泛型lambda的进一步简化。 4. 使用CTAD简化std::function或其他包装器的声明。 需要验证这些点是否正确,并给出具体的代码示例。例如,在C++17中,模板函数可以自动推导lambda的类型,因此可以更简洁地编写接收lambda作为参数的函数。同,可能可以省略某些显式类型声明,让代码更简洁。 另外,用户之前的回答中提到了使用模板参数F来接收可调用对象,这在C++17中依然适用,但可能结合新的特性有更优的写法。例如,使用auto作为函数参数类型,但C++17中普通函数不支持auto参数,所以可能需要继续使用模板。 最后,确保回答结构清晰,分点说明,并给出示例代码,同对比C++17与之前版本的不同之处,突出简洁性。</think>### C++17中Lambda表达式作为函数参数的简洁写法 #### 一、**核心方法:模板参数自动推导(C++17增强)** C++17通过增强类模板参数推导(CTAD)和模板函数推导能力,使得传递Lambda表达式更加简洁: ```cpp // 定义模板函数(无需显式指定std::function) template<typename F> void process(F&& callback) { callback(42); // 直接调用 } // 使用示例 process([](int x) { std::cout << "值:" << x; }); ``` - **优势**: - 完全避免类型擦除,保留Lambda的原始性能[^2] - 支持任意可调用对象(Lambda、函数指针、仿函数等) - **C++17改进**: - 更精确的模板参数推导规则 - 支持完美转发(`std::forward<F>(callback)`) --- #### 二、**constexpr Lambda表达式C++17新增)** C++17允许Lambda在编译期求值,可直接作为模板参数传递: ```cpp // 定义接收编译期Lambda的模板函数 template<auto Func> void constexpr_processor() { constexpr int result = Func(10); std::cout << "结果:" << result; } int main() { constexpr_processor<[](int x) { return x * 2; }>(); // 输出20 } ``` - **应用场景**: - 数学计算库的编译期优化 - 元编程中替代函数对象 - **限制**: - Lambda必须满足`constexpr`要求(无动态内存分配等) --- #### 三、**结构化绑定简化参数传递(C++17特性)** 结合结构化绑定实现多返回值Lambda的简洁处理: ```cpp auto get_values = [] { return std::tuple{42, "hello", 3.14}; }; // 接收Lambda返回值的函数 template<typename F> void handle_result(F&& func) { const auto& [num, str, dbl] = func(); std::cout << num << ", " << str << ", " << dbl; } // 调用 handle_result(get_values); // 输出: 42, hello, 3.14 ``` --- #### 四、**if constexpr优化类型处理** 利用`if constexpr`简化泛型Lambda的类型分支处理: ```cpp template<typename F> void smart_invoke(F&& f) { if constexpr (std::is_invocable_v<F, int>) { f(42); } else { f(); } } // 使用示例 smart_invoke([](auto x) { std::cout << x; }); // 输出42 smart_invoke([]{ std::cout << "无参数"; }); // 输出"无参数" ``` --- #### 五、**标准库工具组合应用** 结合`std::invoke`实现更通用的调用接口: ```cpp #include <functional> template<typename F, typename... Args> decltype(auto) universal_call(F&& f, Args&&... args) { return std::invoke(std::forward<F>(f), std::forward<Args>(args)...); } // 使用示例 universal_call([](int x) { return x * 2; }, 21); // 返回42 ``` --- ### 对比C++11/14/17写法演进 | 特性 | C++11 | C++14 | C++17 | |--------------------|--------------------------------|--------------------------------|--------------------------------| | 类型推导 | 需要显式模板参数 | 支持`auto`参数 | 增强模板参数推导规则 | | 编译期Lambda | 不支持 | 部分支持 | 完整`constexpr`支持 | | 返回值类型推导 | 必须显式声明 | 支持省略`-> ret_type` | 支持复杂类型推导 | | 标准库集成 | 依赖`std::function` | 改进的`std::bind` | `std::invoke`通用化 | --- ### 工程实践建议 1. **优先使用模板参数**: ```cpp // 推荐写法 template<typename F> void modern_handler(F&& f) { /*...*/ } // 避免写法 void old_style(std::function<void()> f) { /*...*/ } ``` 2. **完美转发优化**: ```cpp template<typename F, typename... Args> auto safe_call(F&& f, Args&&... args) { return std::invoke(std::forward<F>(f), std::forward<Args>(args)...); } ``` 3. **类型约束(C++20前)**: ```cpp template<typename F> auto validate(F&& f) -> decltype(f(0), void()) { f(42); } ``` --- ### 完整示例:事件处理器 ```cpp #include <vector> #include <functional> template<typename... Handlers> class EventDispatcher { public: // 变参模板构造函数 EventDispatcher(Handlers&&... handlers) { (register_handler(std::forward<Handlers>(handlers)), ...); } void trigger(int event_id) { for (auto& h : handlers_) { h(event_id); } } private: template<typename F> void register_handler(F&& f) { handlers_.emplace_back(std::forward<F>(f)); } std::vector<std::function<void(int)>> handlers_; }; int main() { // 单行初始化多个Lambda处理器 EventDispatcher dispatcher{ [](int id) { std::cout << "事件A:" << id << "\n"; }, [](int id) { std::cout << "事件B:" << id << "\n"; } }; dispatcher.trigger(1001); // 输出: // 事件A:1001 // 事件B:1001 } ``` --- ### 相关问题 1. 如何在C++17中实现Lambda表达式的完美转发? 2. C++17的`constexpr` Lambda有哪些使用限制? 3. 如何结合C++17的折叠表达式Lambda参数包? 4. 模板参数推导规则在C++17中有哪些重要改进? [^1]: 引用C++ Lambda表达式基础说明 [^2]: 参考Lambda表达式与函数指针的性能对比 [^3]: 函数内部定义Lambda的优势分析 [^4]: Lambda递归调用的实现方式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尘中远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值