2015년 9월 30일 수요일

[C++] member function & function pointer


우리는 함수의 포인터를 사용하여 함수를 또 다른 함수의 인자로 또는 반환 타입으로 사용할 수 있다.

포인터 변수 선언 방법은 어떻게 하는가?
타입 * 변수명; (통상 C++은 type에 *를 붙여쓰고, C개발자는 변수명에 붙여씀)

함수 포인터 선언시 필요한 '함수의 타입'은 무엇인가?
예를 들어, int foo() 란 함수를 가리키는
함수 포인터의 타입은 int를 return 하고 parameter 가 없는 것이다.


함수 포인터 변수 선언은 리턴타입(*변수명)([매개변수]); 으로 작성한다.
함수 호출 시, 함수 호출 연산자(()) 를 사용해야 함을 유의하자.

static function 또한 함수 포인터로 할당할 수 있다.
다만 static_function 은 this 개념이 없지만,
C언어의 전역 변수와 동일한 개념이고, Clazz 라는 namespace에 종속되어 있는 것이 차이가 있을 뿐이다.
따라서 Clazz namespace 를 통한 참조로 접근할 수 있다.

멤버 함수를 함수 포인터로 할당하는 경우를 주의해야 한다.
명시적으로 & 를 붙여줘서 해당 객체에 접근하도록 한다.
하지만 mfp() 호출은 불가능 하다. this 를 참조할 수가 없기 때문인데,
객체의 주소는 레지스터에 저장되어 있기 때문에, 명시적으로 넘겨줄 수 없다.

이런 경우 .* 연산자가 필요하다.(조금 생소하지만 C++ 창시자가 이렇게 만들었다..)
.* 좌측에는 객체가 와야 한다.
하지만 만약 좌측에 온 객체가 포인터라면 ->* 연산자를 사용한다.
->* 연산자는 하나로 봐야하고, 좌측의 객체는 포인터여야 한다.

정리하자면, 일반 멤버 함수는 C언어의 함수포인터로 저장될 수 없다.
함수 포인터에 this 개념이 없기 때문이다.
하지만 정적 멤버 함수의 경우, 일반 함수 포인터에 대입될 수 있다.








[C++] this

this
: 자기 자신 object 의 주소를 가리키는 상수 포인터






위 함수의 실제 signature 는 다음과 같다.


따라서 p.set(0, 0); 을 호출하면, set(&p, 0, 0); 으로 변경되서
memory 의 text 영역에 객체 p의 stack 주소를 알 수 있게 한다.

모두가 잘 아는 얘기지만
memory 는 heap, stack, data, text 영역으로 나뉘고
Point class 의 set function 은 text 영역으로 할당된다.
그리고 Point의 x, y 멤버 변수는 stack 영역에 할당된다.

static 변수와 this 의 관계를 살펴보자.

정적 멤버 함수는 왜 멤버 변수를 참조할 수 없는 것일까?
정적 멤버 함수는 Clazz 의 namespace 에 속해있는 전역 함수이다.
하지만 Clazz 의 namespace 에 속해 있다고 해서 this 키워드를 사용할 수 있는 것은 아니다.
정적 멤버 함수는 this 가 없어 this 키워드를 사용할 수 없다.


static_value 는 전역 변수이므로 this 없이 접근이 가능하다.
this->member_value = 0;으로 바뀌는데 this 로 참조할 수 없으니 에러가 난다.

반면 멤버 함수는 당연하게도 this 를 참조할 수 있어 멤버 변수, 정적 변수에 모두 접근할 수 있다.








[C++] explicit

explicit
: 생성자와 변환 연산자가 암시적 변환이나 복사-초기화가 동작하지 않도록 함





foo함수를 호출할 때 class가 아니라 integer를 넣어줘도 동작한다.
컴파일러는 Stack class를 조사하여 호출이 가능한지 확인하는데,
결국엔 foo(Stack(20)); 으로 변경하여 호출하게 된다.

생성자가 마치 변환 연산자 처럼 사용되어 '변환 생성자'라고 불린다.
C++에서 변환생성자는 따로 있는 것이 아니라, parameter 가 하나인 생성자는 모두 변환 생성자가 될 수 있다.



생성자에 explicit 키워드를 선언함으로써 암시적 변환되어 복사-초기화가 되지 않도록 한다.







2015년 9월 23일 수요일

[C++] Conversion

변환 연산자는 컴파일러에 의해 암시적 호출이 가능하므로 사용시 유의해야 한다.

대부분의 해외 개발자들은 변환 연산자보다는 변환 인터페이스 사용을 권장하고 있다.
Android에서는 인터페이스와 바인더 사이의 변환을 하기 위해서 아래의 두 함수를 제공한다.
asBinder, asInterface


foo(c); 코드는 개발자의 의도와 달리, 컴파일, 런타임에 에러 없이 동작하고 있다.
Complex의 int() 연산자 재정의를 함으로써 암시적으로 형변환이 되어 에러가 발생하지 않는다.

예를 들어, 다음 코드는

