[C++] R-Value Reference와 Move Semantic

언어/C++ 2019. 9. 3. 01:36
R-Value Reference(이하 오른값 참조)는 C++11부터 추가된 아주 복잡하면서도 멋진 녀석이다.

이게 대체 뭐하는 놈인고 하면, 아주 원론적인 부분까지 접해들어갈 필요가 있다.
아래 코드를 봐보자.

int num;
num = 33;

L: 여기서 num은 왼쪽에 있다. 그리고 이름이 있다. 그리고 표현식이 종료되어도 존재한다. 또한 메모리 셀이므로 주소연산(&)을 사용할 수 있다. 이것이 왼값이다.
R: 오른쪽에는 33이 있다. 이건 이름이 없다. 그냥 값이 num으로 전달될 뿐이고, 표현식이 끝나면 증발한다. 또한 상수이므로 주소연산을 사용할 수 없다. 이것이 오른값이다.

const string& s... 같은,
이제까지 우리가 사용했던 참조는 왼값참조였다.
기존에 존재하던 변수(왼값)를 참조했으니까.

하지만 이제 우리가 접근해볼 오른값 참조는 저런 이름없는 임시 값들마저 참조할 수 있다.
표기법은 아래와 같이, &대신에 &&를 사용한다.
int && ref = 333; //오른값 333을 참조

왼값도 std::move를 사용하면 오른값 참조에 묶을수 있다.
std::string s = "text";
std::string&& ref = std::move(s);
//이름이 move지만 진짜 이동하는건 아니고 &&참조로 캐스팅만 해줌

오른값 참조자도 기존의 왼값 참조자처럼 값에 접근해서 이래저래 쓸수 있다.

근데 이게 다라면 이걸 구분해서 쓸 이유가 전혀 없다. 그냥 왼값참조 써도 똑같이 되잖아?


그렇다. 이 오른값 참조로 값이 다른 변수에 전달되고, 그 변수의 타입에 해당하는 이동 생성자(혹은 대입자)가 있다면, 무브 시맨틱(Move Semantic)이라는 것이 발생한다.
말 그대로 이동(move)을 수행한다.

컴퓨터에서 무거운 영상 파일들을 옮겨본 적이 있을것이다.
파일이 너무 무겁다면 그 파일의 복사하고 붙여넣는(Ctrl+c,v) 속도도 엄청하게 느려진다.
하지만 파일을 잘라내고 붙일(Ctrl+x,v) 때는 별다른 시간이 소요되지 않고 뚝딱 완료된다. 이것이 파일이 한 군데만 존재할 것을 보장하는 데서 발생하는 일종의 이동이다.
앞서 언급한 무브시맨틱도 이와 같은 원리라 보면 된다.

예시를 한번 보자.
//가정1: s 안에 담긴 문자열은 크기가 크다.
std::string s = "...";
//이 시점에서 s는 더이상 필요가 없다.
//s2에서 옮겨서 s2로만 쓰면 된다.
std::string s2 = s;

이러면 s2의 복사생성자가 호출되면서 s의 내부값을 일일이 복사해서 가져간다.
기존 s의 크기가 클수록 소요되는 시간도 선형적으로 증가할 것이다.

하지만 이렇게 한다면
std::string s2 = std::move(s);
//s를 std::string&&으로 캐스팅
//s2의 이동생성자 호출

s의 길이와 상관없이 이 작업은 아주 작은 상수시간에 종료된다.
그리고 기존의 s는 텅 비게된다.

그리고 무브시맨틱은 연산중에 발생한 임시객체를 다룰때도 효용을 보인다.
std::string s = "hello";
std::string s2 = " world";
std::string s3 = s+s2;
옛날 방식대로 생각해보자. 위 코드의 세번째줄에서 s와 s2가 더해져서 둘이 이어진 임시객체 std::string("hello world")가 생성된다. 여기까진 좋다. 그럼
std::string s3 = std::string("hello world");
의 형태가 된다.
그럼 저 임시객체의 값을 복사해가기 위해  복사생성자가 호출되고 저 값들을 전부 복사해갈 것이다.
당장 저 길이라면 상관없지만, 임시객체가 저것보다 훨씬 크다면 이건 문제가 된다.

