2015년 10월 3일 토요일

[C++] const function

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. 상수 함수는 선택이 아니라 필수이다.
상수 객체는 상수 함수만을 호출할 수 있기 때문이다.







[C++] function object

function object
: 함수 호출 연산자( () )를 재정의한 객체

먼저 간단한 오름차순, 내림차순 정렬 알고리즘을 작성해보자.

일반적으로 라이브러리를 설계할 때 변하는 것(정책)과 변하지 않는 것(알고리즘)을 분리하게 된다.
하지만 여전히 중복 코드가 발생하는 것을 볼 수 있다.
함수 포인터를 이용해서 코드를 수정해보자.


함수 포인터를 이용하는 것은 라이브러리 로써 성능을 떨어뜨리게 된다.
함수 포인터는 결국에 함수를 호출하는 것인데, 함수 호출하는 것 자체가 부담이 되고,
더군다나 sort 에서 이중 for 문에 포함되므로 부담은 배가 된다.

그러므로 함수 포인터 대신에 inline 함수로 대체할 경우 성능에 큰 향상을 가져올 수 있다.

하지만 asc_int, dsc_int 를 inline 으로 대체한다고 해서 sort함수에서 inline으로 대체되는가?
그렇지 않다. sort 내에서 결국에는 cmp 가 함수 포인터이기 때문에 call로 호출된다.(inline으로 치환되지 않는다!)

bool (*cmp)(int, int) 라는 함수의 타입을 컴파일러가 알게 한다면 inline으로 치환될 수 있다.
C++에서는 함수객체를 이용해서, 함수의 타입을 알 수 있다.



함수 객체란 함수 호출 연산자( () )를 재정의한 객체로,
일반 함수와 달리 상태를 가질 수 있다.
add와 sub 가 다른 타입임을 인지하자!


중복 코드가 발생하므로, 탬플릿 이용해서 다시 코드를 변경해보자.


함수 객체, inline, template을 사용해서
중복 코드를 제거하고 성능향상 까지 가져올 수 있다.


STL library 에서 제공하는 sort는 함수 포인터 버전이다.
그렇다면 개발자가 성능 향상을 목적으로 함수 객체를 사용하고 싶다면?
STL 은 class template 을 제공하여 함수 객체를 사용하도록 돕고 있다.

함수 포인터 버전
장점 : 정책을 변경하여도 sort에 대한 함수의 기계어 코드는 하나가 된다.(메모리 최적화)
단점 : 정책이 inline 치환이 적용되지 않는다.

함수 객체 버전
장점 : 정책을 inline을 적용하여 성능을 향상 시킬 수 있다.
단점 : 정책을 변경한 횟수만큼 sort 함수의 기계어 코드가 생성되므로 메모리 사용량이 증가한다.








2015년 10월 1일 목요일

[C++] inline function

inline function
: 함수의 호출부에 함수의 기계어 코드로 치환되는 함수

매크로 함수는 함수의 호출처럼 보이지만 전처리계 과정에서 치환되는 것일 뿐이다.
매크로 함수의 단점으로 단항연산자(++) 을 정상적으로 처리하지 못한다.
인라인 함수는 디버깅 모드나 컴파일러 최적화 옵션을 설정해주지 않으면 동작하지 않는 단점이 있다.

인라인 함수가 동작하지 않는 조건(컴파일러가 인라인 함수를 무시하는 경우)
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, 크기가 매우 큰 함수, 재귀 호출, 함수 포인터 사용 등






[C++] Temporary Object

임시 객체는 이름없이 runtime에 잠시 생성되었다가 사라지는 객체를 말한다.

임시 객체는 개발자가 의도해서 생성할 수도 있지만 대부분 컴파일러의 필요에 의해서 생성된다.

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









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 키워드를 선언함으로써 암시적 변환되어 복사-초기화가 되지 않도록 한다.







[C++] meta programing

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