Chapter 8. Adventures in Functions

지환·2022년 6월 24일
0

C++ Inline Functions

일단 함수 호출시 내부적으로 어떤 일이 일어나는지 보자.
compilation 이후 executable program이 나온다.
이 """executable program은 machine language instruction으로 이루어져 있다."""
우리가 프로그램을 실행하면 이 machine instruction이 computer memory에 올라간다.
따라서 각 instruction이 특정 memory 주소를 가진다.
차례대로 instruction이 실행되다가 if나 loop를 만나면 다른 곳(다른 주소)으로 점프하게 된다.
함수 호출도 마찬가지인데, 일단 function call instruction을 만나면 해당 instruction 뒤의 주소를 저장해둔다.
그리고 함수 argument를 stack에 복사하고, 해당 함수 instruction의 시작주소로 가서 실행한다.
(마지막으로 아마 return value는 register에 저장하고) 아까 저장해둔 다음 instruction 위치로 온다.

이렇게 앞뒤로 왔다갔다하는 정보를 유지하려면 overhead가 발생한다.
inline function을 사용하면, comiled code에서 함수가 다른 코드 내에 작성된다.
compiler가 함수 호출을 함수 코드로 대체하는 것이다.
이러면 앞뒤로 jump할 필요도 없고 따라서 overhead는 줄지만, memory 측면에선 좋지 않다.

함수 호출 매커니즘에 걸리는 시간과 함수 실행 시간을 잘 보고 판단해야한다.
요즘은 빠르게 잘 처리돼서, 함수가 많이 호출되지 않는 한 시간 절약이 그리 크지 않을 수도 있다.
compiler가 요청을 거부할 수도 있다.

inline function으로 만들려면,
1. function declaration에 inline keyword를 앞에 붙이던가,
2. function definition에 inline keyword를 앞에 붙이던가
둘 중 하나를 무조건 해야한다.
가장 흔한 방법inline을 붙인 함수 definition을 prototype이 와야할 위치에 적는 것이다.

inline 함수를 사용하려면 꼭 그 파일에 definiton이 있어야 한다. 그러므로 inline 함수는 주로 헤더에 정의해두고 include해가며 사용한다.(p.517)
(inline function은 const varaible과 같이 special linkage property를 가지므로 헤더에 막 넣어서 써도 괜찮다고했음. 뭐 special이래봤자 그냥 internal linkage겠다만)

ex)
inline double square(double x) {return x * x; }
한줄로 적는것이 의무는 아니지만, identifier들이 길고, 함수가 두줄 이상으로 적힌다면 inline 함수로 대체되지 않을 가능성이 있다.

macro function VS inline function
macro 함수는 pass by value가 아니라서 c++ 같은 side effect가진게 인자로 들어오면 취약하다.
따라서 macro 함수보단 inline 함수를 추천

Reference Variables

reference variable도 마찬가지로 compound type.
"reference"alternative name이란 뜻이다.
함수의 formal argument(parameter)로 쓰는 것이 reference variable의 주된 목적이다.
포인터의 대안으로써 큰 structure를 함수에서 다루거나, class designing하는데 쓰인다.

기본 개념

(기본적인 개념/사용법만 보는 것, 주로 이렇게 사용되지는 않음)

& symbol을 사용한다.

int rats;
int & rodents = rats; //이제 rodents는 rats의 alias이다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
int *p = &rats;
int & rodents = *p;  //이런 식으로도 가능하다.

포인터 선언할때 *이 앞에 붙어서 "pointer to type X"로 선언됐듯이,
&가 앞에 붙어서 "reference to int"가 된 것이다.
여기서 &는 주소연산자가 아니다.

reference variable은 pointer variable과 완전히 구분된다.
reference variable은 포인터처럼 주소를 저장해서 가리키는 것이 아니라 아예 특정 저장공간의 이름 그 자체가 될 수 잇는 것이다.
즉, reference variable은 같은 type의 저장공간이면 어디든 그 공간의 이름이 될 수 있다. 그림에 3을 저장한 저장공간은 이름이 두개인 것이다.

Requirement
1. 선언할때 initialize 해야한다.
2. 한번 초기화 해두면, 다른 값을 assign 할 수 없다. (하면 다른 의미로 해석된다. 아래 참고.)
(어찌보면 const랑 비슷)

2번에서 다른 값을 assign 하려고해도, 그 공간의 이름이 되도록 지정되는게 아니라, 값만 assign될 뿐이다.

int rats = 101; int &rodents = rats; int bunnies = 50;
rodents = bunnies;  //reference를 바꿀 수 있나? No. rodents(rats) 변수에 50 할당

References as Function Parameters

(이렇게 매개변수에서 많이 쓰인다.)
C++에선 passing by reference를 통해 called function에서 calling function의 variable에 접근할 수 있다.
(C에선 무조~~~건 pass by value였음)

