Override
: 함수를 Override 할 때 정확한 signature 에 맞춰 override 되는지 컴파일 타임에 확인되도록 하는 키워드
위 예제에서 발생하는 실수들이 문제가 되는 이유는
IDE에서 컴파일하는데 문제가 없기 때문이다.
실제로는 잘못된 코드임에도 불구하고 말이다...
그렇다면? 이런 실수를 방지하기 위해서
함수 이름 뒤에 override 키워드를 사용하면 컴파일 타임에 에러를 발생시킨다.
C의 기본 문법 중에 const 키워드를 살펴보자.
const 키워드의 위치에 따라 포인터가 가리키는 곳이 상수인지 포인터의 주소가 상수화되는지 차이가 난다.
더 나아가서 reference(&) 와 결합해서 const int*, int* const 의 reference(&)가 된다.
r2와 r3는 각각 const int*(상수를 가리키는 포인터)의 참조와 int* const(포인터의 주소가 상수)의 참조를 나타낸다.
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 라고 부른다.
: 객체 내부의 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 하기 보다 좀 더 쉬운 방법을 생각해보자.
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&&); 로 호출된다.
해당 코드에 순간적으로만 존재하고, 값을 반환하는 함수가 예가 된다.
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
[C++11] constexpr
const
: Type을 상수화하겠다는 키워드
constexpr
: 변수나 함수의 값이 컴파일 타임에 결정됨을 나타내는 키워드
C++98/03에서 const 의 문제점을 살펴보자.
const 의 의미는 컴파일 타임에 상수화 시키겠다는 것이다.
따라서 컴파일러가 컴파일 할 때 code에서 c가 나오면 매크로처리하듯이 10으로 치환해버린다.
따라서 일반 포인터 변수에 상수 c의 주소를 가리키게 할 수 없다.
하지만 상수 c의 주소를 강제적 형변환을 통해서 포인터 변수에 담을 수 있다.
c와 *p의 출력 결과를 살펴보면,
c의 출력 결과가 10으로 p에 의해 변경된 값이 아닌 것을 볼 수 있다.
int 변수 n을 const int 에 대입했을 때의 경우를 살펴보면,
cn은 컴파일 타임에 결정될 수 없는 runtime constant 이다.
따라서 이후 cn 을 통해서 변경이 불가능할 뿐이지,
p1 포인터가 값을 변경하는데 전혀 문제가 발생하지 않는다.
Runtime constant 는 배열에서도 문제를 발생 시킬 수 있다.
배열 크키는 컴파일 타임에 결정되어야 하는데, runtime constant 는 runtime 에 결정되므로
배열 크기로 사용될 수가 없다.
배열 크기에 대한 혼란
C89 : 배열 크기는 컴파일 시간 상수 이어야 한다.('1989)
C99 : 배열 크기로 변수를 보낼 수 있다. - gcc 지원 ('1999)
하지만, gcc 를 제외한 대부분 컴파일러는 지원하지 않음
결국, c++11에서는 컴파일 전용 상수 키워드를 만들게 된다.
constexpr 키워드를 사용함으로써 반드시 컴파일 타임에 정해진 상수를 사용하도록 강제할 수 있다.
: Type을 상수화하겠다는 키워드
constexpr
: 변수나 함수의 값이 컴파일 타임에 결정됨을 나타내는 키워드
C++98/03에서 const 의 문제점을 살펴보자.
const 의 의미는 컴파일 타임에 상수화 시키겠다는 것이다.
따라서 컴파일러가 컴파일 할 때 code에서 c가 나오면 매크로처리하듯이 10으로 치환해버린다.
따라서 일반 포인터 변수에 상수 c의 주소를 가리키게 할 수 없다.
하지만 상수 c의 주소를 강제적 형변환을 통해서 포인터 변수에 담을 수 있다.
c와 *p의 출력 결과를 살펴보면,
c의 출력 결과가 10으로 p에 의해 변경된 값이 아닌 것을 볼 수 있다.
int 변수 n을 const int 에 대입했을 때의 경우를 살펴보면,
cn은 컴파일 타임에 결정될 수 없는 runtime constant 이다.
따라서 이후 cn 을 통해서 변경이 불가능할 뿐이지,
p1 포인터가 값을 변경하는데 전혀 문제가 발생하지 않는다.
Runtime constant 는 배열에서도 문제를 발생 시킬 수 있다.
배열 크키는 컴파일 타임에 결정되어야 하는데, runtime constant 는 runtime 에 결정되므로
배열 크기로 사용될 수가 없다.
배열 크기에 대한 혼란
C89 : 배열 크기는 컴파일 시간 상수 이어야 한다.('1989)
C99 : 배열 크기로 변수를 보낼 수 있다. - gcc 지원 ('1999)
하지만, gcc 를 제외한 대부분 컴파일러는 지원하지 않음
결국, c++11에서는 컴파일 전용 상수 키워드를 만들게 된다.
constexpr 키워드를 사용함으로써 반드시 컴파일 타임에 정해진 상수를 사용하도록 강제할 수 있다.
[C++11] Inherit constructor, function
Inherit constructor, function
: 부모 class의 생성자와 함수를 상속받을 수 있다.
위 예제에서 main 함수에서 에러가 발생하는 code 는 무엇일까?
d.foo(1, 2); 호출 시 부모 class의 foo(int, int); 가 호출될 것이라 예상하지만, 그렇지 않다.
자식 class 에서 foo 를 만들면 부모에 있는 모든 foo함수는 가려지게 된다.(hide)
즉, 부모&자식 간의 함수 overloading 은 되지 않는다.
Overloading 은 해당 class내의 function 에 적용된다.
(너무나 당연한 얘기지만 가끔은 이런 사실조차도 헷갈릴 수 있다.)
이런 얘기를 하는 것은 해결방법이 있기 때문이 아니겠는가?!
부모 class의 function 을 상속 받아 사용하고 싶다면,
using 키워드를 사용한다.
또한 function 뿐만 아니라, 생성자도 상속이 가능하다.
: 부모 class의 생성자와 함수를 상속받을 수 있다.
위 예제에서 main 함수에서 에러가 발생하는 code 는 무엇일까?
d.foo(1, 2); 호출 시 부모 class의 foo(int, int); 가 호출될 것이라 예상하지만, 그렇지 않다.
자식 class 에서 foo 를 만들면 부모에 있는 모든 foo함수는 가려지게 된다.(hide)
즉, 부모&자식 간의 함수 overloading 은 되지 않는다.
Overloading 은 해당 class내의 function 에 적용된다.
(너무나 당연한 얘기지만 가끔은 이런 사실조차도 헷갈릴 수 있다.)
이런 얘기를 하는 것은 해결방법이 있기 때문이 아니겠는가?!
부모 class의 function 을 상속 받아 사용하고 싶다면,
using 키워드를 사용한다.
또한 function 뿐만 아니라, 생성자도 상속이 가능하다.
[C++11] Delegating constructor
Delegating constructor
: C++11 부터는 생성자에서 다른 생성자를 호출할 수 있다.
Point() 생서자 안에서 Point(0, 0); code 는 무엇을 의미하는 것일까?
보기에는 x, y 를 0, 0으로 초기화하도록 하는 생성자를 호출하는 것 처럼 보일 수 있다.
하지만 이 코드는 생성자를 호출하는 것이 아니라 임시 객체를 생성하는 코드이다.
하지만 C++11 에서는 생성자안에서 다른 생성자를 호출 할 수 있다.
또한 멤버 변수 선언과 동시에 초기화도 지원한다.
: C++11 부터는 생성자에서 다른 생성자를 호출할 수 있다.
Point() 생서자 안에서 Point(0, 0); code 는 무엇을 의미하는 것일까?
보기에는 x, y 를 0, 0으로 초기화하도록 하는 생성자를 호출하는 것 처럼 보일 수 있다.
하지만 이 코드는 생성자를 호출하는 것이 아니라 임시 객체를 생성하는 코드이다.
하지만 C++11 에서는 생성자안에서 다른 생성자를 호출 할 수 있다.
또한 멤버 변수 선언과 동시에 초기화도 지원한다.
2015년 9월 12일 토요일
[C++11] Tuple
Tuple
: 서로 다른 타입의 데이터를 여러개 보관할 수 있는 container
Tuple 은 Variadic arguments 를 활용할 수 있는 좋은 container
그렇다면 Tuple 을 직접 만들어 보자!!
Tuple 만들기
1. variadic arguments 사용한다.
2. 첫 번째 템플릿 인자는 독립적인 타입으로 받아야 한다.
Recursive function 을 만들 듯이,
인자를 한 개만 받을 때를 먼저 생각하면 좀 더 쉽게 접근할 수 있다.
그럼 이제 데이터를 꺼내 오는 방법에 대해 생각해보자.
매번 casting 해서 값을 꺼내오는 방법은 너무 불편하다!
Template partial specialization 을 이용해서 N번째 요소의 값을 가져오는 함수를 만들어 보자!
위 예제를 살펴보면,
xget() 함수 하나로 xtuple 에서 N번째 요소에 접근할 수 있게 되었다.
먼저 이전 예제에서 개선되어야 했던 점은, N번째 요소에 접근하기 위해
해당 xtuple 의 타입을 직접 캐스팅해야 한다는 점이다.
그래서 부분 전문화를 이용해서 N번째의 value의 type과 N번째의 xtuple 의 type을 구분할 수 있게 했다.
결국엔 부분 전문화를 type을 나타내기 위한 사용하였고,
N번째 값에 대한 타입은 xtuple_element::type,
N번째 xtuple에 대한 타입은 xtuple_element::tupleType 으로 나타낼 수 있게 된 것이다.
하지만 이 모든 것이 xget() 내부에 있기 때문에 xget() 함수 호출만으로 편리하게 사용할 수 있게 되었다.
: 서로 다른 타입의 데이터를 여러개 보관할 수 있는 container
Tuple 은 Variadic arguments 를 활용할 수 있는 좋은 container
그렇다면 Tuple 을 직접 만들어 보자!!
Tuple 만들기
1. variadic arguments 사용한다.
2. 첫 번째 템플릿 인자는 독립적인 타입으로 받아야 한다.
Recursive function 을 만들 듯이,
인자를 한 개만 받을 때를 먼저 생각하면 좀 더 쉽게 접근할 수 있다.
그럼 이제 데이터를 꺼내 오는 방법에 대해 생각해보자.
매번 casting 해서 값을 꺼내오는 방법은 너무 불편하다!
Template partial specialization 을 이용해서 N번째 요소의 값을 가져오는 함수를 만들어 보자!
위 예제를 살펴보면,
xget() 함수 하나로 xtuple 에서 N번째 요소에 접근할 수 있게 되었다.
먼저 이전 예제에서 개선되어야 했던 점은, N번째 요소에 접근하기 위해
해당 xtuple 의 타입을 직접 캐스팅해야 한다는 점이다.
그래서 부분 전문화를 이용해서 N번째의 value의 type과 N번째의 xtuple 의 type을 구분할 수 있게 했다.
결국엔 부분 전문화를 type을 나타내기 위한 사용하였고,
N번째 값에 대한 타입은 xtuple_element
N번째 xtuple에 대한 타입은 xtuple_element
하지만 이 모든 것이 xget() 내부에 있기 때문에 xget() 함수 호출만으로 편리하게 사용할 수 있게 되었다.
피드 구독하기:
글 (Atom)
[C++] meta programing
재귀 호출에 관해 template meta programming 을 적용한 예제를 살펴보자. #include using namespace std; int fact(int n){ if(n factorial 연산을 하는 일반적인 재귀 호출 함...
-
Template specialization : class template 을 arguments에 따라서 customizing 하는 기법 먼저 위 예제의 첫 번째 template 선언처럼 일반적인 template 선언은 primary temp...
-
Override : 함수를 Override 할 때 정확한 signature 에 맞춰 override 되는지 컴파일 타임에 확인되도록 하는 키워드 위 예제에서 발생하는 실수들이 문제가 되는 이유는 IDE에서 컴파일하는데 문제가 없기 때문이다...
-
Smart Pointer : 포인터처럼 동작하며 자동으로 메모리를 해제하고 안전하게 resource를 관리하도록 돕는 객체 포인터는 소멸자가 호출되지 않아 memory leak이 발생한다. Java, C#같은 VM이 있는 언어는 VM에서 G...