아래와 같은 단계로 형변환이 되어 동작한다.
int real = c;
int real = int(c);
c.operator int();


하지만 많은 대가 들이 operator 를 이용해서 형변환하지 말것을 강조하고 있다.

위의 형변환 문제는 Android framework에서도 동일하게 나타난다.
Framework에서 service 를 개발할 때, IBinder와 IInterface를 둘 다 상속(다중상속)을 받는데,
IInterface 와 IBinder 간에 형변환이 가능해야 하는 구조를 가지고 있다.
그래서 Android에서는 IInterface 에는 asInterface 를 제공하고, IBinder에는 asBinder를 제공하여
서로의 형변환 처럼 동작하도록 제공한다.

위 코드는 아래와 같이 바뀔 수 있다.








[C++] Type casting


1. static_cast : 이성적인 형 변환을 지원하는 연산자
포인터를 사용하여 변환을 수행할 경우, 일단 static_cast 를 먼저 사용하는 것이 바람직하다.


위 코드가 c로 컴파일 하면 error 발생하지 않고
cpp로 컴파일하면 error가 발생하는 이유는 무엇일까?

malloc() 함수로 반환되는 void* type 을 char*로 casting 하는 것은 사실 C언어에서도 허용되선 안되는 코드이다.
C언어 컴파일러는 개발자의 코드를 100%신뢰하는 것을 전제로 개발되었기에 컴파일 타임 에러를 발생시키진 않는다.
하지만 C++ 는 형변환을 strict 하게 처리한다.
그러므로 개발자가 명시적 형변환을 해야 한다.

하지만, 명시적 형변환은 컴파일러가 무조건 허락해주는데 문제점이 있다.
그러므로, static casting 을 사용해서 위의 문제점을 예방한다.


static_cast 는 이성적인 형 변환만을 진행한다.
malloc 으로 반환되는 void* 는 그 자체로 연산이 불가능하므로,
char* 로의 형 변환은 이성적이라 말할 수 있다.


2. reinterpret_cast : 비이성적인 형변환을 하려는 경우 사용한다.
C언어의 대부분의 형 변환을 지원한다. (마치 C언어의 명시적 형변환 같이 동작한다.)

double 값을 int*로 casting 하듯 down casting이 필요한 경우가 발생한다.
예를 들어, big endian 과 little endian 을 검사하는 경우

c 를 출력할 때 1이 출력되면 little endian으로 볼 수 있다.

하지만 컴파일러는 이성적인 형 변환이 아니기 때문에 에러를 반환한다.
위와 같이 비이성적인 형 변환을 위해서는 reinterpret_cast 를 사용해야 한다.




3. const_cast : 상수 객체를 비상수 객체로 변환할때 사용한다.
const_cast 를 사용했다는 것은 대부분 설계가 잘못되었을 가능성이 높다.


4. dynamic_cast : 안전하게 pointer 와 reference 를 상속관계에 있는 class로 up, down, side casting 을 지원한다.

Reference : dynamic_cast from cpp reference

위 예제서도 볼 수 있듯이 dynamic_cast는 이성적으로 down casting 이 되는 것만 casting 이 되도록 한다.








2015년 9월 21일 월요일

[C++11] Lambda

Lambda
: scope 내에 있는 변수에 접근하여 사용할 수 있는 이름없는 함수 객체(Closure)를 생성하는 방법

먼저 함수 객체가 어떤 개념을 갖는지 살펴보자.



p는 객체인데, 마치 function 처럼 쓰이고 있다.
원리는 p.operator()(1, 2) 를 호출하는 것이다.
괄호()도 재정의가 가능하다!

함수 객체 개념
: () 연산자를 재정의 해서 함수 처럼 사용 가능한 객체(Function operator, Functor)


함수 객체를 왜 사용하는 것일까?

cmp1, cmp2 는 함수 이름은 다르지만 signature 가 같기 때문에
함수 포인터 f1으로 가리킬 수 있다.
하지만 signature 가 같기 때문에 cmp1, cmp2 는 같은 type이라고 할 수 있다.

반면, Less, Greater 의 () 는 모두 같은 signature 를 같지만
Less() 는 Less 만의 typedㅣ고, Greater() 는 Greater 만에 종속된 type 이다.

핵심 1. 일반 함수는 자신만의 타입이 없다.
signature가 동일한 함수는 모두 같은 타입니다.

핵심 2. 함수 객체는 자신만의 타입이 있다.
signature 가 동일해도 모든 함수 객체는 다른 타입이 된다.


inline 사용이 필요한 경우를 살펴보자.

먼저 Sort() 함수를 살펴보자.
우리는 정렬 함수를 구현할 때 비교 함수를 함수 포인터로 받아 상황에 따라 동적으로 바꿀 수 있다.
하지만, 아무리 비교 함수(e.g, cmp1, cmp2) 가 inline 으로 선언되었다 해도
실제로 컴파일을 할 때 inline 치환이 되지 않는다.

cmp 로 올 수 있는 것은 무궁무진하기 때문에 컴파일러 입장에서는 inline 으로 치환 할 수 가 없다.
Sort에 인자로 오는 *cmp 함수 포인터는 절대로 inline 치환 될 수 없다.
결국 inline 을 사용하여 성능 향상을 가져올 수 없다.