void swapp(int * p, int * q);
void swapr(int & a, int & b);

어떤걸 쓰든, 기존 값에 접근해서 swap 가능.
전자는 함수 인자 넘겨줄때 주소연산(&)이 들어가서 알아보기 쉽지만,
후자를 일반적인 pass by value 함수랑 구분하려면 prototype이나 definition을 봐야한다.

reference varaible은 선언할때 초기화돼야한다고 했는데,
이렇게 함수 parameter로 있으면 함수 호출될때 선언과 동시에 argument로 초기화되는 꼴이다.

Temporary Variables, Reference Arguments and const

당연히 변수라던가하는 어떤 저장공간이 표현돼있어야지만 argument로 넘겨줄 수 있다.
즉, refcube(a+3.0); -> 이런 exrpession은 당연히 INVALID
하지만 프로그램이 temporary variable(이름없는 변수)을 만들어서 해당 expression의 값을 그 변수에 assign하고, 최종적으로 reference variable이 그 temporary variable의 reference가 되도록 하기도 한다.
특정 case에서 이런게 가능한데, 어떤 경우인지 한번 보자.

reference parameter가 const이면서 아래 두 경우 중 하나인 경우,
temprorary variable이 생성된다.(이후 reference 변수가 그 임시 변수를 지정하게되는 것)
1. actual argument가 type은 일치하지만, lvalue 가 아닌 경우
2. actual argument가 type은 일치하지 않지만, 맞는 type으로 변환될 수 있는 경우

lvalue 란?
data object that can be referenced by address
원래 C에서 lvalue는 assignment 왼쪽에 온다고해서 그렇게 이름지었다.
이 개념이 const 이전에 나와서 그런데, const 변수가 assignmetn에는 오지못하지만 lvalue이다.
일반 변수를 modifiable lavalue, const 변수를 non-modifiable lvalue 라고 한다.

(이렇게 만들어진 임시변수는 함수 호출 duration 동안만 지속된다.)

ex)
double refcube(const double &ra) { return ra * ra * ra; }: const로 선언됐다고 하자
refcube(7.0);: ra는 temporary variable이다.
refcube(side + 10.0);: ra는 temporary variable이다. (side는 double 변수)

왜 const reference parameter만 가능할까???
잘 생각해보면 애초에 temporary file을 만든다는 것 자체가 reference variable을 사용하는 의미를 퇴색시킨다.
원래 값에 접근도 못하고, 다른 변수가 만들어지는 것이니 시간/공간 절약도 안된다. 일반적인 passed by value랑 다를게 없다.
temporary가 만들어지면 원래 값에 접근을 못하니 그걸 변경하려는 시도가 무의미하다. 그렇기 때문에 애초에 그런 상황 자체를 막은 것이다.
C++ standard 에선 const reference parameter인 경우에만 허용함으로써 값을 변경하지 않는 경우에 대해서만으로 제한하는 것이다.
(여전히 몇몇 compiler에선 warning만 띄울 수 있으니 잘 봐야함)

값 변경하지 않는 경우 등 const를 사용할 수 있으면 사용해주면 좋다.
non-const와 const 둘 다 받을 수 있고, 값도 보호되고.. 장점이 좀 있음.

rvalue reference

library designers가 특정 operation을 더 효율적으로 구현할 수 있도록 돕도록 하는 rvalue reference도 있다.
그래서 위해서 한 애들은 lvalue reference 라고 함.
자세한건 ch18...

Using References with a Structure/Class Object

structures나 class object에도 물론 잘 적용된다. 원래 기본 type보단 이런 애들 때문에 만들어진 것.

원래 값을 수정할 계획이 없더라도 큰 structure나 class object는 reference parameter를 이용하는게 좋다.
값이 모두 복사되지 않기때문에 훨씬 효율적이다.
값 변경 안할거면 constant reference로 사용(const string & a)하면 됨.

non-const도 받아들일 수 있고, 좀 안맞아도 temporary variable만드니까 괜춘
(애초에 const reference 아니면 temporary는 안 만들수도있으니..)
따라서 이렇게 하는게 좋음 습관

string version1(const string & s1); 함수에 대해 version1("123");으로 호출해도 괜찮다.
왜??
: string object에 C-style string을 assign할 수 있는건 앞에서 봐서 알고있다. 그게 어떻게 가능하냐면 string class에 char*-to-string 변환 기능이 있기 때문이다.
이게 첫번째 이유이고, 두번째는 바로 위에서 봤듯이 (const reference parameter인 경우) type이 맞지 않을때 해당 type으로 변환가능하면 temporary가 만들어진다.
이것도 그런 경우이다. 그래서 저런 parameter에는 C-style string을 얼마든지 넘겨줄 수 있는 것이다.

