[C++] 이름과 스코프

 C++에서는 많은 것들이 이름을 가질 수 있다. 변수는 모두 이름을 가지고, 객체는 변수를 통해서 이름을 가질 수 있다. 타입도 이름이 있고, 함수도 이름이 있다. 람다 함수는 익명 함수라는 번역 그대로 이름이 없지만, 변수에 할당하여 이름을 통해 접근할 수도 있다.  이 이름을 어디서부터 어디까지 사용할 수 있는가를 그 이름의 스코프라고 하는데, C++은 상황에 따라 다른 스코프를 사용한다. 이번 글에서는 C++의 이름이 가지는 스코프에 관해서 설명하도록 하겠다. block scope  block scope는 선언된 문장부터 선언된 block이 끝날 때까지 사용할 수 있다. block 밖에서 선언된 같은 이름이 있으면 이름이 선언되기 전까지는 block 밖에서 선언된 이름을 그대로 가지고, 선언된 문장부터 새로 선언한 의미를 가진다.  예를 들어 위와 같은 코드가 있을 때 4번째 줄까지 a는 2번째 줄에서 선언된 a이지만, 4번째 줄부터 block이 끝나는 10번째 줄까지는 4번째 줄에서 선언된 a이다. 또한, block scope를 가지는 이름은 같은 block 안에서 다른 의미를 가지기 위해 다시 사용될 수 없다. 즉, 위에서 5번째 줄에서 10번째 줄까지 사이에서는 a를 다른 의미로 사용할 수 없다.  스코프를 가지는 이름의 대표적인 예제로는 함수 안에서 선언된 변수인 지역 변수(local variable)가 있다. 이때 지역 변수와 automatic storage 에 저장되는 지역 객체를 혼동하면 안 된다. 스코프는 어디까지나 컴파일 시 그 이름을 어디까지 사용할 수 있는가 하는 것이지 객체의 라이프타임과는 상관없다. 지역 변수 중에서도 static 이나 thread_local 같은 storage를 지정해주는 specifier가 붙은 경우 이 객체들은 automatic storage에 저장되지 않는다.  지역 변수 외에도 typedef 나 using 으로 선언되는 type alias, 함수 안에서 선언되는 class와 enum 같은

[C++] object는 언제 생성돼서 언제 소멸되는가 - storage

 C++에서는 타입을 가지는 일정 크기의 값을 object라고 한다. object의 중요한 특징 중 하나는 lifetime. 즉, object가 언제 소멸하는가 하는 것이다. 이 object는 객체 지향 프로그래밍에서 말하는 객체와는 살짝 다른 개념이다. C++ 표준에서 object는 타입, alignment, lifetime 등의 특성을 가지는 일정 크기의 연속된 값을 의미한다. 이는 객체 지향 프로그래밍에서 말하는 객체와는 약간 다른 의미이지만 이 글에서는 그냥 객체라고 부르도록 하겠다.  C++에서 객체의 lifetime을 알기 위해서는 우선 객체가 어디에 생성되는지를 알아야 한다. 일반적으로 C++ 개발자에게 객체가 어디에 생성되는지를 물으면 다음과 같이 대답할 것이다.  로컬 객체는 stack에, new로 생성된 객체는 heap에 저장되는데 stack과 heap은 같은 메모리 영역을 공유하며 stack은 메모리가 감소하는 방향으로 커지고, heap은 메모리가 증가하는 방향으로 커진다. 또한, 전역 변수 중 initialize 되지 않은 것은 bss에 initialize 된 것은 data 섹션에 저장된다.  이 대답은 기술적으로 맞는 대답이다. 사실 매우 훌륭한 대답이다. 실질적으로 대부분의 머신에서 대부분의 컴파일러는 위와 같은 방식으로 코드를 컴파일한다. 하지만 다시 한번 생각해보자. stack과 heap이 같은 메모리를 나누어 쓰는 이유는 한정된 크기의 메모리에서 컴파일 타임에 알 수 없는 두 종류의 동적 메모리를 할당하기 때문이고, bss 영역과 data 영역이 나누어지는 이유는 바이너리 파일의 크기를 줄이기 위한 최적화 때문이다. 즉, stack, heap, data, bss 등의 메모리 영역 등은 구현에 관련된 것일 뿐이다. 그렇다면 C++ 표준 문서에서는 이에 대해서 어떻게 기술하고 있을까?  C++ 표준에서는 객체가 메모리에 생성된다고 하지 않고, 스토리지에 생성된다고 한다. 구현과 표준을 분리하기 위한 결정으로 구체적인 메모리 레이

