레이블이 lvalue인 게시물을 표시합니다. 모든 게시물 표시
레이블이 lvalue인 게시물을 표시합니다. 모든 게시물 표시

2015년 9월 14일 월요일

[C++11] move constructor

Move constructor
: 객체 내부의 resource 를 소유권 이전 시키는 생성자로, 이후 자기 자신은 xvalue 가 된다.

먼저 간단한 예제부터 살펴보자.

main() 함수에서 Cat c2 = c1; 코드는 왜 runtime error 가 발생하게 만들까?
Cat c2 = c1; 코드에서는 복사 생성자가 호출된다.
복사 생성자를 별도로 만들지 않았기 때문에 당연히 얕은 복사가 진행되는데,
main() 함수 종료시에 c2가 stack 에서 소멸되는데 이때 얕은 복사를 진행했기 때문에
name 포인터가 가리키는 메모리(c1의 name 도 가리키고 있는)가 해제된다.
이후 c1의 소멸자가 호출되면서 이미 해제된 메모리 주소를 다시 해제하려고 하면 runtime error 가 발생하게 된다.

또한 단순한 값 복사가 아니라 객체 내부에 포인터가 있기 때문에
깊은 복사가 필요하고 c3 = c2; 코드는 대입 연산자를 재정의하여 깊은 복사가 필요로하게 한다.


복사 생성자와 대입 연산자를 재정의하여 깊은 복사를 구현해보자.

C++98/03 에서는 Rule of 3 라고 하여,
생성자에서 자원을 할당하면, 3개의 함수(복사 생성자, 대입연산자, 소멸자) 를 추가로 반드시 제공해야 하게끔 했다.

대입연산자 재정의시에 주의할 점은 자기 자신과의 비교를 해야된다는 것이다.


하지만 복사 생성자를 구현할 때 반드시 깊은 복사가 이뤄져야만 할까?

깊은 복사가 아닌 소유권을 이전하는 복사 생성자를 구현했다.
이런 생성자가 필요한 이유는 깊은 복사를 하기보단 얕은 복사를 이용하는 것이 효율적인 algorithm 이 있기 때문이다.
예를 들어, swap 함수 같은 경우이다. 또 다른 예는 Socket, I/O stream, file descriptors 에 쓰일 수 있다.
굳이 깊은 복사를 진행하여 메모리를 새로 할당, 해제하기 보다 포인터가 가리키는 주소만 변경하는 것이 효율적이다.

그래서 c++11에서는 결국엔 깊은 복사 생성자, 소유권을 이전하는 복사 생성자 둘 다 만들어주게 된다.

먼저 새롭게 정의된 Move 생성자를 살펴보자.
Cat(Cat&& c); 생성자는 rvalue reference 를 인자로 받아 소유권을 이전하는 방식으로 동작한다.

하지만 mySwap() 내부 동작이 과연 우리가 원하는 동작인가?
tmp, a, b 는 모두 lvalue 로 대입 연산자가 동작하여 깊은 복사가 이뤄진다.
Move 생성자를 호출하지 않아 성능향상을 가져올 수 없다.

그렇다면 tmp, a, b 를 rvalue 로 casting 하는 것은 어떨가?
mySwap 함수를 다음과 같이 변경해보자.

static_cast 로 casting 함으로써 Move 생성자를 호출할 수 있도록 변경되었다.
하지만, 아직은 불편하다.
매번 이렇게 casting 하기 보다 좀 더 쉬운 방법을 생각해보자.



lvalue reference 를 인자로 받아 lvalue reference 로 casting하는 move 함수를 구현했다.
이젠 casting 을 직접하지 않고 함수 호출만으로 move 생성자가 호출될 것이다.

하지만 아직 부족하다.
move 생성자를 위한 대입 연산자를 정의해야 한다.

move 생성자를 위한 대입 연산자를 재정의함으로써 완벽하게 move 생성자를 지원할 수 있게 되었다.

근데 반드시 move 생성자, 대입 연산자를 만들어줘야 할까?

