but with an explicit capture, it's easier to see that the viability of the lambda is dependent on divisor's lifetime. Also, writing out the name, “divisor,” reminds us to ensure that divisorlives at least as long as the lambda's closures. That's a more specific memory jog than the general “make sure nothing dangles” admonition that “ [&]” conveys.
If you know that a closure will be used immediately (e.g., by being passed to an STL algorithm) and won't be copied, there is no risk that references it holds will outlive the local variables and parameters in the environment where its lambda is created. In that case, you might argue, there's no risk of dangling references, hence no reason to avoid a default by-reference capture mode. For example, our filtering lambda might be used only as an argument to C++11's std::all_of, which returns whether all elements in a range satisfy a condition:
template
void workWithContainer(const C& container) {
auto calc1 = computeSomeValue1(); // as above
auto calc2 = computeSomeValue2(); // as above
auto divisor= computeDivisor(calc1, calc2); // as above
using ContElemT = typename C::value_type; // type of
// elements in
// container
using std::begin; // for
using std::end; // genericity;
// see Item 13
if (std::all_of( // if all values
begin(container), end(container), // in container
[&](const ContElemT& value) // are multiples
{ return value % divisor == 0; }) // of divisor...
) {
… // they are...
} else {
… // at least one
} // isn't...
}
It's true, this is safe, but its safety is somewhat precarious. If the lambda were found to be useful in other contexts (e.g., as a function to be added to the filterscontainer) and was copy-and-pasted into a context where its closure could outlive divisor, you'd be back in dangle-city, and there'd be nothing in the capture clause to specifically remind you to perform lifetime analysis on divisor.
Long-term, it's simply better software engineering to explicitly list the local variables and parameters that a lambda depends on.
By the way, the ability to use autoin C++14 lambda parameter specifications means that the code above can be simplified in C++14. The ContElemTtypedef can be eliminated, and the if condition can be revised as follows:
if (std::all_of(begin(container), end(container),
[&](const auto& value) // C++14
{ return value % divisor == 0; }))
One way to solve our problem with divisorwould be a default by-value capture mode. That is, we could add the lambda to filtersas follows:
filters.emplace_back( // now
[=](int value) { return value % divisor == 0; } // divisor
); // can't
// dangle
This suffices for this example, but, in general, default by-value capture isn't the anti-dangling elixir you might imagine. The problem is that if you capture a pointer by value, you copy the pointer into the closures arising from the lambda, but you don't prevent code outside the lambda from deleteing the pointer and causing your copies to dangle.
“That could never happen!” you protest. “Having read Chapter 4, I worship at the house of smart pointers. Only loser C++98 programmers use raw pointers and delete.” That may be true, but it's irrelevant because you do, in fact, use raw pointers, and they can, in fact, be deleted out from under you. It's just that in your modern C++ programming style, there's often little sign of it in the source code.
Suppose one of the things Widgets can do is add entries to the container of filters:
class Widget {
public:
… // ctors, etc.
void addFilter() const; // add an entry to filters
private:
int divisor; // used in Widget's filter
};
Widget::addFiltercould be defined like this:
void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
}
To the blissfully uninitiated, this looks like safe code. The lambda is dependent on divisor, but the default by-value capture mode ensures that divisoris copied into any closures arising from the lambda, right?
Wrong. Completely wrong. Horribly wrong. Fatally wrong.
Captures apply only to non- staticlocal variables (including parameters) visible in the scope where the lambda is created. In the body of Widget::addFilter, divisoris not a local variable, it's a data member of the Widgetclass. It can't be captured. Yet if the default capture mode is eliminated, the code won't compile:
void Widget::addFilter() const {
filters.emplace_back( // error!
[](int value) { return value % divisor == 0; } // divisor
); // not
} // available
Furthermore, if an attempt is made to explicitly capture divisor(either by value or by reference — it doesn't matter), the capture won't compile, because divisorisn't a local variable or a parameter:
void Widget::addFilter() const {
filters.emplace_back(
[divisor](int value) // error! no local
{ return value % divisor == 0; } // divisor to capture
);
}
So if the default by-value capture clause isn't capturing divisor, yet without the default by-value capture clause, the code won't compile, what's going on?
The explanation hinges on the implicit use of a raw pointer: this. Every non- staticmember function has a thispointer, and you use that pointer every time you mention a data member of the class. Inside any Widgetmember function, for example, compilers internally replace uses of divisorwith this->divisor. In the version of Widget:: addFilterwith a default by-value capture,
void Widget::addFilter() const {
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
}
what's being captured is the Widget's thispointer, not divisor. Compilers treat the code as if it had been written as follows:
void Widget::addFilter() const {
auto currentObjectPtr = this;
filters.emplace_back(
[currentObjectPtr](int value)
{ return value % currentObjectPtr->divisor == 0; }
);
}
Understanding this is tantamount to understanding that the viability of the closures arising from this lambda is tied to the lifetime of the Widgetwhose thispointer they contain a copy of. In particular, consider this code, which, in accord with Chapter 4, uses pointers of only the smart variety:
Читать дальше