하지만 이제 우리에겐 무브 시맨틱이 있다.
s+s2의 결과물은 오른값이다.
그러므로 이동생성자가 호출돼서 s+s2의 결과값이 낭비없이 s3에게 쏙 넘어간다.


그리고 이런 성능의 향상 빼고도, 이 무브시맨틱은 하나의 장점이 또 있다.

무브시맨틱은 말 그대로 이동이다.
값이 아래에 있던 것이 위로 가면, 그 값은 위에만 존재한다. 아래에는 존재하지 않는다.
그러므로 이동의 동작은 의미론적으로 유일성(unique)을 보장한다. 복사와 다르게.

그래서 스마트포인터 std::unique_ptr 같은 경우는 이런 이동만을 행할 수 있다. 복사는 금지되어있다.
이 외에도 복사는 금지하고 이동만을 허가하는 클래스들이 꽤 있다.

auto p = std::make_unique<T>(...);
std::unique_ptr<T> p2 = p; //에러! 복사 불가
std::unique_ptr<T> p3 = std::move(p); //p->p3 이동. p는 null이 됨


아 그리고 오른값참조가 등장하면서 원래 4개였던 클래스의 기본 함수들이, 6개로 늘어났다.
클래스명이 T일 경우, 기본 함수들은 아래와 같다.

T()=default; //기본 생성자
~T() = default; //소멸자
T(const T&) = default; //복사생성자
T(T&&) = default; //이동생성자
T& operator=(const T&) = default; //복사대입자
T& operator=(T&&) = default; //이동대입자