반면에, 함수 객체를 사용한 Sort2 함수를 보면
컴파일 입장에서 Sort2에 전달되는 less 는 Less type으로 정확히 알 수 있고,
Less 내부에 () 는 inline 으로 되어 있으므로, inline 으로 치환되어 성능향상을 가져올 수 있다.

하지만 Sort2 는 정책을 바꿀 수 없으므로, Greater type을 받아들일 수 없다.
결국 Template 을 사용하면 정책도 바꿀 수 있도록 해야 한다.

Sort3의 T 는 template 이기 때문에, 전달되는 T type 에 따라서 목적 코드가 커지는 단점이 있다.
하지만 이 Sort3() 함수가 아주 큰 function 은 아니고, 전달되는 T type의 개수가 실제 코드에 아주 많지 않다면 큰 문제가 되지 않는다.




실제로 함수 포인터와 함수 객체를 사용했을 때는 비교하면 꽤나 큰 성능 향상을 가져온다.
간단한 예제 프로그램에서만도 10배 정도의 차이를 나타낸다.

그래서 C++11에서는 이 함수 객체를 더 편하게 사용하기 위해 lambda 를 도입한다.

먼저 () 연산자를 재정의할 때 주의할 점이 있는데,

대부분 함수객체의 () 연산자 함수는 상수 함수로 만드는 것이 좋다.
위 예제의 foo() 함수에서 처럼 상수 객체에서 호출하는 경우가 있기 때문인데,
일반적으로 template 함수 작성시 T type을 인자로 받을 때 값 복사가 이뤄지는 redundant 한 상황을
방지하기 위해 관습적으로 const T& 로 선언하게 된다.
위 예제를 보면 결국 const T& a 가 되는데, a는 상수 객체가 된다.
상수 객체는 상수 함수만을 호출할 수 있으므로 () 연산자 재정의시에 상수 함수로 만들어야 한다.


그럼 이제 Lambda 표현식을 어떻게 사용하는지 살펴보자!

람다 표현식(Lambda Expression)은 함수 객체를 만드는 표현식이다.
[] 는 Lambda Introducer 라고 하며, Lambda가 시작됨을 알리는 표현식이다.

위 sort() 코드는 컴파일러가 아래와 같이 함수 객체를 생성하는 코드로 변경하게 된다.

Closure(클로져)는 람다 표현식을 통해서 컴파일러가 만든 클래스이다.

결국 Lambda 를 사용하는 이유?
함수객체를 사용해서 inline 치환을 하고 성능 향상을 얻기 위해서이다.


Lambda의 특징에 대해 살펴보자.

f1, f2 는 구현이 완전히 똑같은 객체다.
하지만 람다는 함수를 만드는게 아니라 함수 객체를 만드는 것으로
결국 f1, f2 는 다른 타입이 된다.

실제로 f1, f2 의 이름이 다른 것을 RTTI 기법을 사용하여 확인할 수 있다.

결론 : 모든 람다는 다른 타입이다.(함수 객체 이므로..)




위 예제에서와 같이 다시 f1에 함수 객체를 넣으려고 하면,
실제 구현 코드는 똑같아도 위와 다른 타입이기 때문에
f1에 다른 타입을 넣을 수 없어서 컴파일 에러가 난다.
앞으로 f1은 다른 타입을 가리킬 수 없게 된다. 그러므로 inline 치환된다.

Labmda 는 함수 포인터로의 변환도 가능하다.
하지만 f2 는 함수 포인터 변수 이므로, 상수가 아니다.
이후 code에서 다른 함수를 가리킬 수 있으므로,
그러므로 inline 치환되지 않는다.

f3도 다른 function 을 참조할 수 있다. e.g, f3 = &foo;

결국엔 inline 치환되는 것은 오직 f1하나뿐이다.
inline 치환되기 위해서는 컴파일러가 해당 타입이 변경되지 않을 것임을 명확하게 인지되어야 한다.
따라서 아무리 Labmda 표현식을 사용하더라도 그것을 담을 변수가 변경되지 않는 속성을 가져야 한다.

Lambda 에 전달될 수 있는 인자로는 무엇이 좋을지 다시 정리해서 생각해보자.

1. 함수 포인터
- 가능하긴 하지만 함수 포인터는 inline 치환이 되지 않으므로, lambda 를 사용하는 의미가 없어진다.
2. function template class
- 가능하지만 function<> 으로 전달되는 것은 변수이므로 inline 치환되지 않는다.
3. auto
- auto 는 절대 함수 인자로 받을 수 없다.
4. template
- OK!! inline 치환이 된다.

결국, 람다 표현식을 인자로 받으려면 template 을 사용하자!


Lambda에서 return type은 어떻게 표시해야 할까?

f1의 경우 return type이 int 하나로 결정되어 있는 경우, 컴파일러가 자동으로 추론한다.

하지만, f2처럼 -> 를 사용해서 람다의 리턴 타입을 명확하게 표시할 수 도 있다.

