Coding/Unreal, C++

[C++ Primer Plus] 8. Adventures in Functions

C++ Inline Functions

  • Inline Functions은 프로그램의 속도를 올리기 위한 C++ 기능이다.
  • 일반 함수와 인라인 함수의 차이점은 문법 보다는 컴파일러가 다루는 방법이다.
  • 프로그램을 빌드하고 프로그램을 실행한다. 그러다가 function call에 다다르면 프로그램은 현재까지 수행한 명령 지점을 저장하고 매개변수로 넣은 값들을 stack에 저장한다. 그리고 함수의 실행 지점으로 날라가서 함수를 실행시킨다. 함수가 끝나면 아까 저장한 명령 지점을 불러내서 그쪽으로 돌아간다.
  • 이런 과정들이 함수의 오버헤드다. 즉, 속도가 저하될 수 있는 지점이다.
  • Inline function은 함수 호출 오버헤드를 해결하는 방법이다. 컴파일러가 인라인 함수 호출 시마다 함수 코드 자체를 그곳에 복사하는 방식을 쓴다. 이를 통해 왔다갔다하는 비용을 줄인다.
  • 이 방식을 보면 매크로 함수와 아주 비슷하다는 것을 알 수 있다. 실제로 변환되는 시점 말고는 거의 비슷하게 동작한다. 하지만 인라인 함수는 매크로 함수가 가지는 모호성을 해결해준다. 다음의 경우를 보자.
#define SQUARE(X) X*X

a = SQUARE(5.0); // 5.0*5.0 으로 바뀐다.
b = SQUARE(4.5 + 7.5); // 4.5 + 7.5 * 4.5 + 7.5 로 바뀐다. 예상과 다른 결과값.
c = SQUARE(d++); // d++*d++로 바뀐다. 예상과도 다를 것이고 d++이 두번이나 실행되버린다.
  • 위와 같은 일은 인라인 함수에서 벌어지지 않는다. 인라인 함수는 정말로 함수처럼 동작한다.

Reference Variables

  • C++은 새로운 복합자료형으로 reference를 가지고 있다. reference는 기본적으로 어떤 변수에 대한 별명(alias)이다.
  • 이런 게 왜 필요한가? 이름을 잘못지은게 부끄러울 때? 그럴 수도 있겠지만 reference는 함수의 매개변수로 받아들일 때 주로 사용된다.
  • reference는 무조건 선언과 동시에 초기화를 해주어야 한다.
int rats = 100;
int& rodent = rats;
  • 그리고 가리키는 변수를 바꿀 수도 없다. 이런 두 개 특징은 const pointer와 비슷하다.
int & rodents = rats;
int * const pr = &rats;

References as Functions Parameters

  • 보통 reference는 함수의 매개변수로 쓰인다. 이런 방식을 pass by reference라고 부른다.

Temporary Variables, Reference Arguments, and const

  • 함수가 reference 매개변수를 가질 때, 두가지 경우인 경우 임시변수를 만들어낸다.
    1. 자료형은 정확하나 lvalue가 아닐 때
    2. 자료형이 부정확하나 형변환이 가능할 때
  • 아래 코드를 보고 어떤 경우에 임시변수가 만들어지는지 파악해보자.
double refcube(const double &ra)
{
	return ra * ra * ra;
}

double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = {2.0, 5.0, 10.0, 12.0};

refcube(size);			// ra is side
refcube(lens[2]);		// ra is lens[2]
refcube(rd);			// ra is rd is side
refcube(*pd);			// ra is *pd is side
refcube(edge);			// ra is temporary variable
refcube(7.0);			// ra is temporary variable
refcube(side + 10.0);	// ra is temporary variable
  • 위 코드를 보면 임시 변수가 만들어지든 말든 크게 상관없어 보인다. 문제는 아래와 같은 코드에서 발생한다.
void swapr(int& a, int& b)
{
	int temp;
    
    temp = a;
    a = b;
    b = temp;
}

long a = 3, b = 5;
swapr(a, b); // 임시변수 생성으로 인해 swap이 안된다.

Why Return a Reference?

  • Reference를 반환하는 것은 기존에 값을 반환하는 것과 어떻게 다른지 알아보자.
  • 값을 반환하는 방식은 pass by value와 똑같게 돌아간다. 값을 복사해서 넘겨주는 방식이다.
free_throws& plus_val(free_throws& target, const free_throws & source)
{
	target.attempts += source.attempts;
    return target;
}

