[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 re

왜 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 nearest

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 를 어떻게 구현했는지 사용하는 라이브러리의 스펙을 확인해봐야 한다.

JavaScript와 IEEE 754

JavaScript는 표준에서 숫자 타입은 IEEE 754-2008, 64 bit format을 따른다 고 명시돼있다. 따라서 숫자 타입의 연산도 IEEE 754-2008을 따를 거로 생각했다. 하지만 ECMAScript 명세에서 NaN 이나 Infinity 를 포함한 연산에 대해 다른 결과를 내도록 정의한다. 그 대표적인 경우가 1 ∞ 과 -1 ∞ 이다. IEEE 754-2008는 지수를 계산하는 방법을 3가지 정의한다. 첫 번째는 pown 으로 지수가 정수인 경우에 대해 정의돼있고, 두 번째 pow 는 밑과 지수가 모든 실수인 경우 사용할 수 있도록 정의돼 있고, 마지막은 powr 으로 밑이 0 이상의 실수인 경우에 대해서 정의돼 있다. 우선 첫 번째인 pown 는 지수가 ∞ 가 되지 못하므로 관심 대상이 아니다. 다른 두 지수 함수인 pow 와 powr 은 1 ∞ 에 대해 다른 결과를 내도록 정의한다. pow 는 1 을 리턴하고, powr 는 invalid operator exception 을 발생하도록 정의했다. 사실 이는 양쪽 다 이상한 것은 아니다. lim x → ∞ 1 x 은 1 에 수렴하지만 1 ∞ 자체는 부정이기 때문에 1 인 경우와 invalid operator exception 이 발생하는 경우 양쪽 모두 말이 되기 때문이다. 그렇다면 IEEE 754-2008은 -1 ∞ 를 어떻게 정의할까? 이 경우 밑이 0 보다 작으므로 pow 만이 유효한 함수고, 이에 대해서 1 을 리턴하도록 정의했다. lim x → ∞ -1 x 는 발산하고, -1 ∞ 는 부정이기 때문에 이는 수학적으로 올바른 정의가 아니고, IEEE 754에서 어떻게 이렇게 정했는지는 모르겠다. 역사적인 이유이거나 이렇게 할 경우 구현이 편해지기 때문일 것인데 혹시 정확한 이유를 알고 있는 사람이 있으면 알려주기 바란다. ECMAScript에서는 1 ∞ 와 -1 ∞ 의 연산에 대해서 NaN 을 리턴하도록 정의했다. 이는 ECMAScript는 1997년 만들어졌다.

[c++] 생성자에서 예외가 발생하면 어떻게 될까

R.A.I.I. 를 사용하다 보면, 생성자에서 복잡한 일을 해야 할 경우가 종종 생긴다. 복잡한 일은 실패할 수도 있고, 그 경우 예외가 발생하기도 한다. 여기서 궁금한 점이 생긴다. 생성자에서 예외가 던져져도 아무 문제 없을까? 우선 걱정되는 것은 메모리 릭이다. 하지만 다행히도 메모리 릭은 발생하지 않는다. 스택에 생성된 변수는 스택 unwind를 통해 메모리가 해제되며, new operator를 통해 힙에 할당하다 예외가 발생한 경우에도 new operator가 알아서 메모리를 해제하고 nullptr 를 리턴해준다. 그다음으로 걱정되는 것은 멤버 변수의 소멸자가 잘못 불리지 않을까 하는 것이다. 하지만 이 역시 문제없다. 예외가 발생한 시점에서 멤버 변수는 초기화가 완료된 멤버 변수, 초기화 중이었던 멤버 변수, 초기화되지 않은 멤버 변수 3가지로 나눌 수 있다. 초기화가 완료된 멤버 변수는 말 그대로 생성자가 불렸고 정상적으로 메모리 할당을 완료한 멤버 변수다. 위 코드에서는 b 에 해당하는데, 이들은 예외가 발생하면 이 변수들은 정상적으로 소멸자가 불리며 리소스를 해제한다. 초기화 중이었던 변수는 위 코드에서 c 에 해당하는 멤버 변수다. 위 코드는 E 클래스의 생성자에서 C 인 변수 c 의 생성자가 예외를 발생시켰다. 이 경우 E 클래스 입장에서 c 는 초기화 중인 변수가 된다. 위 코드와 같이 멤버 변수의 초기화 중에서 예외가 발생하면 초기화 중인 변수가 1개 존재하지만, 생성자의 본체에서 예외가 발생하면 초기화 중인 변수는 존재하지 않는다. 마지막으로 초기화되지 않은 변수의 경우 생성자가 불리지 않았으니 소멸자가 불리지 않는다. 하지만 이 경우는 아직 생성자가 불리지 않았기 때문에 소멸자가 안 불리는 것이 맞다. 상속받는 경우는 어떨까? 부모 클래스에서 할당한 리소스는 정상적으로 해제될까? 당연히 이 경우도 아무 문제없다. 생성자에서 예외가 발생했을 때, 초기화가 완료 된 멤버 변수의 소멸자가 전부 불리고 나면, 부모 클

[Python] negative index를 사용하자

일반적으로 random access 가능한 자료 구조에서 뒤에서 k 번째 원소를 가져오는 방법은 다음과 같다. 자료구조의 크기를 가져온다. 크기를 기반으로 접근할 인덱스를 구한다. 원하는 자료를 가져온다. 이를 코드로 표현하면 다음과 같다. 파이썬에서는 이를 보다 간결하게 표현하기 위해 negative indexing을 지원한다. negative indexing은 말 그대로 음수를 이용해서 뒤에서부터 자료에 접근하는 것이다. negative indexing을 이용하면 위의 코드는 아래와 같이 표현된다. 파이썬에서는 뒤에서부터 접근할 때 negative indexing을 사용하는 것이 권장되지만 다른 언어로 코딩을 처음 배운 사람들은 파이썬에서도 길이를 가져와 인덱스를 계산하는 방식을 사용하는 경우가 종종 있다. 이런 사람들은 negative indexing이 단순한 syntactic sugar 라고 생각하는 것 같다. 하지만 negative indexing은 syntactic sugar가 아니다. negative indexing이 syntactic sugar라면 negative indexing을 쓴 것과 같은 바이트 코드가 생성돼야 한다. 하지만 둘은 서로 다른 바이트 코드가 생성된다. 위의 각 코드가 실제로 생성하는 바이트 코드는 다음과 같다. 첫 번째 코드는 global memory에서 len 함수를 찾아 호출하여 그 결과에 k 를 뺀 값을 인덱스로 사용하는 코드를 생성하고, 두 번째 코드는 k 를 음수로 만들어 바로 인덱스로 사용한다. 인덱싱을 통해 값을 가지고 오는 바이트 코드는 BINARY_SUBSCR 인데, 물론 인터프리터가 BINARY_SUBSCR 를 실행할 때 주어진 인덱스가 음수이면 내부적으로 리스트의 길이를 가져와 인덱스를 다시 계산 하는 과정을 거친다. 하지만 Global Interpreter Lock(a.k.a. GIL) 때문에 이 둘의 동작은 같지 않다. 파이썬의 GIL은 바이트 코드 단위의 atomi

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API