이동생성자와 이동대입자는 구현할 필요가 없다. 복사생성자와 같은 얕은복사의 문제는 없기 때문에 그냥 =default만 해줘도 된다.(컴파일러에게 자동구현시키기)
그래도 만약 구현하고 싶다면, 아래처럼 하면 된다.
//가정: 타입 T에는 멤버변수로 a와 b를 가진다.
//가정: a와 b는 전부 이동이 가능하다.
T(T&& other) :
a(std::move(other.a)),
b(std::move(other.b)
{}
T& operator=(T&& other)
{
    this->a=std::move(other.a);
    this->b=std::move(other.b);
    return *this;
}

아무튼 오른값 참조는 굉장히 유용하다.
문제는 자잘한 규칙과 함정이 많고, 복잡하다는 것인데

가장 헷갈릴만한 것은, 오른값 참조자는 오른값이 아니라는 것이다. 오른값 참조자 자체는 이름도 있고, 주소연산도 가할 수 있으며, 표현식이 종료되어도 생존한다.
때문에 아래와 같은 경우는 이동이 발생하지 않는다.
std::string s = "...";
std::string&& s2 = std::move(s); //s->s2 이동
std::string s3 = s2; //s2->s3 이동 실패. 그냥 복사 생성자 호출

이동의 과정은 순수하게 오른값으로만 전달되어야 한다.
아래와 같이 고치면 이동이 수행된다.
std::string s = "...";
std::string&& s2 = std::move(s); //s->s2 이동
std::string s3 = std::move(s2); //s2->s3 이동

그리고 오른값 참조자는 const를 붙이면 안된다.
에러는 나지 않지만 이동이 실패한다.


더 자세한 사항들은 모던 이펙티브 C++를 참조하길 바란다.


'언어 > C++' 카테고리의 다른 글

[C++] Q: 왜 동적배열이 vector가 된건가요?  (0) 2019.09.03
[C++] C++17 optional  (0) 2018.12.30
[C++] C++17 variant  (0) 2018.12.30

설정

트랙백

댓글

[C++] Q: 왜 동적배열이 vector가 된건가요?

언어/C++ 2019. 9. 3. 01:27
https://stackoverflow.com/questions/581426/why-is-a-c-vector-called-a-vector


Q: 수학적 의미의 벡터는 대략 알긴 하는데요.
c++의 벡터에 대한 뜻은 알지 못해서 이렇게 질문을 올립니다.


A: vector의 수학적 정의는 집합 Sn의 멤버면서 특정 집합 S에 들어있는 값의 정렬된 시퀀스입니다.
이게 c++의 벡터가 저장하는거죠


A2: 이건 STL의 설계자인 Alex Stepanov가 built-in 배열과 구분하기 위해 vector라고 부른 데서 유래합니다.
근데 수학적 의미의 vector는 길이가 고정된 시퀀스죠... c++의 vector는 길이가 변하는 시퀀스고요.
알렉스도 자신의 실수를 인정했습니다.

게다가 C++0X(C++11)에서는 수학적 의미의 vector와 같은 기능을 하는 클래스를 'array'로 정의해버렸습니다.
엉망진창이네요.

알렉스의 교훈: 이름은 언제나 신중하게 결정하자

'언어 > C++' 카테고리의 다른 글

[C++] R-Value Reference와 Move Semantic  (0) 2019.09.03
[C++] C++17 optional  (0) 2018.12.30
[C++] C++17 variant  (0) 2018.12.30

설정

트랙백

댓글

[C++] C++17 optional

언어/C++ 2018. 12. 30. 22:29

C++17부터 옵셔널 라이브러리가 추가되었다.


이 옵셔널 타입은 원래 함수형 언어에서 나온 개념인데, 개념적으로는 모나드라고 부른다.


예외처리 방법 중 하나며, 안전성이 높아서 명령형 언어들에서도 채용을 많이 하는 편이다.

C#의 Nullable이 그렇고, Swift의 옵셔널, 코틀린의 ??, 러스트의 Result 등이 있다.


단어 자체의 뜻을 보면 '선택적인'이라는 뜻인데. 값이 선택적으로 있을수도 없을수도 있다는 뜻이다.


좀 다르긴 하지만 포인터 타입도 이런 면에서 옵셔널의 특성을 가진다고 할 수도 있다. 

널 포인터를 가지면 값이 없는 것이고 뭐라도 갖고있으면 주소가 있는거니까 값이 있는 것이다.


옵셔널은 이러한 특성을 포인터뿐만 아니라 모든 타입에 적용시킬 수가 있다.

게다가 값이 들어있는지 아닌지 명시적으로 확인을 하게 해서 포인터에 비하면 아주 안전하다.

그대신 귀찮은 면이 있긴 하지만 어느정도 감수할만한 부분이라 생각된다.



옵셔널로 선언된 객체는 그 자체로는 사용할 수 없다.

값이 들어있는 상태인지 아닌지 모르기 때문이다.



옵셔널의 값을 사용하려면 value 메서드를 쓰면 된다. 

다만 값이 없을 때 쓰면 예외가 뜨니까 has_value나 operator bool로 값 여부를 체크하고 사용해야 한다.

"John"이라는 값이 멀쩡히 들어있으니 아주 잘 수행된다. 


값이 없으니 "??"가 나온다.


포인터에서 빈 주소를 표현하는 용도로 nullptr가 예약된것처럼

빈 옵셔널을 표현하는 객체로 nullopt가 있다.


근데 값을 사용할때마다 조건문을 달아주는건 꽤 귀찮은 작업이다.

위에서 했던건 value_or 메서드 하나로 퉁칠 수 있다.

이건 값이 들어있으면 값 그냥 반환하고, 값이 없으면 인자로 받은걸 반환한다.


'언어 > C++' 카테고리의 다른 글

[C++] R-Value Reference와 Move Semantic  (0) 2019.09.03
[C++] Q: 왜 동적배열이 vector가 된건가요?  (0) 2019.09.03
[C++] C++17 variant  (0) 2018.12.30

설정

트랙백

댓글

[C++] C++17 variant

언어/C++ 2018. 12. 30. 22:21

variant는 진화된 공용체다.


기존의 union은 문제가 많았다.


예를 들어 기존의 union을 쓸 때, 

double 버전으로 값을 넣고 int 버전으로 사용을 한다 해도 아무런 문제 없이 작동이 된다.


하지만 출력값을 보면 알겠지만 값이 정상적이지 않다.


C의 공용체는 아무런 타입 검사를 수행하지 않는다. 이 때문에 C의 공용체는 타입-safe하지 못하다고 한다.


아무튼 저런 맛이 간 출력값이 나온 이유는 아무런 조치 없이 메모리에 존재하는 비트값을 그대로 읽어왔기 때문이다. 정수 타입과 실수 타입의 메모리 구조는 완전히 다르다. 게다가 엔디언과 바이트 크기까지 고려하면 같은 계열의 타입이라도 괴리가 발생할 수가 있다.


물론 이런걸 이용해서 편법적으로 사용할 수도 있지만, 

실제로 이런 식의 접근은, 99% 사용자의 실수에서 발생한다. 그리고 실수는 결함으로 이어진다.

union은 이처럼 안전성에 문제가 많다.


게다가 기존의 union은 생성자/소멸자/가상메서드가 없는 이른바 POD 타입만을 멤버로 둘 수가 있다는 심각한 기능적 결함이 있었다.

다시말해 C++스타일의 타입은 제대로 담아둘수 없었다는 것이다.



C++17에서는 이런 문제들을 해결한 variant라는 멋진 녀석을 내놓았다.


variant는 타입-safe한 공용체 템플릿이다.

당연히 C++ 스타일의 클래스도 멤버로 둘 수 있다.


기존의 공용체와 다른 특징으로, 이녀석은 반드시 초기화를 해야만 접근 타입을 바꿀 수가 있다.

해당 타입으로의 초기화를 수행하지 않고 접근을 한다면 예외가 발생한다.

아마 이런 방식을 통해서 타입체킹도 엄격하게 하고 생성자/소멸자도 써먹을 수 있게 한 것 같다.



일단 아래 코드는 아주 정상적으로 작동하는 코드다.

int 타입으로 값 대입 한번 해주고, 접근해서 출력하고.

double로 초기화해주고, 접근해서 출력한다.




하지만 초기화를 수행하지 않으면 이렇게 예외가 나온다.

이렇게



그리고 애초에 없는 타입이나 인덱스는 당연히 컴파일부터가 안 된다.



암튼 그렇다.


자세한 건 레퍼런스를 참조

https://en.cppreference.com/w/cpp/utility/variant

'언어 > C++' 카테고리의 다른 글

[C++] R-Value Reference와 Move Semantic  (0) 2019.09.03
[C++] Q: 왜 동적배열이 vector가 된건가요?  (0) 2019.09.03
[C++] C++17 optional  (0) 2018.12.30

설정

트랙백

댓글

[C] 문자열 유사클래스 구현

언어/C 2018. 12. 30. 21:20

oop 흉내.


C의 한계로, 메서드마다 첫번째 인자로 객체 자신의 주소를 보내줘야 함.


https://github.com/myyrakle/c-oop-string/blob/master/main.c


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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
 
//기본 초기화 목록입니다.
//기본 빈 값을 넣어주고,
//유사 멤버함수들을 전부 넣어줍니다.
#define NullString \
{ \
NULL0, \
_string_set, \
_string_setn, \
_string_gets, \
_string_get_length, \
_string_copy_from, \
_string_clone, \
_string_move, \
_string_append, \
_string_append_raw, \
_string_swap, \
_string_clear, \
_string_is_empty, \
_string_compare, \
_string_compare_raw, \
_string_is_same, \
_string_is_same_raw, \
_string_format, \
_string_append_format, \
_string_at, \
_string_ptr_at, \
_string_findc, \
_string_finds, \
_string_finds_raw, \
}
 
 
//C스타일 문자열 유사클래스입니다.
struct String
{
    char* data;
    size_t length;
 
    //문자열을 할당합니다.
    void (*set)(struct String*const char*);
    void (*setn)(struct String*const char*, size_t);
 
    //문자열을 가져옵니다.
    const char* (*gets)(const struct String*);
 
    //문자열의 길이를 가져옵니다.
    size_t (*get_length)(const struct String*);
 
    //깊은복사를 행합니다.
    void (*copy_from)(struct String*const struct String*);
 
    //깊게 복제된 객체를 반환합니다.
    struct String (*clone)(const struct String*);
 
    //이동합니다. 자신은 clear가 됩니다.
    struct String (*move)(struct String*);
 
    //이어붙입니다.
    void (*append)(struct String*const struct String*); //String 버전입니다.
    void (*append_raw)(struct String*const char*); //생 문자열 버전입니다.
 
    //교환합니다.
    void (*swap)(struct String*struct String*);
 
    //지워버립니다.
    void (*clear)(struct String*);
 
    //비어있는지 여부를 확인합니다.
    int (*is_empty)(const struct String*);
 
    //비교해서 같으면 0... 뭐 그런거요
    int (*compare)(const struct String*const struct String*); //String 버전입니다.
    int (*compare_raw)(const struct String*const char*); //생 문자열 버전입니다.
 
    //같으면 1. 다르면 0.
    int (*is_same)(const struct String*const struct String*); //String 버전입니다.
    int (*is_same_raw)(const struct String*const char*); //생 문자열 버전입니다.
 
    //포맷팅합니다.
    void (*format)(struct String*const char*, ...); //포맷팅해서 그대로 대입합니다.
    void (*append_format)(struct String*const char*, ...); //포맷팅한 문자열을 이어붙입니다.
 
    //인덱싱합니다. 범위를 벗어나면 EOF를 줘요.
    int (*at)(const struct String*, size_t); //그냥 그 문자를 반환합니다.
    int* (*ptr_at)(struct String*, size_t); //그 문자의 주소를 반환합니다.
 
    //문자나 문자열을 탐색해서 인덱스를 줍니다.
    //못찾으면 EOF를 반환합니다.
    int (*findc)(const struct String*char c); //문자를 찾습니다.
    int (*finds)(const struct String*const struct String*); //String 문자열을 찾습니다.
    int (*finds_raw)(const struct String*const char*); //생 문자열을 받아서 찾습니다.
};
typedef struct String String;
typedef String string;
 
 
/*빌더 함수 선언*/
String make_string(const char*);
String make_nullstring();
 
/*String 유사메서드*/
void _string_set(String*const char*);
void _string_setn(String*const char*, size_t);
const char* _string_gets(const String*);
size_t _string_get_length(const String*);
void _string_copy_from(String* self, const String* other);
String _string_clone(const String* self);
String _string_move(String* self);
void _string_append(String* self, const String* other);
void _string_append_raw(String* self, const char* other);
void _string_swap(String* self, String* other);
void _string_clear(String* self);
int _string_is_empty(const String* self);
int _string_compare(const String* self, const String* other);
int _string_compare_raw(const String* self, const char* other);
int _string_is_same(const String* self, const String* other);
int _string_is_same_raw(const String* self, const char* other);
void _string_format(String* self, const char* format,...);
void _string_append_format(String* self, const char* format, ...);
int _string_at(const String* self, size_t index);
int _string_ptr_at(String* self, size_t index);
int _string_findc(const String* self, char c);
int _string_finds(const String* self, const String* key);
int _string_finds_raw(const String* self, const char* key);
 
 
 
/*유사메서드 정의*/
void _string_set(String* self, const char* s)
{
    self->length = strlen(s);
 
    free(self->data);
    self->data = malloc(sizeof(char)*self->length +1);
 
    strncpy(self->data, s, self->length+1);
}
 
void _string_setn(String* self, const char* s, size_t len)
{
    self->length = len;
 
    free(self->data);
    self->data = malloc(sizeof(char)*self->length +1);
 
    strncpy(self->data, s, self->length +1);
}
 
const char* _string_gets(const String* self)
{
    return self->data;
}
 
size_t _string_get_length(const String* self)
{
    return self->length;
}
 
void _string_copy_from(String* self, const String* other)
{
    self->length = other->length;
 
    free(self->data);
    self->data = malloc(sizeof(char)*self->length +1);
 
    strncpy(self->data, other->data, self->length +1);
}
 
String _string_clone(const String* self)
{
    String temp = make_nullstring();
    temp.setn(&temp, self->data, self->length);
    return temp;
}
 
String _string_move(String* self)
{
    String temp = make_nullstring();
    temp.data = self->data;
    temp.length = self->length;
 
    self->data=NULL;
    self->length=0;
 
    return temp;
}
 
void _string_append(String* self, const String* other)
{
    self->length+=other->length;
    realloc(self->data, self->length+1);
    strncat(self->data, other->data, self->length+1);
}
 
void _string_append_raw(String* self, const char* other)
{
    self->length+=strlen(other);
    realloc(self->data, self->length);
    strncat(self->data, other, self->length+1);
}
 
void _string_swap(String* self, String* other)
{
    char* t = self->data;
    size_t t2 = self->length;
 
    self->data = other->data;
    self->length = other->length;
 
    other->data = t;
    other->length = t2;
}
 
void _string_clear(String* self)
{
    self->length=0;
    free(self->data);
    self->data=NULL;
}
 
int _string_is_empty(const String* self)
{
    return self->data==NULL;
}
 
int _string_compare(const String* self, const String* other)
{
    return strcmp(self->data, other->data);
}
int _string_compare_raw(const String* self, const char* other)
{
    return strcmp(self->data, other);
}
 
int _string_is_same(const String* self, const String* other)
{
    return strcmp(self->data, other->data)==0;
}
int _string_is_same_raw(const String* self, const char* other)
{
    return strcmp(self->data, other)==0;
}
 
 
enum
{
    STRING_BUFFER_SIZE = 200 //포맷팅 함수에서 사용할 버퍼 크기. 늘려도 됨
};
 
void _string_format(String* self, const char* format,...)
{
    char buffer[STRING_BUFFER_SIZE];
 
    va_list args;
    va_start(args, format);
 
    vsprintf(buffer, format, args);
 
    self->set(&self, buffer);
 
    va_end(args);
}
 
void _string_append_format(String* self, const char* format, ...)
{
    char buffer[STRING_BUFFER_SIZE];
 
    va_list args;
    va_start(args, format);
 
    vsprintf(buffer, format, args);
 
    self->append_raw(&self, buffer);
 
    va_end(args);
}
 
int _string_at(const String* self, size_t index)
{
    if(index<0 || index>=self->length)
        return EOF;
    else
        return self->data[index];
}
 
int _string_ptr_at(String* self, size_t index)
{
    if(index<0 || index>=self->length)
        return EOF;
    else
        return &(self->data[index]);
}
 
int _string_findc(const String* self, char c)
{
    for(int i=0; i<self->length; ++i)
    {
        if(self->data[i]==c)
            return i;
    }
 
    return EOF;
}
 
int _string_finds(const String* self, const String* key)
{
    const char* finded = strstr(self->data, key->data);
    if(finded==NULL)
        return EOF;
    else
        return (int)finded - (int)(self->data);
}
 
int _string_finds_raw(const String* self, const char* key)
{
    const char* finded = strstr(self->data, key);
    if(finded==NULL)
        return EOF;
    else
        return (int)finded - (int)(self->data);
}
 
 
 
//String 유사클래스 유사객체를 생성해서 반환합니다.
String make_string(const char* s)
{
    String temp = NullString;
    temp.set(&temp, s);
    return temp;
}
 
//빈 유사객체를 생성해서 반환합니다.
String make_nullstring()
{
    String temp = NullString;
    return temp;
}
 
 
const int main(void)
{
    string s = make_string("hello");
 
    printf(s.data);
 
    return 0;
}
cs


설정

트랙백

댓글

[C] Q: 스택의 사이즈는 왜이렇게 작나요?

언어/C 2018. 12. 30. 21:10

Q: When you allocate memory on the heap, the only limit is free RAM (or virtual memory). 
힙에 메모리를 할당할 때는 램이나 가상메모리의 크기 내에서는 자유롭게 사이즈를 줄 수 있죠.

It makes Gb of memory.
그래서 보통 기가바이트 단위도 쓸 수가 있어요.


So why is stack size so limited (around 1 Mb) ? 
그런데 스택은 왜이렇게 작은거죠? 보통 1메가정도밖에 안되더라고요.

What technical reason prevents you to create really big objects on the stack ?
기술적인 이유 때문에 스택에 큰 객체를 두지 못하는건가요?

...



A: My intuition is the following. 
내 생각엔 이래요.

The stack is not as easy to manage as the heap. 
스택은 힙처럼 관리하기가 쉽지 않아요.

The stack need to be stored in continuous memory locations. 
스택은 연속적인 메모리 위치에 저장될 필요가 있거든요.

This means that you cannot randomly allocate the stack as needed, but you need to at least reserve virtual addresses for that purpose. 
스택에는 필요에 따라 랜덤으로 할당을 할수 없단거죠.
하지만 최소한 가상 주소는 예약을 해놔야 합니다.

The larger the size of the reserved virtual address space, the fewer threads you can create.
예약된 가상 주소 공간이 커질수록 만들수 있는 스레드 수는 적어져요.


For example, a 32-bit application generally has a virtual address space of 2GB. 
예를 들어 32비트 앱은 보통 2기가의 가상주소 공간을 가집니다.

This means that if the stack size is 2MB (as default in pthreads), then you can create a maximum of 1024 threads. 
그러니까 스택의 크기가 2메가면 최대 1024개의 스레드를 생성할 수가 있는거죠.

This can be small for applications such as web servers. 
웹서버 같은 앱들은 이게 적어도 되는데요.

Increasing the stack size to, say, 100MB (i.e., you reserve 100MB, but do not necessarily allocated 100MB to the stack immediately), would limit the number of threads to about 20, which can be limiting even for simple GUI applications.
스택의 크기를 100으로 늘리면(100메가를 예약해도 바로 100메가를 할당하진 않음), 스레드 수를 20개 정도로 제한할 수 있죠. 이건 간단한 gui 앱에서도 제한이 될수 있어요.


A interesting question is, why do we still have this limit on 64-bit platforms. 
흥미로운 문제는, 64비트 플랫폼에서도 여전히 이 제한이 존재한다는 거에요.

I do not know the answer, but I assume that people are already used to some "stack best practices": 
이 문제에 대해서는 답을 드리진 못하겠지만, 아마 사람들이 스택의 그 "무난한 관행"에 익숙해졌기 때문인것 같네요.

be careful to allocate huge objects on the heap and, if needed, manually increase the stack size. 
큰 객체는 힙에 놓고 필요할 때만 스택의 크기를 늘리는게 좋다는 겁니다.

Therefore, nobody found it useful to add "huge" stack support on 64-bit platforms.
그래서 64비트 플랫폼에서 어떤 환경도 기본으로 "큰" 스택을 지원하지 않는거에요.


설정

트랙백

댓글

[C] ()와 (void)의 차이

언어/C 2018. 12. 30. 21:09

C에서는 어떤 경우에도 우직하게, 위에서 아래로만 인식을 합니다.

그래서 위에서 아래에 있는 함수를 갖다 쓰려면 애가 찾질 못하죠.

그래서 전방선언-프로토타입이란걸 위에다 붙여줍니다. 보통 헤더에 분리해서 넣어주죠.

이렇게요.


자 근데. 선언부의 파라미터를 전부 지우면 어떨까요?
시그너처가 다르니 에러가 날까요?

한번 봅시다.

잘 됩니다. 

선언부에서의 빈 ()는 이건 뭐든 들어갈수 있어! 라는 뜻이기 때문이죠.

"이건 파라미터를 안 받아!"라는 의미를 부여하려면 void를 명시해줍니다.

이제 (void)라고 확실히 명시가 됐으니, 아래에 있는 정의부와 일치하지 않아서 에러가 납니다.



설정

트랙백

댓글

[C] Q: scanf에서 오류가 나요. in Visual Studio

언어/C 2018. 12. 30. 21:05

일단 이건 비주얼 스튜디오에서만 해당되는 문제입니다.


입력을 받는 간단한 프로그램을 하나 짜보도록 하죠.


1
2
3
4
5
6
7
8
9
10
11
12
#include "stdafx.h"
 
#include <iostream>
 
int main()
{
    char buffer[100];
 
    printf("암거나 입력하세요 : ");
    scanf("%s", buffer);
    printf("결과 %s", buffer);
}
cs



VS에서는 이렇게 문법적으로 아무런 문제도 없는 코드를 짜도, 문제가 발생할 수 있습니다.

초심자들이 한번씩은 걸려드는 함정이죠.


자 오류가 떴습니다. 근데 대체 뭐라고 하는걸까요?

scanf 함수는 안전하지 않으니까 scanf_s를 쓰라고 하는 겁니다.


근데 그건 니들 생각이고, 나는 그걸 쓰고싶지 않아요. 




1.define(안됐는데 알고보니 됨)

어쨌든 scanf를 쓰고싶으면 _CRT_SECURE_NO_WARNINGS라는 매크로를 정의하라고 하는데요. 그럼 이렇게 짜면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdafx.h"
 
#include <iostream>
#define _CRT_SECURE_NO_WARNINGS //잔소리좀 하지 말
 
int main()
{
    char buffer[100];
 
    printf("암거나 입력하세요 : ");
    scanf("%s", buffer);
    printf("결과 %s", buffer);
}
cs

컴파일을 해볼까요?



이런 빌어먹을. 그래도 안된다고 합니다. 얘들이 왜이러지? 예전에는 이러면 됐었는데 뭔가 바뀌었나봅니다.


수정) 알고보니 define을 맨 위에 둬야한다네요. 잘못 씀.