f3처럼 if-else 문이 있더라도 반환하는 타입이 동일하기 때문에, 컴파일러가 리턴 타입을 자동으로 추론할 수 있다.

하지만 f4와 같이 2개 이상의 리턴문이 다른 타입을 반환하면
반드시 trailing return 을 표시해야 한다.


Lambda 내부에서 Lambda외부에 선언된 변수에 접근(Capture)하기 위한 방법은 무엇일까?

Lambda 외부에 선언된 변수에 접근하고자 할 때는 [] 안에 해당 변수이름을 전달한다.
또한 해당 block 내의 모든 지역 변수에 접근하고자 할 때는 "="를 전달한다.

실제로 []에 변수 이름을 전달하거나 "="를 전달했을 때 생성되는 코드는 다음과 같다.

결국엔 Closure 객체 안에 멤버 변수로 보관하고 있는 것이다.


Closure에서 변수를 capture하고 있는 원리에 한 단계 더 접근해보자.

v1 = 0; 은 에러를 발생시킨다.
() 연산자를 재정의한 것이 상수 함수이므로, 멤버 변수를 변경할 수 없기 때문에 컴파일 에러가 발생한다.
그렇다면 Closure_Object 내에 멤버 변수에 mutable 선언하면 되지 않을까?


mutable 키워드를 사용하여 mutable lambda 로 선언한다.
지역변수를 캡쳐한 멤버 변수가 mutable 이 된다.

하지만 Closure_Object의 v1 멤버 변수(복사본)이 변경된 것이지
main 함수의 v1 지역변수(원본)이 변경된 것이 아니다.



main 함수의 지역변수(원본)을 변경하려면 mutable 키워드는 삭제하고, & 를 추가한다.

[]에 &를 전달했을 때 컴파일러가 생성하는 Closure 객체는 다음과 같다.



지역 변수가 아닌 class 의 멤버 변수에 접근하기 위해서는 어떻게 해야할까?


단순히 [] 에 data를 전달하면 되지 않을까 생각할 수 있지만,
data 는 지역변수가 아니기 때문에 [data]라고 쓸 수 없다.


Class내에서 내부 어디서든 접근 가능한 키워드를 생각해보면, this 가 있다!
[]에 this를 전달하면 &키워드가 없으므로, 값이 전달된다.
하지만 this 는 포인터이기 때문에 멤버변수 data의 변경이 가능하다.
결국엔 컴파일러에 의해 생성되는 Closure_Object 멤버 변수로 Test* 가 있는 것이다.

또한 또한 this외에도 "=" 로도 가능하다!




인자가 없는 Lambda 를 표현하려고 한다면?

인자가 없는 경우에는 ()를 생략해도 된다.






2015년 9월 17일 목요일

[C++11] Deleted function

Deleted function
: function 을 사용하지 않고 지워버림을 명시적으로 나타내는 syntax.


Mutex class에서 복사생성자가 필요가 있을지 생각해보자.
개발자가 어떤 function 이나 operator 를 삭제하길 원한다면 delete 키워드를 사용하면 된다.
위 예제에서는 복사생성자가 동작하지 않길 원하기 때문에 복사생성자, 그리고 =(대입연산자)를 삭제했다.

그리고 반드시 private 에 쓸 필요는 없지만,
어차피 복사생성자를 안쓰려는게 목적이기 때문에 private 에 쓰는 것이 더 좋다.

delete 사용시 주의할 점은 삭제하려는 function 의 선언시에 사용되어야 한다는 점이다.

선언 이후에 삭제할 수는 없다.


또한 default 생성자를 명시적으로 나타내는 키워드로 default 를 사용할 수 있다.
물론 Mutex() {} 와 같이 사용할 수 있지만,
C++창시자는 Mutex() = default; 라고 쓰는게 더 직관적이지 않냐고 얘기한바 있다.







[C++11] Scoped enum

Scoped enum
: C++98에서 사용되던 기존의 enum과 달리 그것의 이름을 타입으로 가지고 scope 가 정해진 enumerator 이다.

먼저 c++98에서 사용하던 enum 을 살펴보자.


C++11에서 제공하는 scoped enum 을 살펴보자.

scope enum 은 해당 scope 를 통해서만 접근이 가능하고 암시적인 형변환이 불가능하다.
따라서 해당 enum 값에 접근할 때 scope 를 통해서 접근하고,
형변환을 원한다면 statis_cast를 사용할 수도 있다.






[C++11] User define literal

User define literal
: 사용자 정의 literal operator 를 추가할 수 있다.

예제를 통해서 살펴보자.

premitive type 인 float의 값을 표현할 때 접미사로 f 를 사용할 수 있다.
이와 같이 사용자 정의 class 인 Meter 에도 단위를 직관적으로 표현하도록 literal operator 를 정의할 수 있을까?


Meter classd에서 m operator 를 정의하여 user define literal operator 를 사용할 수 있다.
한 가지 주의할 점은 operator 를 정의할 때 전달 받는 인자의 타입이 정해져 있다는 것이다.