[C++] glvalue와 prvalue

  지난번 글 에서 lvalue와 rvalue의 특징을 설명하면서 예고했듯이 이번에는 glvalue와 prvalue의 특징에 관해서 설명하도록 하겠다.  전에도 말했듯이 C++의 value category의 최하단에 있는 lvalue, xvalue, prvalue 중에서 lvalue와 xvalue가 glvalue에 포함된다. lvalue는 iM, xvalue는 im이고, prvalue는 Im이므로 glvalue와 prvalue를 나누는 기준은 i인지 아닌지. 즉, identity를 가지는지 여부이다. identity를 가진다는 것은 그 값이 expression을 넘어서까지 살아있다는 것이다. 그래서 identity를 가진다는 것은 그 값이 persist하다고 말하기도 한다. temporary object  prvalue는 persist 하지 않다. 이는 prvalue인 expression이 의미하는 object가 특정한 스토리지를 점유하지 않는다는 것이다. 만약 스토리지에 할당될 필요가 있으면 prvalue는 temporary object를 생성한다. 생성된 temporary object는 레퍼런스 변수에 할당되지 않으면, expression이 끝나고 소멸한다. incomplete type  prvalue는 구체화할 때 temporary object를 생성하므로, 컴파일 타임에 expression의 타입을 정확히 알아야 한다. 따라서 전방 선언만 돼있는 incomplete type인 prvalue는 있을 수 없다. 하지만 실행될 일 없는 decltype 안에 있는 expression의 경우 incomplete type인 prvalue가 있을 수 있다. polymorphic  incomplete type의 prvalue가 있을 수 없는 것과 비슷하게 prvalue는 polymorphic 할 수 없다. 다시 말해서 prvalue인 object의 dynamic type은 그 expression의 static type과 항상 같다. persist

[C++] lvalue와 rvalue

  지난번 글 에서 C++의 value category에 관하여 설명하였다. 요약하면 다른 값으로 move될 수 있는지와 identity를 가지는지에 따라서 lvalue, xvalue, prvalue로 나뉘고, lvalue와 xvalue를 합쳐서 glvalue, xvalue와 prvalue를 합쳐서 rvalue라고 부른다는 것이다.  앞에서도 말했듯이 C++의 value category를 나누는 기준은 move 될 수 있는지와 identity를 가지는지 여부이다. 이중 iM 은 lvalue라고 부르고, im 인 xvalue와 Im 인 prvalue를 합쳐 rvalue라고 부른다. identity를 가지지도 않고, move 될 수도 없는 IM 은 C++에 존재하지 않기 때문에 사실상 lvalue와 rvalue를 나누는 기준은 move 될 수 있는지 아닌지이다. 따라서 move 될 수 없으면 lvalue이고, move될 수 있으면 rvalue이다. 이번 글에서는 그래서 lvalue와 rvalue가 구분되는 특성을 설명할 것이고 다음 기회에 glvalue와 prvalue가 구분되는 특성을 설명하도록 하겠다. overloaded function  우선 lvalue와 rvalue의 가장 중요한 차이는 overload 된 함수가 있을 때 어떤 함수가 호출될지가 달라진다는 것이다. 위처럼 같은 타입의 lvalue reference를 받는 함수와 rvalue reference를 받는 함수가 overload돼있을 때, argument가 lvalue이면 lvalue reference를 받는 함수가 호출되고, rvalue이면 rvalue reference를 받는 함수가 호출된다. 따라서 위와 같이 사용했을 때, func(a) 는 a 가 lvalue이므로 0 이 리턴되지만, std::move(a) 는 xvalue이고 1 은 prvalue 이므로 func(std::move(a)) 와 func(1) 은 1 이 리턴된다. universal reference  rvalue

왜 c++은 복잡한 value category를 가지게 됐는가

 C++11이 나오기 전, C++의 value category는 lvalue와 rvalue만으로 이루어진 단순한 분류체계를 가지고 있었다. 하지만 C++11이 나오면서 xvalue, glvalue, prvalue가 추가되면서 복잡한 분류체계를 가지게 됐다. 이번 글에서는 C++11에서 변경된 value category에 대해서 알아보도록 하겠다. until c++03  C++11에서 새로 추가된 value들을 알기 전에 C++03 이전에도 있었던 lvalue와 rvalue가 뭔지부터 확실히 하고 넘어가야 한다. 흔히들 많이 실수하는 것이 lvalue는 assign operator(operator =)의 왼쪽에 올 수 있는 값이고, rvalue는 올 수 없는 값이라고 생각하는 것이다. C에서 lvalue라는 개념이 처음 나왔던 시절에는 assign operator를 기준으로 lvalue인지 아닌지를 구분하는 것이 맞았다. 하지만 C89에서 const 한정자가 추가되면서 더는 맞지 않다. const variable은 lvalue이지만 assign operator의 왼쪽에 올 수 없기 때문이다.  따라서 C89에서는 lvalue를 left value가 아닌 locator value. 즉, 실제 값이 아닌 값이 있는 주소를 지칭하는 locator value라고 정의했다. 즉, 변수는 locator이기 때문에 lvalue이고, 1, 2 같은 숫자 리터럴이나 'a', 'b' 같은 문자 리터럴, 혹은 함수의 실행 결괏값 같은 경우 특정한 주소를 지칭하는 locator가 아니므로 lvalue가 아니다. 이 중에서 const 한정자가 붙지 않은 경우를 modifiable lvalue라고 따로 분류하여, modifiable lvalue만 assign operator의 왼쪽에 올 수 있다. 후에 나온 C++98도 C89의 lvalue 정의를 따랐기 때문에 C++98에서의 lvalue도 locator 라고 보면 된다. 다만 C89와 다르게 레퍼런

