using FilterContainer = // as before
std::vector>;
FilterContainer filters; // as before
void doSomeWork() {
auto pw = // create Widget; see
std::make_unique(); // Item 21 for
// std::make_unique
pw->addFilter(); // add filter that uses
// Widget::divisor
…
} // destroy Widget; filters
// now holds dangling pointer!
When a call is made to doSomeWork
, a filter is created that depends on the Widget
object produced by std::make_unique
, i.e., a filter that contains a copy of a pointer to that Widget
— the Widget
's this
pointer. This filter is added to filters
, but when doSomeWork
finishes, the Widget
is destroyed by the std::unique_ptr
managing its lifetime (see Item 18). From that point on, filters
contains an entry with a dangling pointer.
This particular problem can be solved by making a local copy of the data member you want to capture and then capturing the copy:
void Widget::addFilter() const {
auto divisorCopy = divisor; // copy data member
filters.emplace_back(
[divisorCopy](int value) // capture the copy
{ return value % divisorCopy== 0; } // use the copy
);
}
To be honest, if you take this approach, default by-value capture will work, too,
void Widget::addFilter() const {
auto divisorCopy = divisor; // copy data member
filters.emplace_back(
[=](int value) // capture the copy
{ return value % divisorCopy== 0; } // use the copy
);
}
but why tempt fate? A default capture mode is what made it possible to accidentally capture this
when you thought you were capturing divisor
in the first place.
In C++14, a better way to capture a data member is to use generalized lambda capture (see Item 32):
void Widget::addFilter() const {
filters.emplace_back( // C++14:
[divisor = divisor](int value) // copy divisor to closure
{ return value % divisor== 0; } // use the copy
);
}
There's no such thing as a default capture mode for a generalized lambda capture, however, so even in C++14, the advice of this Item — to avoid default capture modes — stands.
An additional drawback to default by-value captures is that they can suggest that the corresponding closures are self-contained and insulated from changes to data outside the closures. In general, that's not true, because lambdas may be dependent not just on local variables and parameters (which may be captured), but also on objects with static storage duration . Such objects are defined at global or namespace scope or are declared static
inside classes, functions, or files. These objects can be used inside lambdas, but they can't be captured. Yet specification of a default by-value capture mode can lend the impression that they are. Consider this revised version of the addDivisorFilter
function we saw earlier:
void addDivisorFilter() {
staticauto calc1 = computeSomeValue1(); // now static
staticauto calc2 = computeSomeValue2(); // now static
staticauto divisor= // now static
computeDivisor(calc1, calc2);
filters.emplace_back(
[=](int value) // captures nothing!
{ return value % divisor == 0; } // refers to above static
);
++divisor; // modify divisor
}
A casual reader of this code could be forgiven for seeing “ [=]
” and thinking, “Okay, the lambda makes a copy of all the objects it uses and is therefore self-contained.” But it's not self-contained. This lambda doesn't use any non-static local variables, so nothing is captured. Rather, the code for the lambda refers to the static
variable divisor
. When, at the end of each invocation of addDivisorFilter
, divisor
is incremented, any lambdas that have been added to filters
via this function will exhibit new behavior (corresponding to the new value of divisor
). Practically speaking, this lambda captures divisor
by reference, a direct contradiction to what the default by-value capture clause seems to imply. If you stay away from default by-value capture clauses, you eliminate the risk of your code being misread in this way.
Things to Remember
• Default by-reference capture can lead to dangling references.
• Default by-value capture is susceptible to dangling pointers (especially this
), and it misleadingly suggests that lambdas are self-contained.
Item 32: Use init capture to move objects into closures.
Sometimes neither by-value capture nor by-reference capture is what you want. If you have a move-only object (e.g., a std::unique_ptr
or a std::future
) that you want to get into a closure, C++11 offers no way to do it. If you have an object that's expensive to copy but cheap to move (e.g., most containers in the Standard Library), and you'd like to get that object into a closure, you'd much rather move it than copy it. Again, however, C++11 gives you no way to accomplish that.
But that's C++11. C++14 is a different story. It offers direct support for moving objects into closures. If your compilers are C++14-compliant, rejoice and read on. If you're still working with C++11 compilers, you should rejoice and read on, too, because there are ways to approximate move capture in C++11.
The absence of move capture was recognized as a shortcoming even as C++11 was adopted. The straightforward remedy would have been to add it in C++14, but the Standardization Committee chose a different path. They introduced a new capture mechanism that's so flexible, capture-by-move is only one of the tricks it can perform. The new capability is called init capture . It can do virtually everything the C++11 capture forms can do, plus more. The one thing you can't express with an init capture is a default capture mode, but Item 31explains that you should stay away from those, anyway. (For situations covered by C++11 captures, init capture's syntax is a bit wordier, so in cases where a C++11 capture gets the job done, it's perfectly reasonable to use it.)
Using an init capture makes it possible for you to specify
1. the name of a data memberin the closure class generated from the lambda and
2. an expressioninitializing that data member.
Here's how you can use init capture to move a std::unique_ptr
into a closure:
class Widget { // some useful type
public:
…
bool isValidated() const;
bool isProcessed() const;
bool isArchived() const;
private:
…
};
auto pw = std::make_unique(); // create Widget; see
Читать дальше