정수 literal 은 unsigned long long, const char* 로 받을 수 있으며,
부동소수점 literal 은 long double, const char* 로,
무자열 literal 은 (const char*, size_t) 로,
문자 literal 은 char 로 받을 수 있다.

하지만 사용상의 문제점은 다른 개발자가 3m 를 3 미터가 아니라 3분으로 인식할 수도 있다는 것이다.
그래서 C++ 표준 위원회에서는 왠만하면 사용자 정의 상수를 정의하지 말것을 recommend 하고 있다.

C++ 표준에서 제공하는 literal operator 의 예를 살펴보자.

s를 붙여줌으로 써 const char* 와 string 을 구분하여 전달할 수 있다.








[C++11] Raw string

Raw string
: 문자열을 표현할 때 특수 문자를 \ 기호 없이 쉽게 표현하도록 C++11에서 지원



기존 C++98에서 "\"를 표현하기 위해서 "\\" 두번써야 하는 번거로움이 있다.
e.g, 디렉토리 경로나 정규 표현식 사용시 복잡해진다.

C++11 의 RawString 문법 : R"(문자열)"
결국엔 컴파일러가 "( 로 Raw string 문자열 시작을 인지하고 )" 로 끝을 인지한다.
문자열에 )" 를 표현할 일이 없으면 굳이 사용자 정의 토큰을 추가할 필요 없이 편하게 사용할 수 있다.






2015년 9월 14일 월요일

[C++11] Perfect forwarding

Perfect forwarding
: wrapper function(or class)를 만들 때 인자를 완벽하게 전달하는 기술


위 예제는 wrapFunc()함수에 함수와 인자를 전달하여 전달한 함수에 인자를 완벽하게 전달해보려 시도?하는 예제이다.
물론 전혀 완벽하지 않은 코드이다.


template<typename F, typename T> void wrapFunc(F f, T a) 함수는
a를 값으로 전달 받기 때문에 참조가 아니게 되어 n의 값이 변경되지 않는다.

그렇다면 a를 참조로 받는다면 되지 않을까?

하지만 이 함수의 문제점은 rvalue 의 reference 를 받을 수 없다.
T& a 는 rvalue reference(위 예제에선 wrapFunc(foo, 10); 호출 시 10이 rvalue)가
전달되는 경우를 처리할 수 없다.

그렇다면 rvalue 를 전달받기 위해 const 키워드를 이용한다면 가능하지 않을까?

하지만 역시 const 키워드를 사용하면 goo(int&) 함수를 호출하는데 문제가 된다.(signature가 맞지 않음)

결국 위 예제는 perfect forwarding 이 되지 않는다.
이제 이 문제를 해결해보자!!



해결책 1)
함수 overloading 을 이용해서 const 인자가 있는 것과 없는 function 을 2개 선언한다.

단점 : N개의 인자를 perfect forwarding 시키려면 2^n 개수로 template function을 만들어야 한다.

다른 해결 방법을 보기 전에 참조(Reference)에 대해 잠시 생각해보자.

r1 = r2; 코드는 값이 복사되는 것일까? 참조하는 주소가 복사되는 것일까?
정답은, 값이 복사된다!!

Why?
C++에서 참조라는 개념은, 한번 초기화되면 다시는 다른 곳을 가리킬(참조) 수 없다.
const 와 동일한 느낌이다.
그러므로 r1 = r2; code 에서 r1이 가리키는 참조 주소가 변경되는 것이 아니라(변경될 수 없음),
r2가 참조하는 곳의 값이 r1이 참조하는 곳에 다시 쓰여지는 것이다.

참조와 포인터는 모두 기존 메모리를 가리키게 된다.
하지만 대입 연산시에 차이점이 있다.
참조(Reference) : 값이 이동한다. 값을 꺼낼 때 *를 붙일 필요가 없다. 참조는 자동으로 역참조가 되는 상수 포인터라고 볼 수 있다.
포인터(Pointer) : 참조(주소)가 이동한다. 값을 꺼낼 때 *를 붙여야 한다.

그렇다면 값이 아닌 참조가 이동할 수 있는 참조를 만들 수 있지 않을까?!


해결책 2) 이동 가능한 참조 만들기

C++의 참조는 기존 변수에 이름을 하나 만들어서 메모리 주소를 가리키는데 사용한다.
결국에는 객체 하나 만들어서 그 안에 포인터 두고 그 주소를 가리키게끔 하는 거랑 똑같다.
우리는 그것을 xreference_wrapper class로 만들었다.
그리고 참조는 다른 변수를 가리켜야 한다. 그러기 위해선 포인터가 필요하고..
그 포인터는 T* obj; 이다.

main() 함수에서 r1 = r2; 코드는 값이 아닌 참조가 이동한다.
우리는 xreference_wrapper 클래스에 대입연산자를 정의하지 않았지만,
default 대입 연산자가 동작할 것이고 이것은 포인터가 가리키는 주소를 변경하게 된다.
그렇다면, *(asterisk)없이 값에 접근할 수 있는 code만 추가되면 된다.
그것은 &연산자를 재정의함으로써 가능하다.

