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 divisor
lives 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 filters
container) 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 auto
in C++14 lambda parameter specifications means that the code above can be simplified in C++14. The ContElemT
typedef 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 divisor
would be a default by-value capture mode. That is, we could add the lambda to filters
as 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 delete
ing 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 delete
d 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 Widget
s 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::addFilter
could 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 divisor
is copied into any closures arising from the lambda, right?
Wrong. Completely wrong. Horribly wrong. Fatally wrong.
Captures apply only to non- static
local variables (including parameters) visible in the scope where the lambda is created. In the body of Widget::addFilter
, divisor
is not a local variable, it's a data member of the Widget
class. 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 divisor
isn'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- static
member function has a this
pointer, and you use that pointer every time you mention a data member of the class. Inside any Widget
member function, for example, compilers internally replace uses of divisor
with this->divisor
. In the version of Widget:: addFilter
with 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 this
pointer, 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 Widget
whose this
pointer they contain a copy of. In particular, consider this code, which, in accord with Chapter 4, uses pointers of only the smart variety:
Читать дальше