reference도 어떻게 구현됐을까? 잘 생각해보면 결국 pointer를 이용했을 것.
assembly(or C)에선 reference 같은 개념따윈 없기 때문이다.
그러니 기본 built-in type은 reference(pointer) parameter보다
그냥 값으로 받는게 더 효율적일 수 있다.
pointer보다 본인 type 크기가 더 작을 수 있기때문이다. 물론 값 변경을 해야하는 경우는 예외.

Return a reference

free & accum(free & target, const free & source); 이렇게 reference variable을 rturn할 수도 있다.
prototype을 봐야 reference return하는지 알 수 있는 경우가 대부분
ex) return a; prototype봐야지 reference반환인지 일반 변수 copy하는건지 알 수 있음

storage duration 잘 고려해야함. (pointer 다룰때도 마찬가지)
그러기위해, 인자로 넘겨받은 reference 변수를 다시 반환하거나, new로 stack이 아닌 heap에 할당받아서 반환하는 방법이 있다. static으로 선언한 변수 반환하는 것도 한 방법일듯.
(p.400에 예제 코드 나오는데 잘못된 것 같다. 찾아보니 여기서도 틀렸다고 함)

일반적인 return은, "임시저장소"에 return 값을 copy하고 그것을 사용하는 형태이다.
하지만 reference를 return하면, 굳이 어딘가에 임시로 저장하고 그걸 활용하는게 아니라 곧바로 전달된다.
parameter에서처럼 return할때도 reference를 사용하면 시/공간이 절약됨.

reference가 return되면, 이는 lvalue이기 때문에 assign이 될 가능성이 있다.
이를 막고싶다면 return type을 const로 지정한다.
const free & accum(free & target, const free & source);
(근데 이렇게하면 이 return 값을 바로 다른 함수에 넘겨줄때, 그 함수가 non-const를 매개변수로 받는 경우 문제가 됨. const 여부 잘 따지며 하도록)

Guideline

pointer와 reference는 interface의 차이뿐이다. reference가 좀 덜 복잡하게 보이는 것 뿐이다.
"(1)original value 수정", "(2)시간/공간 절약"이라는 사용하는 이유도 둘이 같다.

가이드 라인

수정 않는 경우,

  • data object가 built-in data type이나 small structure 같이 작은 경우라면, pass it by value
  • array라면 애초에 pointer밖에 없다, pointer to const
    링크: 애초에 array는 포인터로 넘겼었다.
    reference도 되긴 하지만, 포인터를 쓰지 보통은,, 크기 충돌할 수 있으니,, 아니면 링크에서처럼 tmeplate 써야됨
  • 꽤 큰 structure라면, const pointer OR const reference
  • class object라면, const reference
class design의 의미자체가 종종 reference를 사용하기를 요구한다.
그래서 C++에서 reference란 것을 추가한 것이다.
""class object argument 넘기는 standard way는 reference""이다.

수정할 경우,

  • built-in data type이라면, pointer
  • array라면 애초에 pointer밖에 없다, pointer
  • structure라면, pointer OR reference
  • class object라면, reference
말 그대로 가이드 라인일 뿐이다. 상황에 맞게 하도록.
ex) cin에 넘겨줄 data는 수정되야하는 built-in type이지만, cin 자체가 reference parameter라서
`cin >> &n;`으로 적는게 아니라, `cin >> n;`으로 적는다.

추가 Object 강의: Objects, Inheritance, References

(ch6에서 봤듯) ostream object와 ofstream object는 동일한 methods를 사용할 수 있었다.
이렇게 한 class의 특징을 다른 class로 pass하는 것inheritance라 한다.
ostream 역할을 base class, ofstream 역할을 derived class 라고 한다.

derived class object를 base class reference에 typecast 없이 assign할 수 있다.
(reference라서 assign이란 표현이 좀 그렇긴한데 뜻 표현하려고,,, 본문에선 refer to 라는 표현 사용)
가장 많이 쓰이는 예시는, base class reference parameter를 가지는 함수인데, 이 parameter에는 base class object는 당연히 넘겨줄 수 있고, derived class obejct도 넘겨줄 수 있다.
('ostream &' parameter에는 ostream object나 ofstream object나 둘 다 넘겨줄 수 있다.)

ios_base::fmtflags : cout 같은 object의 formatting setting 정보를 저장하기 위한 type이다.
setf 함수는 이런 formatting setting 정보를 반환하므로, 이걸 저장해뒀다가 나중에 복구할때 사용 가능


Default Arguments

default argument란, 함수 호출시 대응하는 actual argument를 빼먹었을때 자동으로 쓰이는 값이다.
int sum(int *sum, int a, int b = 0);
이렇게 prototype에만 작성한다. definition은 원래대로 이런거 없이 작성한다.
b 위치에 argument를 작성하면 0이 overwrite되고, 작성하지 않으면 오류 뜨지 않고 기본값인 0으로 된다.