또한 &연산자를 재정의함으로써 int& r3 = r1; 코드에서
r1은 객체인데, c++에서 변환 연산자가 동작하므로 결국엔 int* 를 r3에 전달할 수 있게 된다.

Reference(참조)를 class 로 구현해서 값이 아닌 참조가 이동되도록 했다.


xreference_wrapper 를 이용해서 wrapFunc함수를 호출해보자.

위 예제에서

코드는 모두 n의 주소를 보낸다.
r은 은 xrefernce_wrapper 이므로 int& 으로 변환이 가능하다.
따라서 wrapFunc에 전달된 T 는 int& 가 된다.

하지만 &n 은 wrapFunc에 전달된 T가 int* 가 되므로 goo(int&)으로 호출이 불가능하다!!
여전히 문제점이 남아있다.



먼저 이전 예제에서 사용한 xreference_wrapper에 대해 생각해보자.
Class template(클래스 템플릿)은 T의 암시적 추론이 불가능해서 항상 어렵다.
그렇다면 암시적 추론이 가능한 Function template(함수 템플릿)을 사용해 좀 더 쉽게 만들어 보자!

xref 함수는 전달된 T& 를 이용해서 참조를 나타낼 수 있는 xreference_wrapper로 변환하여 반환하는 함수이다.
xreference_wrapper 로 반환될 경우 참조를 전달받는 함수에 전달이 가능하다!


r은 main 함수 내에 계속 쓸것인가? 아니다. wrapFunc에 잠깐 전달하기 위한 용도다.
그러므로 임시객체를 사용하는 것이 더 바람직하다.

하지만 xreference_wrapper 는 class template 이기 때문에, 생성자를 통해서 type을 추론할 수 없다.
그래서 를 항상 써야 한다.
하지만 function template(함수 템플릿)은 전달되는 인자로 type이 추론 가능하다!



결국엔 참조를 보내려면 이렇게 해야 한다.
Function template은 전달된 인자의 타입을 추론할 수 있어서
함수 내부에서 다시 Class template으로 전달할 때는 이미 추론한 type을 전달하기 때문에
Class template에 전달하는데 문제가 없게 된다.

참조를 전달할 수 있는 ref 를 사용하는 예제를 살펴보자.

먼저 C style의 코드부터 살펴보자.

f1 함수 포인터는 인자가 없고 반환도 하지 않는 함수를 가리킬 수 있다.
위 예제에서는 foo 함수를 가리키는데,
f1 함수 포인터는 signature 가 다른 함수를 가리킬 수 없다.

그럼 C++ 에서는 어떻게 할 수 있을까?

f 의 장점은 signature가 다른 goo 함수를 가리키게 할 수도 있다는 것이다.

하지만 goo 함수를 가리키도록 하여 goo함수를 호출하면,
n 의 값이 변경되지 않음을 알 수 있다.

perfect forwarding 이 되지 않았다는 의미이다.



결국엔 ref 를 이용하여 참조가 전달되도록 하면 n 이 변경되는 것을 확인 할 수 있다.



rvalue, lvalue에 대한 예제를 살펴보자.

foo(int&)에 전달된 c 는 const int 이므로 int& 로 전달받을 수 없다.
또한 goo(int)에 전달된 n 은 int& n으로 전달되었기에 error 가 발생한다.

그렇다면 functcion template 으로 바꾼 후에는 어떻게 될까?

foo(c); 을 호출하면 T가 const int 로 결정되므로 문제가 없다.
또한 goo(&n); 을 호출하면 T가 int* 가 되므로 문제가 없다.

이제 &가 하나가 아닌 좀 더 복잡한 경우를 살펴보자.

이전 포스트에서 &은 lvalue reference 이고, && 은 rvalue reference 라고 했었다.
foo(int&&) 함수는 rvalue reference 를 인자로 받는다.
따라서 foo(10) 은 동작하지만, n은 lvalue 이기 때문에 foo(n) 호출 시 문제가 된다.

goo(T&&)함수를 살펴보자.
goo(r) 을 호출할 때 T는 어떻게 될까?
r은 int& 타입인데, 결국에 goo 에 전달된 a 는 int& && 타입을 갖는 것일까?
이렇게 &가 3개 이상 충돌할 경우에는 어떤 reference 타입이 되는지 규칙이 있다.

& && 만날 경우 & 가 되고
&& & 경우 & 가 되며,
&& && 는 && 가 된다.

결국 && && 두개가 만나야 && 가 된다.

이 규칙에 따라서 goo(r)를 호출하면 결국 a 는 int&& 타입이 된다.

정리하자면,
int& : lvalue reference 는 lvalue 타입만 받는다.
int&& : rvalue reference 는 rvalue 타입만 받는다.
T&& : universal reference(forward reference) 라고 부른다.
이 Universal reference 는 lvalue 가 전달되면 lvalue reference 가 되고
(T : int& 되므로, T&& int& && 가 되고 => int &)
rvalue 가 전달되면 rvalue reference 가 된다.
(T : int&& 되므로, T&& 는 int&& && -> int&&)