free_throws& plus_ref(free_throws& target, const free_throws & source)
{
	target.attempts += source.attempts;
    return target;
}

dup1 = plus_val(a, b); // 복사가 2번 일어난다. 리턴에서 한번, dup1에 넣을 때 한번
dup2 = plus_ref(a, b); // 복사가 1번 일어난다. dup2에 넣을 때 한번
  • 특정 상황에서 reference로 넘겨주면 값 복사를 최적화할 수 있다.
  • 그렇다고 다음처럼 무턱대고 reference를 리턴해주면 안된다.
const free_throws & clone2(free_throws & ft)
{
	free_throws newguy; // 문제의 시작
    newguy = ft;
    return newguy;
}
  • 이 때, newguy는 함수 호출이 끝남과 동시에 사라진다. scope를 벗어났기 때문이다.

Why Use const with a Reference Return?

plus_ref(dup, five) = four;
  • 왜 위와 같은 expression이 가능할까? Assignment의 왼쪽에는 modifiable lvalue가 필요하다. 근데 plus_ref가 넘겨주는 것이 그것에 해당하기 때문이다.
  • 보통 함수는 rvalue를 넘겨줄 것이라고 기대하기 때문에 위와 같은 식이 불가능하도록 하기 위해 const를 넣을 수 있다.

Default Arguments

  • default arguments는 특정 매개변수를 빠트려도 기본값으로 사용할 수 있게 해준다.
  • default arguments를 쓰기 시작한 매개변수부터 오른쪽 끝까지 모두 default argument를 사용해야 한다.
int harpo(int n, int m = 4, int j = 5); // VALID
int chico(int n, int m = 6, int j); // INVALID
int groucho(int k = 1, int m = 2, int n = 3); // VALID

Function Overloading

  • Function polymorphism 또는 Function Overloading은 C++의 주요 기능 중 하나다.
  • Function Overloading이 작동하는 방식은 function signature, 즉 function's argument list에 따라 결정된다.
  • 문제는 함수 오버로딩의 모호함에서 발생한다. 다음 코드를 보자.
void print(const char * str, int width); #1
void print(double, int width); #2
void print(long l, int width); #3
void print(int i, int width); #4
void print(const char *str); #5

unsigned int year = 3210;
print(year, 6); // ambiguous call
  • 먼저 #1, #5는 자료형이 다르므로 불가능하다. 문제는 #2, #3, #4가 모두 가능하다는 것이다. 이 경우에 C++은 에러라고 판단한다.
  • 어쨌든 Function Overloading은 이해하기 쉽고 쓰기도 좋아보인다. 하지만 남용하는 것은 금물이다. 기본적으로 코드의 길이를 길게 하고 가독성도 Default Argument에 비해 좋지 않기 때문이다. 자료형 자체가 달라서 써야하는 것이 불가피할 때에만 쓰자.

Function Templates

  • Function Template를 통해 특정 타입에서 벗어난 함수를 작성할 수 있다. 해당 함수를 호출할 때에는 어떤 타입인지 명시해서 컴파일러가 해당 타입을 사용하는 함수를 생성하도록 한다. 이런 과정을 generic programming이라고 한다. 또한 타입을 매개변수처럼 넘겨준다고 해서 parameterized types라고도 부른다.
  • 기본적인 문법은 다음과 같다.
template <typename AnyType>
void Swap(AnyType &a, AnyType &b)
{
	AnyType temp;
    temp = a;
    a = b;
    b = temp;
}
  • 이 때, template와 typename은 의무적인 키워드다. 단, typename은 class로 바꿔 쓸 수 있다. 이렇게 바꾸면 뭔가 크게 달라질 것 같은데, 적어도 template 문법에서는 완전히 동일하다.

Overloaded Templates

  • 함수 오버로딩과 비슷한 방식으로 템플릿 함수에도 오버로딩을 적용할 수 있다. 오버로딩이니까 당연히 함수 시그니쳐가 달라야한다.하지만 그게 불가능한 경우가 대다수다. 크게 기본 자료형, 포인터, 레퍼런스, 클래스 등이 들어올텐데, 이에 대해서 다 같은 알고리즘을 적용하는 것은 불가능하거나 비효율적이다. 이를 해결하기 위해 Overloaded Template을 사용할 수 있다.
template <typename T>
void Swap(T &a, T &b)
{
	T temp;
    temp = a;
    a = b;
    b = temp;
}