이 예제는 move 생성자를 만들지 않았는데도 error 가 발생하지 않는 이유가 무엇일까?
mySwap()함수에서 move(a); 코드는 rvalue 이지만 move 생성자가 없으므로,
Cat(const Cat& c) 복사 생성자에서도 처리할 수 있다.
rvalue 는 const lvalue reference 에 대입될 수 있다.
그러므로 move 생성자가 없다면, 복사 생성자를 사용하게 된다.
rvalue => Cat&&, 없다면 const Cat& 사용한다.
결국엔 성능 향상을 위해서는 move 생성자를 만들어줘야 한다.


C++ 표준에서 제공하는 swap() 함수는 move()를 사용해서 구현되어 있다.


마지막으로 Move 생성자를 만들 때 주의할 점은, 멤버 변수 중에 객체 또한 move()로 옮겨야 한다.

Move 생성자를 만들 때, 멤버 변수가 일반 premitive 타입 변수, pointer 가 아니라
객체일 경우 대입 연산자를 사용할 경우 복사 생성자(깊은 복사)가 호출된다.
따라서 반드시 move() 를 이용하여 소유권이 이전되는 얕은 복사가 진행되도록 해야한다.

하지만 위 방법 보단 initializer 를 사용하는 것이 더 효과적이다.


마지막으로 이전 posting에서 다뤘던 rvalue, lvalue 에 대해 다시 살펴보자.

Test t2 = move(t1); 코드 이후에 t1 객체는 사실상 expired 된다.(t1은 껍데기만 남은 객체가 된다.)
이것을 xvalue 라고 부른다.
결국에는 c++11에는 rvalue, lvalue, xvalue 3가지 개념이 등장하게 된다.
1) lvalue : 이름 있는 변수
2) 상수, 임시 객체를 rvalue라고 불렀지만,
더 이상 이것을 rvalue 라 하지 않고 prvalue라고 부른다.
3) xvalue : 최초에는 lvalue로 태어났지만 껍데기만 남은 상태로 변하게 된 경우
4) xvalue, prvalue 를 묶어서 rvalue 라고 부르고,
5) lvalue, xvalue 를 묶어서 glvalue 라고 부른다.








2015년 9월 13일 일요일

[C++11] rvalue

rvalue : =(대입연산자)의 우측에만 사용 가능한 변수로 이름이 없다.
해당 코드에 순간적으로만 존재하고, 값을 반환하는 함수가 예가 된다.
lvalue : =(대입연산자)의 좌변, 우변에 모두 사용가능한 변수로 이름이 있으며 일정 시간동안 lifetime을 갖는다.
참조를 반환하는 함수가 예가 된다.

xvalue, prvalue, glvalue 가 더 있으나 일단은 위 두가지만 이해해 보도록 하자.

Detail Reference : http://en.cppreference.com/w/cpp/language/value_category


객체의 lifecycle 에 대해 알아보자.

전역 객체 p1은 main() 함수보다 먼저 생성된다.
지역 변수 p2 는 자신을 선언한 main() 함수 block 을 벗어날 때 소멸된다.
main()함수 안에서 Point(); 코드는 임시객체 생성을 의미하며,
자신이 포함된 문자(full expression)이 끝날 때 소멸된다.
임시 객체는 이름이 없기 때문에, unnamed object 라고도 불린다.

임시 객체에 대해 좀 더 알아보자.

main() 함수내에 선언된 p 객체가 만약 foo() 함수에 전달하고 사용될 필요가 없는 객체라면?
이렇게 단순히 인자로 전달하기 위한 객체는 임시객체로 만들어 사용하는 것이 바람직하다.



foo() 에서 반환하는 지역 변수 p는
실제로는 foo() 함수가 끝남과 동시에 소멸된다.
따라서, 반환할 임시객체를 생성하고 p 를 소멸하고 임시객체를 반환한다.
임시객체는 복사생성자를 통해서 생성된다.
Point p를 만들고 다시 임시 객체를 생성하는 중복 작업이 성능 저하를 가져오기 때문에
바로 임시객체를 만들어서 전달하는 것이 효과적이다.
이 기법을 RVO(Return Value Optimization)이라고 부른다.

하지만 RVO 기법을 사용하지 않고 이전 코드를 사용하더라고 컴파일러가 RVO 를 적용한다.
Microsoft VC 같은 경우엔 release 모드로 컴파일 할 경우, 컴파일러가 알아서 RVO를 적용한다.
최근의 컴파일러들은 이름있는 객체도 최적화할 수 있다.
이런 기법을 NRVO(Named RVO) 라고 부른다.