round - 실수를 정수로 근사하기

  지난번 글 에서 round라는 것은 반올림이 아니고 근삿값을 구하는 방법이기에 다양한 방법이 있다고 설명했었다. 하지만 프로그래밍에서 round 함수는 대부분 실수를 정수로 만드는 round이다. 그래서 이번 글에서는 실수를 정수로 근사하는 방법들을 설명하도록 하겠다.  우선 실수를 정수로 만드는 근사법 중에서 가장 유명한 것은 ceil 과 floor 이다. 이는 각각 round up 과 round down 이라고 부르기도 하는데 이를 번역하여 한국어로는 올림과 내림이라고 부른다. 번역 그대로 ceil 은 근삿값을 구하는 자릿수에서 값을 올리고, floor 는 내린다. 따라서 ceil(2.5)는 3이 되고 floor(2.5)는 2가 된다. 또한, 이 둘은 양의 무한과 음의 무한을 향해 가는 모습이기 때문에 각각 round towards positive infinity 와 round towards negative infinity 라고 불리기도 한다.  이와 구현이 비슷한 것으로 truncate 가 있다. truncate 는 보통 버림이라고 번역되는데, 이는 근삿값을 구할 자릿수 아래의 값을 버린다. 따라서 truncate(2.5)는 2가 된다. 이는 floor 와 같아 보이지만 다른 구현이다. 예를 들어 floor(-2.5)의 경우 이는 음의 무한을 향해 가기 때문에 -3이 되지만, truncate(-2.5)는 자릿수를 버려 -2가 된다. 이는 0에 가까운 수를 고르는 것 같은 동작을 하므로 round towards zero 라고 부른다. 이와는 반대로 0에서부터 멀어지는 방향으로 움직이는 round away from zero 라고 부르는 방법도 있지만, truncate 에 비해 딱히 특색이 있지는 않아서 잘 쓰이지 않는다.  지금까지 설명한 ceil , floor , truncate , round away from zero 는 모두 큰 쪽이거나 작은 쪽이거나 한 방향으로 움직인다. 하지만 이보다 더 많이 사용되는 것은 가까운 정수( to near

round(1234.5)의 결과는 무엇일까

 혹시 1235라고 생각했는가? 그렇다면 아마 다음과 같은 과정을 거쳤을 것이다. round 는 주어진 실수를 정수로 반올림하는 것이다. 반올림은 가장 가까운 수를 만드는 것이다. 반올림할 자릿수가 0, 1, 2, 3, 4이면 내리고 5, 6, 7, 8, 9이면 올린다. 따라서 1234.5의 5는 올려야 하므로 답은 1235이다. 만약 이렇게 생각했다면 round 가 무엇인지 제대로 이해하지 못한 것이다.  round가 무엇인지 알기 위해 우선 round 의 정의부터 확실히 해보자. 흔히들 착각하는 것이 ceil 은 올림, floor 는 내림, round 는 반올림이라고 생각하는 것이다. 하지만 round 는 반올림이 아니다. Wikipedia에서 말하는 round 의 정의는 다음과 같다. Rounding a numerical value means replacing it by another value that is approximately equal but has a shorter, simpler, or more explicit representation.   round 란 것은 반올림이 아니라 근삿값을 구하는 것이다. 즉, 반올림을 구하는 것도 round 이지만, 올림과 내림도 round 고, 1234 5678 같은 복잡한 분수를 1 5 로 간단하게 표현하는 것도 round 이다.  또한, 반드시 일의 자리까지 round 할 필요도 없다. 물론 보통의 round 구현은 실수를 입력받아 정수 근삿값을 만든다. 하지만 반드시 이럴 필요는 없다. 경우에 따라서 십의 자릿수 까지 근사값을 만들 수도 있고, 백의 자릿수 까지 근사값을 만들 수도 있다. 즉, round ( 1234.5 ) 의 답은 1000이 될 수도 있고, 1200이 될 수도 있고, 50 단위로 근사값을 구한다면 1250이 될 수도 있다.  따라서 round 를 어떻게 구현했는지 사용하는 라이브러리의 스펙을 확인해봐야 한다.