Restrictions
default 값은 우->좌로 작성해야한다.
int chico(int n, int m = 6, int j); : INVALID
int chaco(int n, int m = 6, int j = 7); : VALID

actual argument는 대응하는 formal argument와 좌->우로 assign된다. 중간 argument를 skip할 수 없다.
b = chaco(2, , 5); : INVALID

p.411 programming note, new로 할당받을때 주의해야할 점
: 사용자 입력에만 의존하면 크기 너무 크게 받을 수도 있으니,
max 값이 어떤 경우인지 잘 고려해서 할당받을 수 있도록.

library 함수 호출로 프로그램 커지고 느려지고 하는 경우,
간단하게 반복문 등으로 필요한 기능만 구현해서 해결할 수도 있음.

Function Overloading

Function Overloading (Function Polymorphism)을 통해 같은 이름을 여러 함수가 사용할 수 있다.
(polymorphism이란 말은 여러 형태를 가지고 있단 뜻)
이를 통해 본질적으로 같은 기능을 하지만, argument lists만 다른 함수들같은 이름으로 정의할 수 있다.

> Function Signature
function's argument list이다.
같은 type이 같은 순서로 같은 개수로 있다면, 두 함수는 같은 signature를 가졌다고 한다.(이름은 상관 X/ type, 순서, 개수가 signature을 가르는 요인)

> Function Overloading
C++은 같은 이름이지만 다른 signature인 함수를 정의하도록 해주는 것이다.
(이름 같고 signature도 같은 함수는 당연히 허용 안됨)
return type이 아닌 signature로 overloading 가능 여부 결정(return type은 같든 다르든 상관 X)
함수를 호출하면, 같은 이름의 여러 함수 중 같은 signature을 가지는 함수가 호출되는 방식이다.

C에서는 이 기능을 지원하지 않으므로 void* 같은걸로 구현해야함.
(근데 이러면 argument가 무조건 pointer여야하긴 함)
참고: https://www.geeksforgeeks.org/does-c-support-function-overloading/
아니면 뭐 macro 함수로도 어느 정도 가능은 할듯... void*처럼 type 신경 안쓰니까

C++에선 C와는 다른 name mangling이란 것으로 함수 오버로딩을 지원함.
즉, 함수 signature가 다르면 그냥 다른 identifier로 인식되도록 해버리는 것.

Restrictions

overloaded된 함수들 중 어떤 함수를 호출할지는, signature을 보고 맞는걸 찾는 것인데,
모호한 arguments를 넘기면 convert 되는게 아니라 error를 띄운다.

ex)
void print(int);  //#1
void print(long); //#2
. . .
print((short) 1);

#1과 #2중 어떤 함수를 사용해야하나?
함수가 하나 뿐이라면 변환되겠지만, 선택지가 두가지나 되므로 이 경우 error

function signature을 확인할때는 reference type과 해당하는 일반 type을 같다고 간주한다.
compiler 입장에서 reference parameter와 일반 parameter는 함수 호출문만 보고서는 어느 경우인지 구분하지 못하기 때문이다.

ex)
void print(int);  //#1
void print(int &); //#2
. . .
int a;
print(a);

#1과 #2중 어떤 함수를 사용해야하나? 위나 얘나 둘다 문제임.
이런 경우는 처음부터 허용되지 않음

constnon-const variable은 구별한다.
예를들어 특정 함수의 parameter가 const인 버전과 non const인 버전이 둘 다 있다면,
const actual argument는 const를 가지는 함수를, non-const actual argument는 non-const를 가지는 함수를 호출
-> 변수가 pointer이거나 reference인 경우에만 해당되는 듯(p.433)

Reference paramter로 overloading할때도 이렇게 여러 경우를 다 포함하는 함수가 생길 수 있는데,
마찬가지로 더 정확한 함수를 사용(호출)한다.

ex)
void stove(double &);       //#1
void stove(const double &); //#2
void stove(double &&);      //#3, rvalue reference는 rvalue에 매칭됨.
. . .

이 경우 non-const이든 const이든 rvalue이든 전부 다 #2 함수의 인자로 들어갈 수 있지만,
non-const argument는 #1, const argument는 #2, rvalue는 #3을 호출한다.

자세한 rule은 아래 "Compiler는 뭘 고르나?" 참고

Function Overloading 언제 사용?

남용해선 안된다.
근본적으로 같은 기능을 하지만, 다른 형태의 data를 다룰때 사용하는 것이다.

예를들어, 다음 함수를 보자.

char * left(const char * str, unsigned n);  //n이 들어오면 str에서 n번째 문자까지 반환
char * left(const char * str);              //n이 없으면 str에서 1번째 문자까지 반환