2.pragma

다른 매크로를 써보죠, pragma란 것도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
 
#include <iostream>
 
#pragma warning(disable:4996//좀 닥쳐봐
 
int main()
{
    char buffer[100];
 
    printf("암거나 입력하세요 : ");
    scanf("%s", buffer);
    printf("결과 %s", buffer);
}
cs

다시 컴파일


오! 이번에는 다행히 됩니다.



3.그냥 scanf_s를 쓸까?

근데 저놈들은 이게ㅔ 뭐라고 자꾸 쓰라고 강요를 하는 걸까요?

실제로 scanf에 취약점이 존재하기 때문입니다. 전달받은 변수보다 입력으로 들어오는 값이 더 크면 버퍼오버플로가 발생하는데, 이게 상당히 위험할 수 있거든요.

그래서 더 큰 값이 들어와도 잘라버릴 수 있도록 맨 마지막 인자로 변수의 크기를 넣어줍니다. '너무 커도 이 변수 크기까지만 넣어라!' 라는 뜻이죠. 


이렇게요.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdafx.h"
 
#include <iostream>
 
int main()
{
    char buffer[100];
 
    printf("암거나 입력하세요 : ");
    scanf_s("%s", buffer, sizeof(buffer)); //이제 됐냐?
    printf("결과 %s", buffer);
}
 
cs


잘 됩니다.


근데 사실 _s는 그렇게 좋은 기능만은 아닙니다. 이걸 이렇게 강력하게 지원하는 컴파일러가 사실상 이거밖에 없는데요... 

굳이 이걸 쓰지 않아도 취약점을 막을 수 있는 방법이 많기 때문입니다.. 마지막 인자 때문에 추가되는 오버헤드도 있고요.




4.프로젝트 설정

코드에 손을 대고 싶지 않다면, 좀더 근본적으로 문제를 차단할 수도 있습니다.

자꾸 _s 버전 쓰라고 강요하는걸 SDL 체크라고 하는데요. 이걸 꺼버리면 됩니다.


솔루션 탐색기 창에서 프로젝트 파일에 마우스 오른쪽 버튼을 누르고

속성 창으로 들어갑니다.


그리고 C/C++ -> 일반 -> SDL 검사에서 '아니요'를 선택해주시고 확인 누르시면


잘 됩니다.

설정

트랙백

댓글