Effective Modern C++ 条款31:避免使用默认捕获模式
避免使用默认捕获模式
- 引言
- 按引用捕获的危害
- 示例:悬空引用的危险
- 按值捕获的问题
- 示例:按值捕获的误导
- 显式捕获的好处
- 示例:显式捕获的安全性
- 成员变量的捕获
- 示例:隐式捕获`this`指针
- 解决方案:显式捕获成员变量
- 总结
在C++11中,lambda表达式引入了两种默认捕获模式:按引用捕获([&]
)和按值捕获([=]
)。这两种模式虽然提供了便利,但也隐藏着潜在的风险。本文将深入探讨默认捕获模式的危害,并提供最佳实践建议,以帮助开发者编写更安全、更可靠的代码。
引言
默认捕获模式在C++中提供了极大的灵活性,但如果不加以注意,可能会导致悬空引用(dangling reference)或误导开发者认为lambda是独立的。这些潜在的问题可能在代码运行时引发未定义行为,影响程序的稳定性和正确性。
按引用捕获的危害
按引用捕获模式([&]
)允许lambda捕获其所在作用域内的所有局部变量和形参的引用。然而,这种模式可能导致悬空引用,因为引用的生命周期可能超过被引用对象的生命周期。
示例:悬空引用的危险
using FilterContainer = std::vector<std::function<bool(int)>>;void addDivisorFilter()
{auto calc1 = computeSomeValue1();auto calc2 = computeSomeValue2();auto divisor = computeDivisor(calc1, calc2);filters.emplace_back([&](int value) { return value % divisor == 0; });
}
在这个示例中,divisor
是一个局部变量。当addDivisorFilter
函数返回时,divisor
的生命周期结束,而lambda捕获了对它的引用。如果之后使用这个lambda,将导致悬空引用,引发未定义行为。
按值捕获的问题
按值捕获模式([=]
)将lambda所在作用域内的所有局部变量和形参按值捕获。虽然这避免了悬空引用的风险,但它也可能带来误导,使开发者认为lambda是独立的,而实际上可能依赖外部变量。
示例:按值捕获的误导
void addDivisorFilter()
{static auto calc1 = computeSomeValue1();static auto calc2 = computeSomeValue2();static auto divisor = computeDivisor(calc1, calc2);filters.emplace_back([=](int value) { return value % divisor == 0; });++divisor;
}
在这个示例中,divisor
是一个静态变量。尽管lambda使用了按值捕获模式,但它并没有捕获divisor
,而是引用了静态变量。每次调用addDivisorFilter
时,divisor
都会递增,导致lambda的行为发生变化。这与开发者可能认为的按值捕获是独立的形成鲜明对比。
显式捕获的好处
显式捕获([variable]
或[&variable]
)允许开发者明确指定lambda捕获的变量。这种做法提高了代码的可读性和维护性,并减少了潜在的错误。
示例:显式捕获的安全性
void addDivisorFilter()
{auto calc1 = computeSomeValue1();auto calc2 = computeSomeValue2();auto divisor = computeDivisor(calc1, calc2);filters.emplace_back([divisor](int value) { return value % divisor == 0; });
}
在这个示例中,divisor
被显式捕获,确保其生命周期与lambda一致。这种做法避免了悬空引用的风险,并使代码更易于理解和维护。
成员变量的捕获
在类成员函数中,lambda可能会隐式捕获this
指针。如果不加以注意,可能导致悬空指针。
示例:隐式捕获this
指针
class Widget {
public:void addFilter() const;
private:int divisor;
};void Widget::addFilter() const
{filters.emplace_back([=](int value) { return value % divisor == 0; });
}
在这个示例中,divisor
是Widget
类的成员变量。lambda隐式捕获了this
指针,而不是divisor
。如果Widget
对象在lambda使用前被销毁,将导致悬空指针。
解决方案:显式捕获成员变量
void Widget::addFilter() const
{auto divisorCopy = divisor;filters.emplace_back([divisorCopy](int value) { return value % divisorCopy == 0; });
}
在这个解决方案中,divisor
被显式捕获并存储在divisorCopy
中,确保其生命周期与lambda一致。
总结
默认捕获模式在C++中提供了便利,但如果不加以注意,可能导致悬空引用或误导开发者。显式捕获是一种更安全和可靠的做法,能够提高代码的可读性和维护性。在实际开发中,建议避免使用默认捕获模式,并显式指定lambda捕获的变量。