|
| 1 | +# Item31 : Avoid default capture modes |
| 2 | + |
| 3 | +### Lamda expression : 표현식, 소스코드의 일부로 아래가 람다 표현식이다 |
| 4 | +```c++ |
| 5 | +std::find_if(container.begin(), container.end(), |
| 6 | + [](int val) { return 0 < val && val < 10; }); |
| 7 | +``` |
| 8 | +### Closure : 람다에 의해 만들어진 실행시점 *객체*, capture mode 에 따라 클로저는 capture된 data의 복사본이나 참조를 가질 수 있다. |
| 9 | +### (위 예제에서 세번째 paramter 로 전달되는 객체) |
| 10 | +
|
| 11 | +### Closure class : 클로저를 만드는데 사용된 클래스, 컴파일러는 각 람다들에 대해 고유한 closure class 를 만들어낸다. |
| 12 | +------------------------------------ |
| 13 | +
|
| 14 | +## Default capture mode |
| 15 | +### 1. By Reference "\[&\]\(\) {...}" |
| 16 | +### * 참조가 대상을 잃을(dangling) 위험이 있다. |
| 17 | +### 2. By Value "\[=\]\(\) {...}" |
| 18 | +### * by value 는 capture 된 객체가 대상을 잃지 않을것 같지만 실제로 그렇지 않고, |
| 19 | +### self-contained 처럼 보이지만 그렇지 않은 경우도 있다. |
| 20 | +---------------------------------------- |
| 21 | +
|
| 22 | +## 1. By Reference 로 발생할 수 있는 문제 |
| 23 | +```c++ |
| 24 | +using FilterContainer = |
| 25 | + std::vector<std::function<bool(int)>>; // 필터링 하는 "함수 객체"를 받는 컨테이너 |
| 26 | +FilterContainer filters; // 필터링 함수들 |
| 27 | +
|
| 28 | +void addDivisorFilter() { |
| 29 | + auto calc1 = computeSomeValue1(); |
| 30 | + auto calc2 = computeSomeValue2(); |
| 31 | +
|
| 32 | + auto divisor = computeDivisor(calc1, calc2); |
| 33 | + filters.emplace_back( // danger! ref to divisor will dangle |
| 34 | + [&](int value) { return value % divisor == 0; } |
| 35 | + ); |
| 36 | + } |
| 37 | +``` |
| 38 | +#### filter 가 추가되고 이후에 다른 context 에서 filters 에 포함된 함수객체들이 호출이 될 수 있으나 해당 시점에는 지역 객체인 divisor 가 존재하지 않음 |
| 39 | +```c++ |
| 40 | +filters.emplace_back( |
| 41 | + [&divisor](int value) // default caputre 모드를 사용하지 않아도 여전히 같은 문제 발생 |
| 42 | + { return value % divisor == 0; } |
| 43 | + ); |
| 44 | +``` |
| 45 | +
|
| 46 | +#### closure 가 바로 사용된다면? |
| 47 | +```c++ |
| 48 | +template<typename C> |
| 49 | + void workWithContainer(const C& container) |
| 50 | + { |
| 51 | + auto calc1 = computeSomeValue1(); |
| 52 | + auto calc2 = computeSomeValue2(); |
| 53 | + auto divisor = computeDivisor(calc1, calc2); |
| 54 | +
|
| 55 | + using ContElemT = typename C::value_type; |
| 56 | + |
| 57 | + |
| 58 | + using std::begin; |
| 59 | + using std::end; |
| 60 | + |
| 61 | + if (std::all_of( |
| 62 | + begin(container), end(container), // 컨테이터의 모든 값이 divisor의 배수인가? |
| 63 | + [&](const ContElemT& value) |
| 64 | + { return value % divisor == 0; }) |
| 65 | + ) { |
| 66 | + … // 그런 경우 |
| 67 | + } else { |
| 68 | + … // 아닌 경우 |
| 69 | + } |
| 70 | +``` |
| 71 | +#### 위 예제는 안전함, 하지만 해당 람다를 copy&paste 해서 가져다가 쓰는 경우에는 분명히 문제가 발생함 |
| 72 | +#### (일반 함수라면 문제가 발생하지 않았을 것) |
| 73 | + |
| 74 | +### => 람다가 의존하는 지역 변수들과 매개변수를 명시적으로 표기하는것이 소프트웨어 공학적인 관점에서 더 좋다. |
| 75 | +------------------------------------ |
| 76 | + |
| 77 | +## 2. By Value 로 발생할 수 있는 문제 |
| 78 | +### - Dangling 이 여전히 발생 할 수 있음 |
| 79 | +### - Self-contained(자기 완결성?) 인것처럼 보이지만 그렇지 않음 |
| 80 | +```c++ |
| 81 | +filters.emplace_back( |
| 82 | + [=](int value) { return value % divisor == 0; } // 더 이상 dangling 문제가 발생하지 않음 |
| 83 | +); |
| 84 | +``` |
| 85 | +
|
| 86 | +#### 하지만 by value 로 pointer 가 capture 된다면? |
| 87 | +```c++ |
| 88 | +// Widget 클래스가 필터들의 컨테이너에 필터 함수를 추가하는 method 를 가지고 있다고 가정 |
| 89 | +class Widget { public: |
| 90 | + … // 생성자 등등 |
| 91 | + void addFilter() const; // filter를 filters 에 추가 |
| 92 | +
|
| 93 | +private: |
| 94 | + int divisor; // Widget의 필터에 사용됨 |
| 95 | + } |
| 96 | +``` |
| 97 | +#### divisor 를 lamda 에서 사용하고 싶다면 어떻게 해야 할까? |
| 98 | + |
| 99 | +#### capture 는 static 이 아닌 지역변수(함수의 paramter)에만 적용되어서 아래처럼 사용은 불가함 |
| 100 | +```c++ |
| 101 | +void Widget::addFilter() const |
| 102 | + { |
| 103 | + filters.emplace_back( |
| 104 | + [](int value) { return value % divisor == 0; } // 멤버 변수 Widget::divisor를 사용할 수 없음 |
| 105 | + ); |
| 106 | +``` |
| 107 | +```c++ |
| 108 | +void Widget::addFilter() const |
| 109 | +{ |
| 110 | + filters.emplace_back( |
| 111 | + [divisor](int value) |
| 112 | + { return value % divisor == 0; } // 지역 변수 divisor 가 없기 때문에 컴파일 되지 않음 |
| 113 | + ); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +```c++ |
| 118 | +void Widget::addFilter() const |
| 119 | +{ |
| 120 | + filters.emplace_back( |
| 121 | + [=](int value) { return value % divisor == 0; } |
| 122 | + ); |
| 123 | +} |
| 124 | +``` |
| 125 | +#### 위 코드는 안전할것 같아 보이지만 실제로는 addFilter 는 멤버 함수이기 때문에 this가 사용되어 아래와 동일한 코드가 됨 |
| 126 | +```c++ |
| 127 | +void Widget::addFilter() const |
| 128 | +{ |
| 129 | + auto currentObjectPtr = this; |
| 130 | + filters.emplace_back( |
| 131 | + [currentObjectPtr](int value) |
| 132 | + { return value % currentObjectPtr->divisor == 0; } |
| 133 | + ); |
| 134 | +} |
| 135 | +// 이 람다로 만들어진 closure의 유효성이 해당 closure가 만들어진 시점에 capture된 Widget 객체의 수명에 의해 제한됨(this 때문) |
| 136 | +``` |
| 137 | +#### 문제가 발생하는 예제 |
| 138 | +```c++ |
| 139 | +using FilterContainer = |
| 140 | + std::vector<std::function<bool(int)>>; |
| 141 | +FilterContainer filters; |
| 142 | + |
| 143 | +void doSomeWork() |
| 144 | +{ |
| 145 | + auto pw = |
| 146 | + std::make_unique<Widget>(); |
| 147 | + |
| 148 | + |
| 149 | + pw->addFilter(); // Widget::divisor를 사용하는 필터를 추가 |
| 150 | + … |
| 151 | + } // Widget 이 제거 되고 filters 에 dangling pointer 가 발생 |
| 152 | +``` |
| 153 | +
|
| 154 | +#### 멤버변수 divisor 를 사용하고 싶다면 아래와 같이 하면 된다. |
| 155 | +```c++ |
| 156 | +void Widget::addFilter() const { |
| 157 | + auto divisorCopy = divisor; // copy data member |
| 158 | +
|
| 159 | + filters.emplace_back( |
| 160 | + [divisorCopy](int value) // capture the copy |
| 161 | + { return value % divisorCopy == 0; } // use the copy |
| 162 | + ); |
| 163 | +``` |
| 164 | + |
| 165 | +#### C++ 14 에서는 멤버 변수를 capture 하는 편한 방법이 있다. |
| 166 | +```c++ |
| 167 | +void Widget::addFilter() const |
| 168 | +{ filters.emplace_back( // C++14: |
| 169 | + [divisor = divisor](int value) // copy divisor to closure |
| 170 | + { return value % divisor == 0; } // use the copy |
| 171 | + ); |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | + |
| 176 | +### Self-contained 가 아닌 예 |
| 177 | +람다는 static 으로 선언된 객체를 "사용" 할수는 있지만 "capture" 할수는 없다. |
| 178 | +```c++ |
| 179 | +// 아래예는 모든 내용을 값으로 "capture" 하고 람다 내에서 사용한다고 생각할 수 있지만 실제로는 전혀 그렇지 않다. |
| 180 | +void addDivisorFilter() |
| 181 | +{ |
| 182 | + static auto calc1 = computeSomeValue1(); // now static |
| 183 | + static auto calc2 = computeSomeValue2(); // now static |
| 184 | + |
| 185 | + static auto divisor = // now static |
| 186 | + computeDivisor(calc1, calc2); |
| 187 | + |
| 188 | + filters.emplace_back( |
| 189 | + [=](int value) // 아무것도 capture 되지 않음 |
| 190 | + { return value % divisor == 0; } // 그냥 위의 static divisor 를 가리킨다 |
| 191 | + ); |
| 192 | + ++divisor; // divisor 를 수정 |
| 193 | +} |
| 194 | +``` |
0 commit comments