이런 경우는 굳이 overload 안하고, default argument로도 충분히 구현할 수 있으니 그렇게 해야한다.
그렇게하면 함수를 하나만 적으면 되고, 따라서 memory 공간도 절약되고, 나중에 수정하기도 간편하다.
다른 type의 argument인 경우에는 어쩔 수 없이 function overloading을 써야한다.

※ C++은 overloaded function을 어떻게 구분하는 것일까?
: 각 함수에 secret identity를 부여함으로써 구분한다.
compile할때 compiler가 "name decoartion(name mangling)"이란 것을 한다.
함수의 formal parameter를 기준으로 함수 이름을 암호화(?)하는 것이다.
예를들어, `long MyFunc(int, float);`은 `?MyFunc@@YAXH` 처럼 컴파일러가 내부 표현법으로 바꾸는 것이다.
다른 signature이면, 다른 symbol이 추가되게 된다. 이를 통해 overloaded function을 구분한다.
decorating 방법은 compiler마다 다를 수 있다.

Function Templates

Function Templategeneric type 측면에서 함수를 정의한다.
그렇기때문에 Generic Programming이라 불리기도 한다.
type들이 parameter에서 표현되기때문에 template feature을 parameterized type이라고 하기도 한다.
특정 type을 template에 parameter로 넘김으로써 compiler가 해당 type의 함수를 발생시키게 한다.

특정 함수가 여러 type에 대해 작동하도록 하려면, overloading을 해야한다.
하지만 손으로 하나씩 하면 오류 가능성도 있고 문제가 발생할 수 있다.
이럴땐 template을 사용해주면 좋다.
(signature에서 개수가 달라버리면 그냥 overloading해야됨)

> 문법

template <typename AnyType>  //template을 만든다는걸 알려주는 구문
void Swap(AnyType &a, AnyType &b);  //값 변경해야되니 reference로 받음

template <typename AnyType>  //template을 만든다는걸 알려주는 구문
void Swap(AnyType &a, AnyType &b) {
	AnyType temp;
    temp = a;
    a = b;
    b = temp;
}

template <typename AnyType>으로 prototype과 definition 모두 이렇게 명시해줘야한다.
여기서 typename keyword는 class keyword로도 쓰일 수 있다.
AnyType은 다른 이름으로 적어도 되는데 보통 T로 간단하게 적는다.
두개 이상의 type을 generic하게 사용하고 싶다면 <> 여기 안에 <class T1, class T2> 식으로 만들면 된다.

> 설명
template은 아무 함수도 생성하지 않는다. compiler에게 함수를 어떻게 만드는지 방향만 알려줄 뿐이다.
함수를 특정 type의 인자로 호출하면
Swap(p, q); //p와 q는 int
그때 compiler가 template pattern에 따라 해당 인자의 type에 대한 함수를 만든다.(호출하면 그 type대로 함수가 만들어지고, 동시에 사용도 하는 것)
즉, 같은 알고리즘에 다양한 type 적용된다면 template를 이용하면 좋다.

C++98 이후로 `typename` keyword가 추가됐다.
그래서 그 이전엔 `class` keyword를 이용해서 많은 library나 프로그램을 만들었었기때문에,
C++ Standard에선 "이 문맥에선" "두 keyword를 같다고 본다."
('typename'이 더 명확한 의미이니 이렇게 쓰는게 good)

overload대신 function template을 쓴다고해서 executable programs을 짧게 만들어주지 않는다.
final code에는 template은 사라지고, 그걸로 만들어진 function definitions만 남는다.(진짜 틀의 역할만 하네)
대신 많은 함수 definition을 간단하고 믿을만하게 만들 수 있다.(손으로 하나하나하면 오류 가능성..)
header file에 template 만들어두고 include해서 쓰는게 가장 일반적인 방식(링크에서도 그렇다고하네)

한 type으로 정의된 template에 type 여러 개를 넘기고 싶으면 explicit instantiation 활용해야함.
instantiation으로 Swap<double>(i,d);를 하면,
일단 double로 만들어지기때문에 i와 d의 type이 달라도 알아서 변환됨.
일반적인 경우엔 type을 보고 definition을 만들기때문에 불가능

Overloaded Templates

같은 알고리즘을 여러 type이 쓸 때 template을 쓴다고 했는데, 모든 type이(data가) 같은 algorithm을 공유하지 않을 수도 있다.
이때, 일반적인 함수 definition을 overload 했듯이, template definition도 overload 할 수 있다.
당연히 둘은 다른 signature을 사용해야 한다.

ex)

template <typename T>
void Swap(T &a, T &b);  //일반적인 변수 swap

template <typename T>
void Swap(T *a, T *b, int n);  //array swap