template <typename T>
void Swap(T a[], T b[], int n)
{
	T temp;
    for (int i = 0; i < N; i++)
    {
    	temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

Template Limitations

template <class T>
void f(T a, T b)
{
	...
}
  • 위 함수에서 우리는 a = b, a > b, T c = a*b 와 같은 표현식을 모든 타입에 대해 장담하고 쓸 수 있을까? 그건 불가능하다. 애초에 많은 클래스들이 a > b와 같은 비교 연산을 지원하지 않을 염려가 높다.
  • 이를 위해 사용할 수 있는 방법 중 하나가 Specialization이다.

Explicit Specialization

  • 다음과 같은 structure를 가지고 있다고 생각해보자.
template <class T>
void swap(T &a, T &b)
{
	T temp;
    temp = a;
    a = b;
    b = temp;
}

struct job
{
	char name[40];
    double salary;
    int floor;
};
  • swap은 job이 들어왔을 때도 잘 작동할 것이다. 그런데, 만약에 job인 경우엔 name 멤버를 바꾸고 싶지 않다면 어떡할까? 이 경우엔 template overloading을 쓸 수 없다. 함수 시그니쳐가 같기 때문이다. 이 때 쓰는 것이 Explicit Specialization이다.
  • C++98에서는 관련해서 다음과 같이 서술한다.
    •  각 함수 이름마다 non template function, template function, explicit specialization template function, 그리고 각자에 대한 overloading function을 가질 수 있다.
    • Explicit specialization template는 프로토타입과 정의 부분에서 template <>를 맨 앞에 둬야 한다. 그리고 함수 이름 뒤에 있는 <> 안에 Specialized type을 넣어야 한다.
template <typename T>
void Swap(T &, T &);

// job type에 대한 explicit specialzation
template <>
void Swap<job>(job &, job &);

Instantiations and Specializations

  • Instantiation과 Specialiazation을 제대로 구분해야 template를 이해할 수 있다.
  • int i, j를 활용해서 Swap(i, j)를 쓰면 어떻게 동작할까? 이렇게 되면 int를 활용한 specific instantiation이 일어난다. 이렇게 function instantiation을 하는 것을 implicit instantiation이라 부른다. 원래 C++은 implicit instantiation만 지원했지만 98 이후부터 explicit을 지원한다. explicit instantiation은 다음과 같이 사용한다.
template void Swap<int>(int &, int &); // explicit instantiation

template<> void Swap<int>(int &, int &); // explicit specialization
template<> void Swap(int &, int &); // explicit specialization
  • 보다시피 explicit instantiation과 explicit specialization은 유사한 문법을 가지고 있다. 함수 정의 전에 <>가 있냐 없냐 정도다.
  • 다시 정리해보자. template을 쓰는 모든 함수는 funtion instantiation된 구체적인 함수다. instantiation를 할 수 있는 방법은 implicit way와 explicit way가 있다.
template <class T>
void Swap (T &, T &); // template prototype

template<> void Swap<job> (job&, job&); // explicit specialization

int main(void)
{
	template void Swap<char>char &, char &); // explicit instantiation
    
    short a, b;
    Swap(a, b); // use implicit instantiation for short
    
    job n, m;
    Swap(n, m); // use explicit specialization for job struct
    
    char g, h;
    Swap(g, h); // use explicit instantiation for char
}
  • implicit instantiation과 explicit instantiation, explicit specialization 세 개를 묶어 Specialization이라고 부르기도 한다. (참내..)

Why use explicit instantiation?

  • explicit specialization과 달리 explicit instantiation은 그 용도를 한번에 이해하기 힘들다. 앞으로 쓸 function instance를 가독성 좋게 앞에서 알려주는 것에 불과한 것일까?
  • 마이크로소프트 문서가 답을 알려준다.
You can use explicit instantiation to create an instantiation of a templated class or function without actually using it in your code. Because 
this is useful when you are creating library (.lib) files that use templates
 for distribution, uninstantiated template definitions are not put into object (.obj) files.
  • 앞에서 template은 함수 객체가 아니라고 했다. prototype이다. template 그 자체는 컴파일되지 않는다. 즉, obj 파일에 들어가지 않는다. 이러면 빌드된 라이브러리를 배포해야할 때 문제가 생긴다. 이를 위해 explicit instantiation을 사용해 사용해야 하는 template function을 instantiation되게끔 해놓을 수 있다.

Which Function Version Does the Complier Pick?

  • 함수 이름 하나당 non template function, templation function, template specialization function과 각기 overloading이 올 수 있다. 이렇게 다양성을 제공하는 만큼 C++은 어떤 버전을 써야하는지 정확한 전략을 가지고 있어야 한다. 이를 overload resolution이라고 한다.
    1. 먼저 같은 이름을 가진 모든 함수들을 불러모은다.
    2. 함수 중에서 함수 시그니쳐가 동일한 것(이 때, 형변환이 가능한 것도 동일하다고 판단한다)들을 찾는다.
    3. 이 중에서 순위를 매긴다.
      1. 정확히 똑같은 함수 시그니쳐. 이 때, non template function이 template function보다 우위다.
      2. Conversion by promotion (char -> short -> int 또는 float -> double)
      3. Conversion by standard conversion (int -> char 또는 long -> double)
      4. User-defined conversions

Exact Matches and Best Matches

  • C++은 exact match를 판단할 때, 'trivial conversion'을 실행한다. 아래 표를 보자.
From an Actual Argument To a Formal Argument
Type Type &
Type & Type
Type [] * Type
Type (argument-list) Type (*) (argument-list)
Type const Type
Type volatile Type
Type * const Type *
Type * volatile Type *
  • 표만 봐서는 이해가 쉽지 않은데, 예제를 보면 바로 와닿는다.
struct blot (int a; char b[10]; };
blot ink = {25, "spots"};

void recycle(blot);			#1
void recycle(const blot);	#2
void recycle(blot &);		#3
void recycle(const blot &);	#4

recycle(ink); // 이 함수는 몇번 함수에 가장 맞을까?
  • C++은 1~4번 모두 정확히 같다고 판단한다. 이 경우에 컴파일러는 ambiguous라는 이유로 에러 메세지를 준다.
  • template에서 most specialization은 conversion을 가장 적게 해도 되는 경우를 일컫는다.
template <class Type> void recycle (Type t); // 1
template <class Type> void recycle (Type* t);// 2

blot ink = {25, "spots"};
recycle(&ink); // 1? 2?
  • 위 경우에 1번 2번 모두 가능하다. 하지만 Type Deduction에서 형변환이 적은 2번이 선택된다.

Template Function Evolution

  • Template Function은 충분히 유연해보이지만 모호한 상황이 연출될 수 있다. 다음의 예시를 보자.
template <class T1, class T2>
void ft(T1 x, T2 y)
{
	...
    ?type? xpy = x + y;
    ...
}
  • x와 y를 더하는 단순한 템플릿 함수다. 문제는 두 자료형이 다를 때, xpy의 자료형은 무엇이 되어야 할까? 여기에 무턱대고 T1으로 한다고 하면 함수가 이상하게 작동하게 된다. ft(1, 2.5) != ft(2.5, 1)가 되어버린다.

The decltype Keyword

  • C++11의 해결책은 decltype이다. type을 컴파일 시점에 결정할 수 있다.
template<typename T1, typename T2>
void ft(T1 x, T2 y)
{
	...
	decltype(x + y) xpy;
    xpy = x + y;
    ...
}
  • decltype은 type을 추정하기 위해 생각보다 복잡한 단계를 거친다.
    1. expression이 괄호를 가지고 있지 않으면 해당 expression의 결과 type이다.
    2. expression이 함수 호출이라면 함수의 결과 type이다.
    3. expression이 괄호를 가지고 있고 lvalue라면 reference 타입이다.
double xx = 4.4;
decltype((xx)) r2 = xx; // r2 is double & (stage 3)
decltype(xx) r3 = xx; // r3 is double (stage 1)
  • 여기까지만하면 decltype이 해결하지 못하는 문제가 생긴다.
template<class T1, class T2>
?type? gt(T1 x, T2 y)
{
	return x + y;
}
  • scope 문제로 인해 decltype을 gt()안에 쓸 수는 없다. 이를 해결하려면 C++11의 기능을 몇개 더 가져와야 한다.
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
	return x + y;
}

References

 

Explicit template instantiation - when is it used?

After few weeks break, I'm trying to expand and extend my knowlege of templates with the book Templates – The Complete Guide by David Vandevoorde and Nicolai M. Josuttis, and what I'm trying to

stackoverflow.com

  • C++ Primer Plus 6th, Chapter 8