지금까지 얘기하는 이 모든게 perfect forwarding 을 위해서이다.
타입의 종류와 인자 개수에 제한받지 않고 인자를 전달받기 위해서 template 과 universal reference 를 쓰는 것이다.

그렇다면 universal reference 를 사용하는 간단한 예제를 살펴보자.

wrapFunc(foo, 10); 을 호출하면 10은 rvalue 이므로,
T&& 는 int&& 가 되어 rvalue 를 전달받을 수 있게 되고 foo(int) 함수를 호출하는데 문제가 없다.
또한 wrapFunc(goo, n); 을 호출하면 n 은 lvalue 이므로,
T&& 는 int&가 되어 lvalue 를 전달받을 수 있게 되어 goo(int&) 함수를 호출할 수 있다.



위 예제에서 문제점은 무엇일까?

wrapFunc(foo, 10)을 호출하면 10은 rvalue 이기 때문에 T&& 는 int&& 로 전달된다.
하지만 a는 lvalue 이기 때문에! foo(int&&) 에 전달할 수 없다.

a가 lvalue 인 것이 이해하기 어렵다면 아래 코드를 살펴보자.

위 코드에서 r 은 rvalue 인가? lvalue 인가?
10은 rvalue 이지만 r 은 lvalue 이다.
이전 예제에서도 동일하게 wrapFunc(F f, int&& a) 에서 a는 lvalue 가 되는 것이다.
그리고 이 lvalue 인 a를 foo(int&&)에 전달하니 error가 발생하는 것이다.

그렇다면 casting 을 해서 rvalue 일 때는 rvalue 로, lvalue 라면 lvalue로 전달해주면 되지 않을까?

static_cast<T&&> 는 인자가 rvalue 라면 rvalue 로, lvalue 라면 lvalue로 캐스팅해준다.
rvalue가 오면 static_cast(a);
lvalue 가 오면 static_cast(a); 가 된다.

하지만 좀 더 직관적으로 표현할 수는 없을까?
캐스팅이 아닌 함수를 호출하는 것으로 대신할 수는 없을까?

캐스팅하는 코드를 xforward란 함수를 만들어 감추고 단순 함수 호출로 사용해보자.
하지만 이 코드에는 문제가 있다.
wrapFunc(F f, T&& a)에서 a로 10을 전달하면,
T&& 는 int&&가 되고 a는 lvalue 가 된다.
따라서 xforward의 T&& 는 int& 로 추론된다.
컴파일러가 타입을 암시적으로 추론해버리는 것이다.

결국엔 반드시 컴파일러가 명시적으로 추론하도록 해야 한다.

위 예제에서 만든 xforward는 이미 C++ 표준에서 forward 함수로 제공하고 있다.

그렇다면 이제 타입에 관해서는 해결을 했으니, 여러 개의 인자를 받고 반환해주는 일이 남았다.

일단 여러개의 인자를 전달 받기 위해서는 '...' 를 인자에 붙여준다.
또한 forward 에 전달할 때도 ... 를 붙여주는데 (args) 밖에 써야한다는 점에 주의하자.

return type을 알 수 없으므로, auto 키워드를 사용하여 반환한다.

완벽한 전달자가 되려면,
1. 인자를 Universal(forward) reference 사용
2. 원래 함수를 보낼 때 인자를 다시 forward로 묶어서 전달
3. forward 방식의 명시적 추론을 사용
4. 여러개의 인자를 받기 위해 가변인자 템플릿을 사용

하지만 아직까지도 문제점이 남아 있는데...
반환 타입이 참조일 경우 문제가 생긴다.
만약 int& foo(int a); 라면 어떻게 될까?


그전에 잠시 ... 가 밖에 써야하는 점에 대해 간단한 예제를 통해 살펴보자.

goo(args...);를 호출하면 goo(1, 2, 3); 이 될 것이다.
hoo(args...); 를 호출하면 어떻게 될까?
에러가 발생한다. hoo 함수는 인자가 1개이기 때문이다.

goo(hoo(args)...); 를 호출하면
goo(hoo(args의 1번째), hoo(args의 2번째), hoo(args의 3번째)); 와 같다.

이전 예제에서 ... 를 밖에 써야 하는 이유에 대해 조금은 이해가 됐으리라고 생각한다.

마지막으로 Perfect forwarding 이 적용된 예제를 살펴보자.

Android 에서 singleton 을 구현할 때 사용하는 CRTP 기법에 perfect forwarding 을 적용해보자.

CRTP 기법은 부모가 템플릿인데 자식이 상속 받으면서 자신의 이름을 부모에 템플릿 인자로 전달하는 기법이다.
하지만 반드시 default constructor가 필요하다는 단점이 있다.
perfect forwarding 을 사용하여 이 단점을 해결할 수 있다.







[C++11] Override & const

Override
: 함수를 Override 할 때 정확한 signature 에 맞춰 override 되는지 컴파일 타임에 확인되도록 하는 키워드