. . .

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) {  //모든 template parameter가 template parameter type일 필요는 없다.
	T temp;
    for (int i = 0; i < n; i++) {
    	temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

이러면 함수 overloading에서처럼, signature 일치하는 template을 찾아서 작동함.
모든 template parameter가 template parameter type일 필요는 없다.

function overloading과 function templates, templates overloading을 살짝 정리하고 가자면,
우선 셋 다 목적은 signature가 다른 함수를 만드는 것이다.
오버로딩은 모든 경우를 다 만들 수 있지만, 템플릿만으론 오버로딩으로 할 수 있는 모든 경우를 커버못함.
왜냐하면 템플릿은 딱 정해진 argument에서 type만 generic해지기 때문.
하지만 templates 조차도 overloading된다면 overloading을 손으로 하나하나 하지 않아도,
template으로 모두 자동화 시킬 수 있게 되는 것.

Explicit Specializations

Template 한계

template 내에서 연산이 좀 제한되기도 한다.
예를들어, template <class T> void f(T a, T b) { . . . } 으로 돼있을 때, 정의 내부에 if(a > b)~ 라는 statement가 있다고 해보자.
그럼 type T는 일반적인 structure일 수 없다.
혹은, T c = a*b;라면 type T는 array, pointer, structure일 수 없다.

이처럼 template function이 특정 type을 다루지 못할 수도 있다.
(1) 이럴땐 그 operator를 overload함으로써 해결할 수 있다.(Ch 11)
(2) 혹은 특정 type에 맞는 specialized template definition을 제공할 수도 있다.

Explicit Specializations

위에서 말했듯 한 template이 모든 type을 처리할 순 없다.
이럴 땐 template overloading을 한다고 했었는데, overloading이 안되는 경우가 있다.
일반함수는 비교적 간단하게 overloading이 가능하지만, function template이라는건 원래부터 좀 generic한 형태이기때문에, 작성하려는 두 template의 signatrue내의 인자 개수가 다른 경우가 아니면 overload하기가 비교적 어렵다.
ex)

template <typename T>
void Sum(T &a, T &b);

로 선언했을 때, double type은 다르게 처리하고 싶다고해보자. 한번 해봐라 template overloading 해도 안된다.
이럴때 explicit specialization을 이용하는 것이다.

> 문법

  • template <> void Swap<job>(job &, job &);
    template <> void Swap(job &, job &);
    (<job> 부분은 optional)

  • definition은 위 prototype을 함수 헤더로 하게 같이 짝을 이뤄서 작성해야 한다.
    (둘은 별개니 당연)

"Swap() template을 사용해서 function definition을 만들지 말고, job으로 만들어진 별도의 specialized function definition을 사용해라." 라는 뜻

근데 이게 왜 필요하지?
specialized라는 것 자체가 특별한 경우일때 따로 정의하는 것인데,
그럼 그냥 non-template 함수(일반 함수) 하나 만들면 되는거 아닌가? 일반함수가 전부 override하는데?
: template이 수정되거나, explicit instantiation 등의 경우에서는
일반 함수가 overrride하지 못할 수 있다.
즉, 일반함수보단 더 적합한 특수화(?)된 대안인 것이다.
참고: https://stackoverflow.com/questions/6406833/explicit-specialization-in-c
  • ※ Third-Generation Specialization (ISO/ANCI C++ Standard)
    1. 특정 함수 name을 non template function, template function, explicit specialization template function 에서 동시에 사용할 수 있다. (추가로 각 함수의 overloaded version까지)
    (non template, template, explicit specialization이 같은 이름이어도 괜찮다는 뜻)
    2. explicit specialization의 prototype과 definition은 꼭 template <>로 시작해야하고, specialized type을 작성해야한다.
    3. specialization은 일반 template을 override하고, non template function은 이 둘을 override한다.

Instantiations & Specializations

함수를 호출하면 compiler가 template을 이용해 해당 type의 function definition을 만든다. 이 결과를 instantiation of the template(탬플릿의 인스턴스화) 이라 한다.
template은 function definition이 아니지만, 특정 type을 사용해 만들어진 specific instantiation은 function definition이다.
보통은 함수의 인자를 보고서 function definition의 필요성을 compiler가 추론해서 만들기 때문에 이를 implicit instantiation이라 한다.

instantiation을 function definition이랑 아예 같은 의미로 사용하기도하고
아니면 function definition을 만들도록하는 구문? 과정?이라는 뜻으로도 사용하는 것 같다.
인스턴스'화' 이니까 후자가 좀 더 맞긴하지.

> explicit instantiation
explicit instantiation 이란 것도 있다.
<> 안에 특정 type을 작성하고, 함수 선언문 앞에 template keyword를 붙이는 것이다.

  • template void Swap<int>(int, int); : explicit instantiation
    얘도 <> 부분은 optional
    : "Swap() template을 사용하여 int type의 function definition을 만들어라" 라는 뜻

인자를 넘겨서 함수 생성과 동시에 사용할 수도 있고, 아니면 위처럼 그냥 instantiation을 일단 만들어두기만 할 수도 있다.
인자를 넘겨줘서 expicit instantiation 만들고 바로 사용하려면

  • Swap<double>(x,m)처럼 앞에 template과 return type을 생략하면 된다.
    (근데 이게 어디에 쓸모가 있는진 모르겠네. 그냥 template 쓰는거랑 다른가?)
explicit instantiation 언제 쓰는거지?
https://stackoverflow.com/questions/2351148/explicit-template-instantiation-when-is-it-used
https://stackoverflow.com/questions/13068993/when-would-you-use-template-explicit-instantiation

> 정리
implicit instantiations, explicit instantiations, explicit specializations을 통틀어서 specializations 라고 한다.
template처럼 generic description을 나타내는게 아니라, specific type의 function definition을 나타낸다.

template 뒤에 <>가 붙는가 안붙는가로 explicit specializations인지 explicit instantiations인지 구분한다.
explicit specializations: template을 이용하지말고 기존 만들어둔 정의를 이용하라. 는 것
explicit instantiations : temlate을 이용해 특정 type의 function definition을 만들어라. 는 것

같은 translation unit내의 explicit instantiation과 explicit specialization에
같은 type을 동시에 적용하면 이는 error

이미 특정 상황에 대해 function definition을 만들었는데 또 만드는 꼴임
template <class T>
void Swap(T &, T &);  //template prototype

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

int main(void) {
	template void Swap<char>(char &, char &); //explicit instantiation for char
    //바로 위 경우 instantiation(function definition)만 일단 만든 것
    short a,b;
    job n,m;
    char g,h;
    
    Swap(a,b); //template을 이용해 만든 implicit instantiation을 사용
    Swap(n,m); //explicit specialization을 사용
    Swap(g,h); //이미 민들어진 explicit instantiation을 사용
}

Compiler는 뭘 고르나?

function overloading부터 template, template overloading 등등..
함수 호출이 발생했을 때 같은 이름의 함수들이 많다면, 이들 중 어떤 종류의 function definition을 선택해야하는지를 결정하는 과정을 overload resolution 이라 한다.

1) 후보 Functions을 모두 모은다. 같은 이름을 가지는 functions or template functions..

