Skip to content

Commit 10b8b64

Browse files
authored
Update ITEM31.md
1 parent 528f047 commit 10b8b64

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed

ITEM31.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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

Comments
 (0)