임시 객체와 lvalue에 관해 살펴보자.

제일 처음에 설명한 것을 다시 생각해보면 rvalue, lvalue 라는 것이 있고,
rvalue 는 =(대입연산자)의 우측에만 올 수 있으며 이름이 없다.
또한 해당 코드에 순간적으로만 존재하고, 값을 반환하는 함수가 예가 된다.
반면 lvalue 는 =(대입연산자)의 좌변, 우변에 모두 사용가능한 변수로 이름이 있으며 일정 시간동안 lifetime을 갖는다.
참조를 반환하는 함수가 예가 된다.

위 예제에서 foo() 함수는 값(value)을 반환하기 때문에 임시 객체를 생성하여 반환될 것이다.
전역변수 p의 복사본이 임시객체로 생성되어 반환되는 것이다.
따라서 main() 함수에서 foo().x = 10; 코드는
좌변에 rvalue(임시객체) 가 오는 형태가 되므로 컴파일 에러가 발생한다.

하지만 foo1() 함수는 참조(reference) 를 반환하기 때문에
전역객체 p의 주소가 그대로 반환되며 이것은 lvalue 이다.
lvalue 는 =(대입연산자)의 좌변에 올 수 있어 foo1().x = 10; 코드는 정상적으로 동작하고
실제 x의 값이 10으로 변경된 것을 볼 수 있다.


rvalue, lvalue 에 대해 조금 더 알아보자.

10 = n1; 이란 코드를 본적이 있는가?
10이 이름을 갖는 변수인가?
여기서 10은 rvalue 로 =(대입연산자) 좌변에 올 수 없다.

하지만 이름을 갖는 변수 n1, n2는 lvalue 이기에 n1 = 10; n2 = n1; 코드는 정상이다.

또한 이전 예제에서도 설명했듯이,
값을 반환하는 함수는 임시객체를 생성하여 전달하고 이것은 rvalue 이다.
따라서 foo().x = 10; 은 컴파일 에러를 발생시킨다.


lvalue, rvalue 의 규칙을 살펴보자.

규칙
1. C++ 일반 참조는 lvalue 만 참조할 수 있다.
2. const reference 는 rvalue, lvalue 모두를 참조할 수 있다!!
3. C++11 에서는 rvalue reference(&&)를 제공하여 rvalue 만 참조할 수 있도록 지원한다.

따라서, C++11 에서 공식적으로 &는 그냥 reference가 아닌 lvalue reference 라고 부르고
&& 는 rvalue reference 라고 부른다.


참조가 사용되는 함수와 Overloading 규칙을 살펴보자.

main()함수 내의 foo(n); foo(10); 코드를 먼저 살펴보면,
이전 예제에서 설명했듯이, const int& 는 rvalue, lvalue 를 모두 참조할 수 있다.
따라서 lvalue 인 n의 참조를 가리킬 수 있는 foo(int&);이 호출 우선순위가 높지만 이 함수가 없을 경우
foo(const int&);가 호출될 수 있다.
또한 rvalue 인 10은 rvalue 만을 가리키는 &&(rvalue reference) 를 인자로 받는 foo(int&&); 가 호출 우선순위가 높지만,
이 함수가 없을 경우 foo(const int&); 가 호출될 수 있다.
하지만 const int c 는 반드시 foo(const int&); 가 없으면 에러가 발생하는 것에 유의하자.

중요한 것은 다음 코드이다.
int&& r = 10; 에서
10은 rvalue 이고 r은 이름있는 참조 변수인 lvalue 이다.
따라서 foo(r); 코드는 foo(int&);이 호출된다.

이름이 있는 rvalue reference 는 lvalue 인 것이다.(named rvalue reference)
rvalue 와 rvalue reference 를 혼동하기 쉬운데,
10은 rvalue 이고 r 은 rvalue reference(결국 lvalue) 인 것이다.

그렇다면 r을 foo(int&&) 함수로 호출하고 싶다면?
Casting 해야 한다!
foo(static_cast(r)); 처럼 casting 해서 호출하면 foo(int&&); 로 호출된다.







[C++] meta programing

재귀 호출에 관해 template meta programming 을 적용한 예제를 살펴보자. #include using namespace std; int fact(int n){ if(n factorial 연산을 하는 일반적인 재귀 호출 함...