몸살 감기로 아팠다. 그래서 기념으로 좀 쉬고, 일어나서 포스팅한다ㅠ
먼저, C++ 언어에서의 L-Value와 R-Value에 대해서 정리할 필요가 있다. 너무나도 당연하다고 생각해왔던 것들을 새삼스럽게 정리를 하려고 보니 좀 어려운 면이 없지 않아 있다.
L-Value와 R-Value란?
위의 그림에서와 같이 대입 연산자를 기준으로 왼쪽의 특성을 갖는 요소는 무조건 L Value라고 한다. 이 말은 오른쪽에 위치해도 L Value라고 할 수 있다는 소리다. 그 반대로 대입 연산자를 기준으로 오른쪽의 특성을 갖는 요소는 무조건 R Value라고 한다. 그러나 R Value는 대입 연산자 기준으로 왼쪽에 올 수 없다.
아마 특징과 예시를 보면 L Value와 R Value가 무엇인지 알 수 있을 것이다.
L-Value와 R-Value의 특징
먼저, 그 특징이다.
L value는 보통 변수를 대부분의 예로 들 수 있으며, R value는 임시 객체 혹은 주소 +, - 등의 연산 결과 등이 있다.
그런데 C++에서는 R Value에서 임시 객체라고 하는 것들, 요 부류에 해당하는 것들이 꼭 문제가 된다. 왜냐하면 다음과 같은 경우를 보자.
R-Value에서 문제가 되는 부분
대충 이러한 클래스가 있다.
이 클래스를 기반으로 여러 가지 생성과 동시에 초기화를 할 것이다. 그 생성과 동시에 초기화를 할 때, 두 가지 경우를 보자.
1번의 설명대로, 생성과 동시에 초기화하는 두 경우는 문법 상, 복사 생성자가 호출되는 것이 맞다. 왜냐하면 초기화할 값의 대상으로 객체가 넘어오기 때문이다. 위의 클래스에서처럼 인스턴스화 할 때 동적 할당을 해야하는 경우에는 깊은 복사가 필수이므로, 복사 생성자가 호출되어야 한다.
그런데 자세히 보면 효율적이지 못한 부분이 있다.
AAA b(AAA("ccc")); 이 문장의 경우에는 분명 괄호 안의 AAA("ccc")는 R value 로 임시 객체이며 곧 사라진다. 그렇기 때문에 b 내부의 str을 또 동적 할당하고~, 임시 객체의 str 내용을 또 b의 str에 복사하는 이런 작업을 하지 말고, 2번 처럼 그냥 임시 객체의 str의 주소 값을 b의 str 에 넣어버리고 임시 객체는 그냥 소멸해버리면 되는 것 아니냐?? 어차피 사라질 놈이니까!
만약에 깊은 복사가 이루어지는 것이 뭐 단순 str 같은 것들이 아니라 100MB 정도되는 동영상이라고 생각해보자. 객체 하나 복사하는데 100MB 짜리가 막 두개 였다가 하나였다가 메모리가 아주 들쑥날쑥 할 것이다.
그러나 실제로는 2번과 유사하긴 하지만 조금 다르게 복사 생략(copy elision)이라는 현상이 발생한다.
또한 이런 경우도 존재한다.
위와 같은 경우도 결국 s 혹은 z 객체는 곧 사라질 객체이기 때문에 새로 생성되는 임시 객체에 값을 전달할 때, 복사 생성자가 호출된다.
위 경우들 모두 어차피 사라질 객체들이므로, 객체 내의 깊은 복사가 필요한 부분은 그냥 사라질 객체 내의 str 변수의 메모리 주소만 보존하여 가지고 와서 갱신해주고, 객체는 소멸 시켜버리면 된다.
그래서 나온 개념이 r-value 레퍼런스와 이동 생성자이다.
r-value 레퍼런스, 이동 생성자
205라인은 const & 참조자를 이용해 예외적으로 R-value를 잡을 수 있었다. 그러나 [11]부터는 211 라인에서 처럼 r - va lue를 잡을 수 있는 r value 레퍼런스가 등장했는데, 이를 이용하여 r-value가 해당 라인이 종료된 후에도 소멸되지 않고 잡아둘 수 있을 뿐만 아니라, 값의 변경까지 허용한다!
이를 이용하여 만들어진 것이 이동 생성자이다!
이동 생성자와 복사 생성자를 함께 보도록 하자.
위의 이동 생성자로 인해 다음과 같이 복사 생성자의 호출을 막고, 메모리가 새로 할당되고, 복사하고 이런 과정을 막았다. 이는 매우 효율적이다!
결론적으로 이동 생성자는 다음과 같이 보면 된다.
C++ 11에서는 바로 이러한 이동 생성자라든가, R value 레퍼런스 등의 문법 덕분에 자원 문제가 해결되었다. 아래의 내용을 기록하려고 위를 기록한 것이다 결국..
STL 컨테이너 등을 사용할 때, 위처럼 대입하는데에만 복사 생성자가 2번 넘게 호출되며 메모리가 들쑥날쑥할텐데 이동 생성자라든가, 복사 생략 등으로 인해서 자원 효율이 좋아진다는 점이다.
RVO & NRVO
RVO는 Retrun Value Optimization 의 약자이고, NRVO는 Named Return Value Optimization의 약자이다.
뭔가 리턴 값을 최적화 한다는 소리인 것 같은데 다음의 예제를 보자.
위의 두 경우의 함수가 있다. 앞에는 컴파일러에 의해 NRVO 가 적용되는 함수이고, 뒤에는 컴파일러에 의해 RVO가 적용되는 함수이다.
이를 호출할 경우, 두 경우 반환하는 형태가 모두 참조형으로의 반환이 아니기 때문에 복사 생성자가 호출된다!
그러나 실제로는 그렇지 않다.
그렇다고 해서 이동 생성자가 호출된 것도 아니고, 실제로 이동 생성자가 호출되지도 않는다
RVO, NRVO는 반환되는 값에 대해서 최적화하여 복사 생성자가 호출이 되지 않게 하는 것이다. 이동 생성자나 복사 생성자보다 더 우선순위 있게 실행이 된다. 당연하게도 최적화기 때문에 우선순위가 높은 것이다.
그러면 위에서 이동 생성자가 호출된 NRVO 함수 호출은 무엇이었을까?
위의 예시처럼 NRVO가 실행이 안 되는 경우에는 이동 생성자가 호출되는 것이다.
근데 이게 컴파일러마다 조금씩 다르다 VS 2019 와 g++ 컴파일러에서는 다르게 표현되는 경우도 있고 한 것 같다. 하 이게 정말 별거 아닌 개념인 것 같은데 컴파일러마다 다르고, 이미 올라와있는 11 내용이랑 17, 20에서 바뀐 표준안이랑 살짝 충돌되는 경우들도 있어서 알아보느라 너무 힘들었다 정말.
아래는 사용한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
#include <iostream>
#include <vector>
#include <cstring>
#pragma warning(disable:4996)
class AAA
{
private:
char* data;
public:
AAA(const char* data)
{
this->data = new char[1000000];
std::strcpy(this->data, data);
std::cout << "Call AAA(char * data)![data:" << this->data << "] / this : [" << this << ']' << std::endl;
}
AAA(const AAA& ref)
{
this->data = new char[1000000];
std::strcpy(this->data, ref.data);
std::cout << "Call AAA(const AAA& ref)![data:" << this->data << "] / this : [" << this << ']' << std::endl;
}
AAA(AAA&& ref) : data(ref.data)
{
ref.data = nullptr;
std::cout << "Call AAA(AAA&& ref)![data:" << this->data << "] / this : [" << this << ']' << std::endl;
}
~AAA()
{
if (this->data != nullptr)
{
std::cout << "Call ~AAA()![data:" << this->data << "] / this : [" << this << ']' << std::endl;
delete[]this->data;
}
else
{
std::cout << "Call ~AAA()! / this : [" << this << ']' << std::endl;
}
}
void ShowData(void)
{
if (this->data == nullptr)
std::cout << "data : " << std::endl;
else
std::cout << "data : " << this->data << std::endl;
}
};
AAA FuncNrvo(void)
{
AAA s("ssssssssssssssssssssssssssssss");
return s;
}
AAA FuncNrvoS(int a)
{
AAA s("ssssssssssssssssssssssssssssss");
AAA z("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
if (a == 100)
{
return s;
}
else
{
return z;
}
}
AAA FuncRvo(void)
{
return AAA("ssssssssssssssssssssssssssssss");
}
AAA FuncRvoS(int a)
{
if (a == 100)
{
return AAA("ssssssssssssssssssssssssssssss");
}
else
{
return AAA("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
}
}
int main(void)
{
std::cout << "1. 일반 생성자 호출" << std::endl;
{
AAA a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
AAA b("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "2. 복사 생성자 호출" << std::endl;
{
AAA a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
AAA b("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
AAA c = a;
AAA d(b);
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "3. 임시 변수 r-value의 생성 - 라인이 끝나면 소멸된다." << std::endl;
{
AAA("cccccccccccccccccccccccccccccc");
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "4. l value 에 r value 대입 - 생성과 동시에 초기화 및 라인이 끝났음에도 소멸되지 않음." << std::endl;
{
AAA e(AAA("dddddddddddddddddddddddddddddd"));
std::cout << "&e : " << &e << std::endl;
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "5. r-value를 const & 를 통해서 가리킴 - 라인이 끝났음에도 소멸되지 않음." << std::endl;
{
const AAA& f(AAA("eeeeeeeeeeeeeeeeeeeeeeeeeeeeee"));
std::cout << "&f : " << &f << std::endl;
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "6. FuncNrvo() 함수의 AAA 반환으로 생성과 동시에 초기화." << std::endl;
{
AAA g(FuncNrvo());
std::cout << "&g : " << &g << std::endl;
g.ShowData();
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "7. FuncNrvoS() 함수의 AAA 반환으로 생성과 동시에 초기화." << std::endl;
{
AAA h(FuncNrvoS(11));
std::cout << "&h : " << &h << std::endl;
h.ShowData();
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "8. l-value를 r-value화 하여 생성과 동시에 초기화." << std::endl;
{
AAA a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
AAA i(std::move(a));
std::cout << "&i : " << &i << std::endl;
i.ShowData();
a.ShowData();
FuncNrvo();
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "10. 그냥 FuncNrvoS()만 호출" << std::endl;
{
FuncNrvoS(19);
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "11. 그냥 FuncRvo()만 호출" << std::endl;
{
FuncRvo();
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "12. 그냥 FuncRvoS()만 호출" << std::endl;
{
FuncRvoS(19);
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "13.FuncRvo()을 받으며 호출" << std::endl;
{
AAA j(FuncRvo());
std::cout << "&j : " << &j << std::endl;
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
std::cout << "14. FuncRvoS()을 받으며 호출" << std::endl;
{
AAA k(FuncRvoS(19));
std::cout << "&k : " << &k << std::endl;
std::cout << "라인이 넘어감" << std::endl;
}
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
// 복사 생성자를 호출하지 않으며, AAA(100)의 임시객체는 즉시 소멸하지도 않는다.
// -> e 자체를 AAA(100)으로 만들어진 객체로 취급
std::cout << "--- --- --- --- --- --- --- --- --- --- --- --- ---" << std::endl;
const int& lref = 100;
//lref = 101;
std::cout << lref << std::endl;
std::cout << "--------------------------" << std::endl;
int&& rref = 100;
rref = 101;
std::cout << rref << std::endl;
return 0;
}
|
cs |
'프로그래밍응용 > Modern & STL' 카테고리의 다른 글
함수 포인터를 대체하는 std::function (0) | 2021.02.08 |
---|---|
Lambda Expression (0) | 2021.01.27 |
자료형 추론 auto / 범위 기반 for (0) | 2021.01.21 |
Iterator 사용 방법 예시 (0) | 2021.01.05 |
Priority Queue (0) | 2021.01.04 |