2) 후보들 중 실행 가능한 Functions만 모은다. 당연히 argument 개수는 같아야 할 것이고, implicit conversion이 일어나서 정상작동하는 경우도 포함한다.
return type은 상관없다, signature만 보고서 판단한다.

3) 가장 실행 가능한(viable) Function을 고른다. best가 하나가 아니라면 해당 함수 호출은 error이다.
실행 가능한 함수들 중 best를 고르기 위해 아래와 같은 기준 순서로 ranking을 매긴다.

  1. exact match인 경우.(p.432 표 참고, 완전 일치하지 않고 이 표대로 일치해도 exact match임)
    regular function과 template 둘 다 해당되면 regular function이 상위 rank로 된다.
  2. conversion by promotion인 경우 (ex. short가 int로 되거나, float이 double로 되거나)
  3. conversion by standard conversion (ex. int가 char이 되거나, long이 double이 되거나)
  4. class declaration에 의해 정의된 것 같은 user-defined conversion인 경우

여기까지 해도 하나로 결정이 안된다면? 그럼 ambiguous 같은 error message를 띄움.
하지만, 아래 경우는 하나로 결정된다.
1. pointer나 reference variable이 non-const data를 받는다면, non-const pointer/reference parameter가 있는 것으로 결정된다.
(일반 변수는 이렇게 구분 안되므로 ambiguous error)
2. non-template function과 template function이면 전자를 선택(이건 위에서 말한건데 뭐 쨌든)
3. 둘 다 template function이라면 좀 더 specialized인 놈 선택,, 더 specialized하다는건 더 적은 conversion이 일어나는 경우를 말한다.(예시는 p.433~p.436)
-> 더 specialized인 놈을 선택하는걸 partial ordering rules 이라 함.

정리는 해봤는데 자세하게 보고싶으면 p.431부터 보도록.. 술술 읽힘

진짜 문제가 되는건 multiple argument일때라고 함.
매개변수 3개 있는데, viable function들 중 매개변수 하나씩만 더 잘 매칭된다면?
뭐 좀만 고민해봐도 머리아프네. 쨌든 이 책에선 복잡한 이런건 다루지 않는다고 하네.
어떤 경우든 해결하는 rule이 있다고는 함.
(메모) p.434 Listing 8.14보면, 배열의 원소들 print하는 template을 만든다.
이때 일반 변수들은 그냥 받아서 print하면 되지만,
string이면 배열 원소가 char\* 이기때문에 다른 algorithm을 사용해야한다.
그래서 이런 경우도 template overloading을 하는 것이다.

