#include
using namespace std;
//1) 기본 템플릿
//primary template(주 템플릿)
template class Stack
{
T buff[10];
public:
void push() {cout << "push(T)" << endl;}
};
//2) 부분적인 타입에 특화된 템플릿을 사용하고 싶다면?
//partial specialization(부분 전문화)
template class Stack
{
T buff[10];
public:
void push() {cout << "push(T*)" << endl;}
};
//3) 특정 타입에 특화된 템플릿을 사용하고 싶다면?
//specialization(전문화)
template<> class Stack //를 삭제한다.
{
char* buff[10]; //T를 특정 타입으로 변경한다.
public:
void push() {cout << "push(char*)" << endl;}
};
void main()
{
Stack s1;
s1.push(); //T
Stack s2; //c++에서는 포인터도 타입으로 인지한다.
s2.push(); //T*
Stack s3;
s3.push(); //char*
}
정리 :
모든 타입에 대해서 처리할 수 있는 템플릿을 primary template이라 하고
부분 타입에 대해서 처리할 수 있는 템플릿을 partial specialization 이라 한다.
특정 타입에 대해서 처리할 수 있는 템플릿을 specialization 이라 한다.
왜 필요한가??(template meta programming 의 장점)
예를 들어, 5!(factorial)의 값을 얻으려 할 때,
loop 혹은 recursive code는 run time 에 동작한다.
하지만 class template과 function template 은 compile time에 동작하고
따라서 partial specialization과 specialization 을 이용하면 runtime이 아닌 compile time에 작업을 처리할 수 있다.
#include
using namespace std;
class Stack
{
int buff[10];
public:
void push(int data){}
int pop() {return 0;}
};
void main()
{
Stack s;
s.push(55); //int
s.push(10.123); //double ??
}
Stack에 저장되는 data가 integer type이 아닌 double 형 type으로 바꾸길 원한다면..?
#include
using namespace std;
template class Stack
{
T buff[10];
public:
void push(T data){}
T pop() {return 0;}
};
void main()
{
Stack s;
s.push(10.123);
}
template을 사용하여 type에 의존적인 부분을 변경한다.
class template은 function template과 달리 암시적 추론이 되지 않는다.
Stack s; 로 선언한것은 완전하지 않다.
따라서 Stack<double> s;와 같이 상세 type을 적어야 한다.
#include
using namespace std;
int Max(int a, int b) {return a > b ? a : b;}
double Max(double a, double b) { return a > b ? a : b; }
void main()
{
printf("%d\n", Max(1, 2));
printf("%f\n", Max(10.22, 2.34));
}
type 에 따라 각 함수를 정의하는 것은 비효율적이다.
#include
using namespace std;
#define MAKE_MAX(T) T Max(T a, T b) { return a > b ? a : b; }
MAKE_MAX(int)
MAKE_MAX(double)
void main()
{
printf("%d\n", Max(1, 2));
printf("%f\n", Max(10.22, 2.34));
}
매크로를 이용하여 타입을 정의할 수 있고 좀 더 효율적이다.
하지만, 매크로는 컴파일러가 아닌 전처리기가 다루는 것이기 때문에 문제점이 있다.
(e.g, 이항연산자 처리 시 문제가 발생한다. 전처리기는 타입 체크를 하지 않는다.)
#include
using namespace std;
template T Max(T a, T b) { return a > b ? a : b; }
void main()
{
//printf("%d\n", Max('A', 2)); //error
printf("%d\n", Max('A', 2));
}
매크로를 template 으로 변경한다.
template은 컴파일러가 다루는 부분이고, 타입 체크를 할 수 있고 이항연산자 문제를 해결할 수 있다.
printf("%d\n", Max('A', 2));
위 코드에서 'A'는 ASCII 코드 65로 처리되지 않는다.
template function 을 사용할 때, 타입을 명시적으로 선언해줘야
컴파일러가 그 타입에 맞게 변경하여 template function 을 호출하게 된다.
정리 :
함수 템플릿은 템플릿의 인자를 생략하는데,
이는 컴파일러가 컴파일 타임에 타입을 추론하여(암시적)
기계어 코드를 생성하므로 굳이 타입을 명시적으로 사용할 필요가 없다.
다만, 사용자가 템플릿 타입을 명시적으로 사용하고 싶다면 아래와 같이 타입을 선언해서 쓸 수 있다.
printf("%d\n", Max<int>('A', 2));
Android Framework에서도 IBinder를 asInterface 로 제공하는데
Google 에서 interface_cast(binder); 를 사용하여 제공하도록 개선하였다.
결국엔 interface_cast 도 function template이다.
Smart Pointer
: 포인터처럼 동작하며 자동으로 메모리를 해제하고 안전하게 resource를 관리하도록 돕는 객체
포인터는 소멸자가 호출되지 않아 memory leak이 발생한다.
Java, C#같은 VM이 있는 언어는 VM에서 Garbage collector가 leak 이 발생하는 메모리를 해제시킨다.
C++에서도 자동으로 메모리를 해제하는 기법을 개발하게 되는데 이것이 smart pointer이다.
Sptr이 진짜 pointer라면 -> 연산자가 동작해야하는데 동작하지 않는다.
p.operator->()go(); 이렇게 처음에 바뀌지만 문법에 맞지 않아서 컴파일러는 아래와 같이 바꾼다.
(p.operator->())->go();
따라서 -> 연산자를 재정의해야 한다.
* 이것도 마찬가지로 operator로 제공되야 한다.
(p.operator*()).go(); 로 바뀌어서 호출될 것이다.
정리)
특정 객체가 임의의 포인터 타입이 되기 위해서는 아래의 두 연산자를 재정의 해야 한다.
*, ->
Smart pointer 특징)
진짜 포인터가 아니고 객체이므로 개발자가 생성/복사/대입/소멸의 과정을 직접 제어할 수 있다.
e.g, 소멸자를 활용한 자동 삭제
그리고 궁극적으로 Smart Pointer 로 활용되기 위해서는 어떤 object pointer 도 받아 들이기 위해서 template 을 이용해야 한다.
Smart pointer 의 복사 생성자
진짜 pointer 는 대입 연산자가 동작한다.
제대로 동작하지 않는다.
위 코드는 default 복사 생성자가 호출되어 얕은 복사가 진행됐기 때문에
소멸자에서 이미 해제된 메모리를 또 다시 해제하려고 할 것이다.
그렇다면, 얕은 복사 대신에 깊은 복사를 하는 복사 생성자를 만들면 해결이 될까?
하지만 Android Framework 에서는 객체 복사시에 깊은 복사 방식을 사용하지 않고
Reference counting(참조 계수) 방식으로 객체 복사를 구현하고 있다.
참조 계수 방식으로 Smart pointer 객체의 복사를 구현
왜 깊은 복사가 아닌 참조 계수 방식을 사용할까?
예를 들어, Smart pointer를 깊은 복사로 구현할 경우,
위 코드에서 p2에서 값을 수정할 경우 p1의 값이 변경되지 않는다
이것은 pointer 의 개념을 벗어나게 되고, 깊은 복사가 Smart pointer에 적용되지 않는 이유다.
이미 참조 계수 기반의 스마트 포인터는 C++ 표준에서 지원한다.
하지만 Android Framework 는 이 방식의 Reference counting 기법을 적용한 Smart Pointer 를 사용하지 않는다.
Reference counting 기법을 적용한 Smart pointer 의 문제점
Smart pointer가 처음에 의도했던(Pointer 같은 Object) 와 달리 sizeof 연산자를 사용하면
int* 와 다른 값을 나타낸다.
Reference counting 기법을 사용한 Smart pointer 는
참조 계수 기반으로 구현해야 하기 때문에, 포인터를 2개 가져가므로 8 byte 를 사용하게 된다.
그래서 Android Framework 에서는 표준에서 제공하는 Smart Pointer 를 사용하지 않는다.
해결책 :
reference count 을 Smart Pointer 에서 관리하지 않고 해당 object 에서 관리하도록 변경한다.
Reference count를 object 에서 관리하도록 변경
Smart Pointer 를 다시 되돌아 보면, Smart Pointer 의 장점은 객체의 생성/복사/대입/소멸을 직접 관리할 수 있다는 것이다.
그러므로, Smart Pointer 의 생성/소멸 시점에서 내부 객체(e.g, Car)의 참조 계수를 증가/감소 시키면 될 것이다.
더불어 Smart Pointer 는 내부 객체의 포인터만을 관리하게 됨으로써 궁극적으로 Pointer 그 자체로써 표현이 가능하게 된다.
결과적으로 개발자가 객체를 관리하는 것이 아니라, Smart Pointer 가 책임을 지게 되고
이렇게 위임된 코드 스타일을 proxy pattern 이라 한다.
하지만 객체의 포인터를 사용할 때, 매번 참조 계수를 inc/dec 하는 것은 매우 번거롭다.
그렇다면 Smart Pointer 의 생성/소멸에서 내부 객체의 참조 계수 증가/감소를 호출하는 방식을 살펴보자.
Smart Pointer 의 생성자,소멸자에서 내부 객체의 참조 계수 증가/감소를 호출하여
직접적으로 참조 계수 증가/감소를 개발자가 호출하지 않도록 했다.
이 방식을 적용한 Smart pointer는 진짜 pointer와 동일한 크기를 갖게 된다.
하지만 Android Framework 는 primitive type을 smart pointer로 지원하지 않는다.
primitive 는 mCount 같은 참조 계수가 내부 없기 때문이다.
결국, Android Framework 는 RefBase의 class 만을 Smart Pointer 로 지원한다.
정리 :
C++ 표준에서의 참조 계수 기반의 Smart Pointer 는 shared_ptr 이다.
shared_ptr
장점 : 모든 타입에 대하여 처리 가능
단점 : 메모리의 사용량 증가(reference count 를 내부에서 관리하므로)
Android 에서의 참고 계수 기반의 Smart Pointer 는 sp(strong pointer) 이다.
장점 : 메모리 사용량에 있어 overhead가 없다.(reference counting을 내부에서 관리하지 않음)
단점 : RefBase 클래스의 자식이 아닌 경우, Smart pointer로 처리될 수 없다.
sp : Smart Pointer 가 아니라 Strong Pointer로 불린다.
wp : weak pointer
상호 참조 - Reference counting 기법을 사용한 Smart pointer의 문제점
p1, p2 는 서로를 참조하므로 reference count 가 감소되지 않아 소멸자가 호출되지 않는다.
이런 상호 참조 문제점을 해결하기 위해,
상호 참조하는 경우 reference count 를 증가시키지 않도록 하는 방법이 제안되었다.
이것이 weak pointer 이며 C++ 표준으로 지원한다.
Android 에서는 객체 복사를 위해 아래 3가지 방법을 지원한다.
1) sp(strong pointer) : reference counting(참조 계수) 기반
2) wp(weak pointer) : reference counting 을 하지 않는 pointer (C++ 표준)
3) uniquePtr : 복사 금지에 사용하는 pointer
위 코드는 default 복사 생성자가 호출되어 얕은 복사가 진행된다.
하지만 main 함수가 종료될 때, p1의 소멸자가 호출되면서 "kkk"에 대한 메모리가 해제되는데,
p2에서 다시 해제하려고 하기 때문에 에러가 발생한다.
객체를 복사하는 기법
1) 깊은 복사(Deep copy)
Person class에서 복사 생성자를 정의하여 깊은 복사를 하도록 했다.
하지만 대부분의 open source에서는 이와 같은 깊은 복사를 진행하지 않는다.
복사 생성자 호출 시 내부적으로 메모리 동적 할당을 시도하기 때문에 비용이 높고 성능에 영향을 미친다.
객체를 복사하는 기법
2) 소유권 이전
소유권 이전 방식은 언제 필요한가?
e.g, swap
일반적으로 swap 은 source, target, temp 3개의 객체가 서로 복사되는 overhead가 발생한다.
그래서 얕은 복사로 진행하여 성능을 향상 시킬 수 있다.
소유권 이전 방식은 C++에서 표준으로 채택하였으며,
C++11 에서는 move 생성자를 지원하기도 한다.
하지만 소유권 이전 방식은 성능에 약간의 향상을 얻을 수 있지만 Android 는 이 방식을 사용하지 않는다.
객체를 복사하는 기법
3) Reference counting(참조 계수)
참조 계수 방식은 기본적으로 얕은 복사를 진행하나,
Reference counting 을 증가/감소시킴으로써
실제 참조하는 object 가 없을 때, 소멸되도록 하는 방식이다.
객체를 복사하는 기법
4) 복사 금지
참조 계수를 이용하는 것 조차 성능상에 문제가 발생할 수 있는 경우는,
복사 생성자와 대입연산자를 private 영역에 선언함으로써
아예 복사 자체를 금지하게 하는 방법도 있다.
Android Framework에서는 SP(Strong Pointer), WP(Weak Pointer) 는 참조계수 방식으로
UniquePtr 은 복사금지 방법으로 제공한다.
Singleton pattern
: 오직 유일한 하나의 객체만을 만드는 design pattern
Singleton pattern 구현 방법
1) 객체의 생성을 막기 위하여 생성자를 private 영역에 정의한다.
2) 생성된 객체 없이도 함수를 호출할 수 있기 위해서 정적 함수를 제공한다.
3) 객체의 복사와 대입을 금지하기 위하여 복사 생성자와 대입 연산자 함수를 private 영역에 정의한다.
하지만 Cursor class 내의 함수에서 일어나는 복사, 대입연산자로 인해
singleton 개념이 침해되는 것을 막을 수 없다.
따라서 객체 내부에서 일어나는 복사, 대입 연산자를 막기 위해서 함수를 선언만 한다.
하지만 함수의 정의를 하지 않고, private 영역에 선언만 해버리면, 컴파일 타임에는 문제가 없지만 링킹 타임에 에러가 발생한다.
또한 실제 개발할 때는 header와 source 코드가 나뉘기 때문에
header 파일에 위 처럼 선언 부분만 되어 있다면
이 객체를 사용하는 개발자는 이것이 선언만 되어 있는지 정의도 되어 있는지 알 수 없다.(실제 source code를 살펴보지 않는한..)
그래서 C++11 에서 제공하는 delete 키워드를 사용한다.
하지만 구현 할 때마다 singleton pattern을 매번 구현하는 것이 번거롭기 때문에
metacode 와 template 을 이용해서 좀 더 간단하게 singleton 을 구현한다.
1) 복사, 대입연산자를 매크로로 선언한다.(MAKE_NO_COPY)
2) 생성자, static 변수, 복사, 대입 연산자를 하나의 DECLARE 패턴으로 사용한다.
3) 구현 코드도 매크로로 치환한다.
구현 코드를 객체 내부에서 밖으로 가지고 나와서
생각해보면 매크로로 만드는데 조금더 쉽게 생각할 수 있다.
최종적으로 정리된 매크로로 구현한 Singleton pattern
Singleton 을 사용할 때 발생하는 concurrency 문제를 살펴보자.
일반적으로 multi thread 프로그래밍을 구현할 때,
critical section 의 자원을 동기화하기 위해 mutex 를 사용한다.
하지만 new Cursor에서 메모리 할당이 실패하면, exception 이 발생하게 된다.
예외가 발생한 시점 부터 이전으로 stack unwinding 한다.
이 상황에서 mutex.lock()이 걸려 있는 상태가 유지되어 deadlock 이 발생하게 된다.
이 문제를 해결하기 위한 RAII(Resource Acquisition Is Initialisation) 기법을 사용한다.
RAII 기법의 기본적인 원리는
C++은 지역 객체가 사라질 경우 소멸자가 불려지는 것을 보장하고,
혹은 heap 에 할당된 객체를 delete 할 경우 소멸자가 호출되는 점을 이용한다.
이렇게 block 내에서 lock 을 해제해주는 테크닉을 auto lock 혹은 scope lock 이라 한다.
C++11 에서는 위와 같은 auto lock 기법을 lock_guard class로 지원한다.
const function
: 멤버 함수 내부에서 사용되는 멤버 변수를 상수화하는 함수로 멤버 변수의 값을 변경할 수 없다.
const 키워드를 함수에 사용하게 되면, 함수의 signature 가 변경된다.
const 사용전에는 void display(); 의 signature는 void Point::display(Point* const this); 가 될 것이다.
this 자체의 주소는 변경할 수 없지만, this 포인터로 접근하여 변경할 수 있다.
const 를 사용하면, void Point::display(const Point* const this);로 signature가 변경되기 때문에
this->x 로 접근하여 변경이 불가능하게 된다.
display() 는 멤버 변수를 변경하기 위한 것이 아니라, 단순하게 출력이 목적이다.
멤버 변수 x의 값을 변경하는 동작은 display 함수 목적에 맞지 않다. bug가 된다.
이렇게 함수의 목적과 다르게 멤버 변수를 수정하지 않도록 보장하기 위해서 const function 을 사용하게 된다.
또한 상수 함수는 signature 가 다르기 때문에 overloading 이 가능하다.
상수 함수의 사용 목적 :
객체의 상태 값을 변경하지 않게 하기 위해서, 객체의 안정성을 높인다.
하지만, 그 본질은 안정성 때문이 아니다. 안정성은 장점이지 본질이 아니다.
또한 함수의 인자로 전달되는 원본 객체에 대한 수정을 방지하기 위해 const 와 & 를 사용한다.
display 가 const 가 아닐 경우, foo 함수 내에서 p의 객체 내부를 변경할 수 있게 된다.
상수 객체로 들어온 인자는 상수 함수밖에 호출이 안되기 때문에, const 를 사용하는 것이다.
(안정성 때문에 사용하는 것이 아니라..)
const 를 사용하는 것은 optional 이 아니라 requirement 이다.
상수 객체로 들어온 인자는 상수 함수 밖에 호출이 안되기 때문이다.
void display() const 를 주석 처리하면,
main 함수에서 foo(p); 코드가 실행이 안되는 이유가 바로
상수 객체로 들어온 인자는 상수 함수 밖에 호출이 안되기 때문이다.
상수 함수와 멤버 함수가 overloading 되었을 때, 누가 호출되는가?
1) 일반 객체에서는
일반 멤버 함수가 먼저 호출되고, 이것이 없을 대 상수 함수가 호출된다.
2) 상수 객체에서는
당연히 상수 함수만이 호출될 수 있기 때문에, 일반 멤버 함수는 호출되지 못한다.
util function 으로 to_string() 을 지원하는 class 를 작성했다.
하지만 sprintf 가 매번 호출되는 것이 성능 저하의 원인이라 가정하고, cached 개념을 적용해보자.
to_string()함수는 객체 내부의 x, y의 상태를 변경하는 함수가 아니다.
하지만 const 함수로 변경한다면, 아래 cache_valid, sprintf 코드에서 에러가 발생한다.
그렇다면 const내에서 사용되는 멤버 변수 중에서 상수화를 보장할 필요 없는 것은 mutable 키워드를 사용하면 어떨까?
하지만 mutable 를 사용한다는 것은, 클래스의 설계가 잘못되었다고 볼 수 있다.
mutable 을 사용하길 원하지 않는다면, mutable 대신에 변수를 구조체로 변경하고
그 구조체를 가리키는 포인터를 선언하여, 그 포인터로 const 함수에서 변경하도록 할 수 있다.
하지만 Cache 객체를 동적할당하기 때문에 그에 따른 단점도 발생할 수 있다.
Summary
1. 상수 함수는 멤버 변수의 포인터나 또는 참조를 리턴할 수 없으며, 값의 수정도 불가능하다.
2. 상수 함수 내에서 상수 함수가 아닌 함수를 호출하는 것은 불가능하다.
3. 상수 함수 내에서 멤버 변수의 포인터나 또는 참조를 리턴하고 싶은 경우,
혹은 값을 수정하고 싶은 경우에는 mutable 키워드를 사용한다.
혹은 변수를 구조체로 변경하여 해당 구조체의 포인터를 사용한다.
4. 변하는 것(정책)과 변하지 않는 것(알고리즘)을 분리하는 개념을 적용해도 된다.
5. 일반 함수와 상수 함수가 오버로딩된 경우 호출 순서
일반 객체 : 일반 함수를 먼저 호출하고, 일반 함수가 없을 경우, 상수 함수 호출
상수 객체 : 상수 함수만 호출 가능하다.
6. 상수 함수는 선택이 아니라 필수이다.
상수 객체는 상수 함수만을 호출할 수 있기 때문이다.
매크로 함수는 함수의 호출처럼 보이지만 전처리계 과정에서 치환되는 것일 뿐이다.
매크로 함수의 단점으로 단항연산자(++) 을 정상적으로 처리하지 못한다.
인라인 함수는 디버깅 모드나 컴파일러 최적화 옵션을 설정해주지 않으면 동작하지 않는 단점이 있다.
인라인 함수가 동작하지 않는 조건(컴파일러가 인라인 함수를 무시하는 경우)
1) 재귀 호출
2) 함수 포인터 사용 시
3) 함수의 기계어 코드 크기가 매우 큰 경우, 컴파일러가 inline으로 실행하지 않음
전통적으로 C언어 개발자들은 함수 호출에 따른 오버헤드를 줄이기 위해서 매크로를 사용한다.
의 결과는 어떻게 될까?
9라고 예상하기 쉽지만, 12 혹은 16 으로 출력된다.
처럼 3이 전달되고 3 * 3 연산이 진행될 것으로 예상할 수 있으나,
으로 치환된다.
결과적으로 매크로 함수는 위와 같은 단항연산자의 문제를 해결할 수 없다는 문제점이 있다.
C++ 에서는 매크로 함수를 전처리기가 실행하는 것이 아니라, 컴파일러가 실행하도록 변경하였다.
inline 함수가 이렇게 동작한다.
위 코드는 add 함수와 달리 함수의 기계어 코드가 남게 된다.
add 함수는 asm 코드를 확인해보면 call add 를 호출하게 되는데,
inline_add 는 inline 함수이므로 컴파일러가 해당 line 을 기계어 코드로 변경하게 된다.
한 가지 주의할 점은, inline 함수를 사용하려면 컴파일 최적화 옵션을 주어야 한다.
Visual studio 개발 환경에서는 아래와 같이 cl 호출 시 옵션을 /Ob1 을 줘야 한다.
C:\ cl inline_function.cpp /FAs /Ob1
혹은 visual studio 에서는 project > 속성 > c/c++ > 최적화 에서 설정할 수 있다.
결론적으로,
인라인 함수는 함수의 호출 자리에 기계어 코드로 치환되는 함수를 의미한다.
장점 : 인라인 함수는 전처리기가 아닌 컴파일에 의해 처리되므로 매크로의 효율성은 그대로 유지하면서 코드의 안전성은 높아지게 된다.
단점 : 자칫 목적 파일의 크기가 커질 수 있으므로, 함수의 구조가 간단한 경우에만 사용하는 것을 권장한다.
하지만 inline 함수가 적용되지 않는 한 가지 경우가 있다.
재귀 함수의 경우 inline 함수가 적용되지 않는다.
또 한 가지 inline 함수가 적용되지 않는 예를 살펴보자.
foo(), goo() 함수 호출 시 아래와 같은 기계어 코드로 변경된다.
call 로 호출이 되냐 안되냐에 차이가 있는데
inline function 은 call 명령어를 사용하지 않는다.
(call 명령어는 instruction pointer에 있는 내용을 stack 에 넣고(push) 해당 주소로 jump 한다.)
하지만 function pointer의 경우 runtime 에 언제든지 변경될 수 있으므로,
컴파일러가 이것이 inline function 을 가리킬지 일반 함수를 가리킬지 확신할 수 없다.
따라서 function pointer 의 경우 inline function 을 가리키게 해도 inline 으로 치환되지 않는다.
Summary
1. 인라인 함수는 함수 호출 명령이 함수 코드로 확장되는 함수
2. 일반적으로 컴파일러는 인라인 명령을 수행하지 않으며 최적화 옵션을 통해 실행될 수 있다.
3. 함수 호출로 인한 성능 저하가 없으며 매크로 함수보다 안전하다.
4. 목적 파일의 크기가 커질 수 있으므로 가급적 짧은 함수에 사용하는 것이 좋으며
목적 파일의 크기가 오히려 작아질 수 있다.
5. 경우에 따라서 컴파일러가 인라인 명령을 무시할 수도 있다.
e.g, 크기가 매우 큰 함수, 재귀 호출, 함수 포인터 사용 등
임시 객체는 개발자가 의도해서 생성할 수도 있지만 대부분 컴파일러의 필요에 의해서 생성된다.
primitive 말고 객체를 임시 객체로 생성되는 경우 성능에 영향을 미칠 수 있다.
그러므로 함수 안에서 객체를 지역 변수로 단순히 생성해서 반환하는 경우 임시 객체를 반환하도록 해서
임시 객체가 컴파일러에 의해 생성되지 않도록 최적화해야 한다.
먼저 main 함수에서 Int32(10); 코드는 임시 객체를 생성한다.
이 코드에서 임시객체는 main 함수가 끝나지 않았는데도, 해당 line 에서 생성되고 line 이후에는 바로 소멸된다.
cc = aa + bb; 코드에서는 aa.operator + (bb) 를 통해 임시 객체가 생성되고
이 임시객체의 복사생성자에 의해 cc 로 결과가 대입된다.
primitive type 의 경우에도 마찬가지이다.
z = a + b; 코드에서 a + b 는 임시 객체가 생성된다.
결국엔 대입 연산자 때문에 임시 객체가 생성되는데,
10 + 20 의 결과 30이 메모리상에 먼저 저장되고 이후 z에 저장된다.
premitive type 에는 임시 객체 생성/소멸이 큰 부담이 되지 않지만
user defined class 의 임시 객체 생성과 소멸은 다른 얘기가 된다.
따라서 위 예제에서는 + 연산자를 재정의한 코드를 수정함으로 써
임시객체를 바로 반환하도록 할 수 있다.
지역 변수를 생성해서 반환하는 것이 아니라, 임시 객체를 명시적으로 반환한다.
이렇게 하면 아래 cc = aa + bb 에서 임시 객체가 추가로 생성되지 않는다.
이렇게 임시 객체를 바로 반환하는 기법을 RVO(Return Value Optimization) 이라 한다.
마지막으로 임시 객체와 참조에 대해 생각해보자.
값이 아닌 참조를 전달할 경우에는 임시 객체가 생성되지 않는다.
따라서 임시 객체 생성으로 인한 성능 저하를 막기 위해서 아래 2가지 방법이 사용될 수 있다.
1. RVO
2. Return of Reference