위 예제에서 발생하는 실수들이 문제가 되는 이유는
IDE에서 컴파일하는데 문제가 없기 때문이다.
실제로는 잘못된 코드임에도 불구하고 말이다...

그렇다면? 이런 실수를 방지하기 위해서
함수 이름 뒤에 override 키워드를 사용하면 컴파일 타임에 에러를 발생시킨다.



C의 기본 문법 중에 const 키워드를 살펴보자.

const 키워드의 위치에 따라 포인터가 가리키는 곳이 상수인지 포인터의 주소가 상수화되는지 차이가 난다.
더 나아가서 reference(&) 와 결합해서 const int*, int* const 의 reference(&)가 된다.
r2와 r3는 각각 const int*(상수를 가리키는 포인터)의 참조와 int* const(포인터의 주소가 상수)의 참조를 나타낸다.










[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++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 키워드를 사용함으로써 반드시 컴파일 타임에 정해진 상수를 사용하도록 강제할 수 있다.







[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 뿐만 아니라, 생성자도 상속이 가능하다.









[C++11] Delegating constructor

Delegating constructor
: 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() 함수 호출만으로 편리하게 사용할 수 있게 되었다.









[C++11] Variadic arguments

Variadic arguments
: function 에 여러개의 인자를 전달하는 기법



여러개의 인자를 ... 표현으로 class template, function template에 전달할 수 있다.
그리고 template 인자 타입을 표현하는 T 는 가변인자일 경우 Types 로 표현하는 것이 관례이다.

그렇다면 전달된 인자가 몇 개인지 아는 방법은 무엇일까?

'sizeof...' 연산자를 이용해서 가변인자의 개수를 알 수 있다.
'...'이 sizeof 뒤에 붙는 것에 유의하자.

또한 전달받은 인자들을 그대로 다른 function 에 전달할 수도 있다.
'변수이름...' 형태로 전달한다. '...' 이 뒤에 붙는 것에 유의하자.



그렇다면 이제 전달받은 인자를 꺼내서 써봐야 하지 않을까?

가변인자 꺼내기는 방법
1) 첫 번째 인자만큼은 독립된 타입으로 받아야 한다.
2) recursive 를 사용한다.
3) 종료를 위해 인자 없는 함수 제공한다.

Recursive를 사용한다는 의미에 대해서 다시 생각해보자.
위 예제 코드는 실제로 recursive 하게 동작하지는 않는다.

호출되는 foo template 함수는 인자 개숫에 따라 모두 달라지게 된다.
static 으로 선언된 sn 이 호출되는 횟수에 따라서 1, 2, 3 이렇게 증가할 것 같지만
예제를 실행해보면 output 으로 sn 값이 증가하지 않고 항상 1인 것을 볼 수 있다.

매번 같은 foo 함수가 호출되는 것이 아니라는 얘기다.
foo(1, 2.2, 'a'), foo(2.2, 'a'), foo('a'), foo() 가 호출된다.
결국엔 template function 은 컴파일러에 의해 3개의 코드가 생성된다.
매번 새로운 function 이 호출되는 것이다.
결국엔 진짜 recursive 한 logic 은 아니라는 얘기이다.

또한 마지막에 'a' 를 전달할 때는 error가 발생하는데,
args 에 전달할 인자가 없기 때문이다.(T value에 'a'만 전달되고 args에는 아무것도 없다는 얘기)
그래서 인자가 없는 foo() 를 정의한다.








[C++11] begin, end


자료 구조를 순회하여 데이터에 접근하는 방법을 살펴보자.

Container 에 저장된 데이터에 접근하기 위해 대표적으로
Iterator의 begin(), end() function 을 사용한다.

하지만 위 예제에서 show() 함수에 배열이 전달된다면?
배열에는 는 container가 아니므로 begin(), end()를 제공할 iterator가 없어 error가 발생한다.


그렇다면 traits를 사용해서 전달되는 인자가 배열인지, container 인지 판별할 수 있지 않을까?



하지만 좀 더 생각해보면,
애시당초 배열에서도 begin(), end() 함수를 쓸 수 있도록 하면 되지 않을까?

container 의 멤버 함수를 이용하는 것이 아니라
template partial specialization을 이용해
배열, container 를 구분하여 begin(), end() 일반 함수를 제공하는 것이다.

결론적으로, 일반함수 begin(), end() 에 container, array type를 전달하여 호출하면 모두 사용할 수 있다!

하지만 이미 C++11 에 begin(), end()가 정의되어 있다!


좀 더 편한 방법은 없을까?

ranged-for 가 제일 편하다!!


그렇다면 ranged-for 하나로 되는 원리는 무엇일까?

위 예제에서 for(auto n : x) 코드는 컴파일 후 아래와 같이 변경된다.



결국엔 ranged-for 를 쓰나 일반함수 begin(), end() 를 쓰는 것이나 원리는 동일하다.


그렇다면 사용자 정의 class 에 ranged-for 를 사용하려면 어떻게 해야 하는가?

사용자 정의 class에는 begin(), end() 를 정의해야 ranged-for 를 사용할 수 있다.







[C++] meta programing

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