making your own choices

함수를 호출할때 lesser<>(m,n);이런 식으로 옆에 angle brackets(<>)를 붙여서 하게 되면 template function을 사용하도록 강제한다.
혹은 lesser<int>(m,n);으로 explicit instantiation을 이용해 template을 이용하도록 강제할 수도 있다.
위 두 경우 모두 regular function 정의가 있더라도 template function을 사용하게 된다.


Template Function Evolution

template 연구/확장해온 사람들 덕에 Standard template library가 생기고, C++ standard에 변화가 있는 등 발전이 있었다.
여기선 몇몇 관련 문제와 그들의 해결법에대해 알아보자.

What's That Type?

template function 내에서 특정 변수에 어떤 type이 적당할지 고르기가 쉽지 않을 수 있다.

template<class T1, class T2>
void ft(T1 x, T2 y) {
	~~? xpy = x + y;
}

이 경우 xpy의 type은? T1로하기도 그렇고, T2로 하기도 그렇고, 그렇다고 아무거나 큰 type을 하기도 좀 그렇고... + operator가 overloaded 됐으면 더 어렵다.
C++98에선 xpy의 type을 정할만한 명확한 방법이 없다.
(일반함수에선 괜찮은데, template 사용할때 발생하는 문제...)

The decltype Keyword (C++11)

decltype( expression )
형태로 사용한다. 그럼 expression과 같은 type을 반환(?)한다.
ex) decltype(x+y) xpy = x + y; 이러면 문제 해결!
(여러번 사용해야되면 typedef 활용해도 된다.)

compiler가 어떤 type을 쓸지 계산하는 과정은 좀 복잡한데,, 간단하게 설명.
decltype(a) b; 라고 하자.
1. a가 괄호가 전혀 포함되지 않는 identifier라면 b의 type은 a와 같다. const 같은 성질도 물론 포함해서
2. a가 함수호출문이라면 b는 해당 함수의 return type과 같다.
여기서 함수호출은 실제로 일어나지 않는다, 왜냐하면 compiler가 prototype으로 그냥 return문만 확인하기 때문
3. a가 lvalue라면 b는 해당 type의 reference type이다.
1번과 충돌한다고 생각할 수도 있는데, lvalue이지만, 괄호같은게 없는 identifier는 1번에 이미 다 해당된다. 저기 포함안되고, lvalue인 경우를 말하는 것이다.
1번 규칙을 뚫고 regerence type으로 만들고 싶다면 decltype((x)) b;처럼 괄호를 활용하면 된다.
4. 위 경우들 중 아무것에도 해당되지 않는다면, b는 그냥 a와 같은 type으로 된다.
ex)
decltype(100L) b; : b는 long
p와 q가 int & p, &q; 라면,
decltype(p+q) b; : b는 int (p,q 각각은 reference이지만, p+q는 당연히 int)

Trailing Return Type (C++11's Alternative Function Syntax)

decltype이 return type은 못 정해준다.
return type 부분에 decltype(x+y) func1(~~~) {~} 식으로 하면되지않나 라고 생각할 수 있는데, 저 위치에선 parameter가 안 보인다.

그래서 trailing return type이라는 새로운 문법을 이용한다.
double h(int x, float y);
를 아래와 같이 바꿀 수 있다.
auto double h(int x, float y) -> double;

이렇게되면 -> 뒤에선 parameter가 scope에 들어와서 보이므로 사용할 수 있다.

template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x+y) {
	. . .
    return x + y;
}

해결 완료.


Chapter Review

inline 함수 후보?
: nonrecursive, short, fit in one line 이면 아주 좋은 후보

default argument는 prototype에만 적어주면 됨.. definition은 그런거 없이 그대로 작성.
(prototype이 그 함수의 signature을 미리 알리는 것이니... prototype에 안적으면 compiler가 함수 인자 개수가 다르다고 판단할 것이다. prototype을 보고 함수호출할때 default argument랑 같이 넘겨줘버리는거니까 definition엔 굳이 안적어도 되는게 아닐까)


영단어

elapse (시간이) 흐르다
reminiscent 연상되는
pledge 약속
allegiance 충성
rodent 설치류
oddities 기괴한 일
purposefully 의도적으로
exotic 이국적인
dump 버리다
thwart 좌절시키다
flaw 결함
fore 앞부분에 위치한
upshot 결과
solicit 간청하다
focal 중심의, 초점의
magnification 확대
revert 되돌아가다
good-sized 꽤 큰
burden 부담, 짐
neat 깔끔한
lop 잘라내다
mangling 심하게 훼손하다
overlook 못 보고 넘어가다, 간과하다
deduce 추론하다
viable 실행가능한 (=비슷= feasible)
outranking 상위의
envision 마음속에 그리다

0개의 댓글