학습목표

  • 인라인 함수
  • 참조 변수
  • 함수 매개변수를 참조로 전달
  • 디폴트 매개변수
  • 함수 오버로딩
  • 함수 템플릿
  • 함수 템플릿의 특수화

8.1 C++ 인라인(inline) 함수

인라인 함수는 프로그램의 실행 속도를 높이기 위해 C++에 새로 보강된 함수임

  • 일반적인 함수 호출시 해당 함수의 주소로 점프한 후 함수 처리가 종결되면 다시 원래의 자리로 돌아옴
  • 인라인 함수는 컴파일된 함수 코드가 프로그램의 다른 코드 안에 직접 삽입됨
  • 즉, 함수 호출을 그에 대응하는 함수 코드로 대체하여 그 자리에서 처리함
  • 그렇기 때문에 시간적인 측면에서는 유리하나 메모리 사용 측면에서는 불리함
  • 함수 코드를 수행하는데 걸리는 시간이 매우 짧고, 빈번하게 호출되는 함수일 경우에 인라인 함수를 사용하는 것이 유리함
// inline.cpp

#include <iostream>
inline double square(double x) { return x * x; }
int main()
{
	using namespace std;
	double a, b;
	double c = 13.0;

	a = square(5.0);
	b = square(4.5 + 7.5);
	cout << "a = " << a << ", b = " << b << endl;
	cout << "c = " << c;
	cout << ", c의 제곱 = " << square(c++) << endl;
	cout << "현재 c = " << c << endl;
	return 0;
}

인라인 함수는 함수 선언 앞 또는 함수 정의 앞에 inline이라는 키워드를 붙여서 사용함
일반적으로는 원형을 아예 생략하고 그 자리에 함수 정의 전체(함수 머리와 함수 코드)를 놓아서 사용함
인라인 함수는 재귀호출이 허용되지 않고, 컴파일러에 따라 함수의 크기가 클 경우 인라인 요청을 거부할 수 있음

C의 매크로는 인라인 코드를 소스적으로 구현한 형태
매크로는 함수에 매개변수를 넘기는 것이 아닌, 텍스트를 대체함으로써 동작함
C++의 인라인 함수를 사용하는 것이 권장됨



8.2 참조 변수

참조(reference) : 미리 정의된 어떤 변수의 실제 이름 대신 쓸 수 있는 대용 이름

  • 매개변수로 사용시 함수는 복사본 대신 원본 데이터를 가지고 작업함
  • 클래스를 설계할 때 필수적으로 사용됨

참조 변수의 생성

// firstref.cpp

#include <iostream>
int main()
{
	using namespace std;
	int rats = 101;
	int & rodents = rats;

	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;
	rodents++;
	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	cout << "rats의 주소 = " << &rats;
	cout << ", rodents의 주소 = " << &rodents << endl;
	return 0;
}

&는 주소 연산자가 아닌 데이터형 식별자의 일부로 참조 선언을 나타낼 수 있음
참조는 *가 자동적으로 포함된 포인터와 비슷하지만, 포인터처럼 참조를 먼저 선언하고 나중에 값을 지정할 수 없음

// secref.cpp

#include <iostream>
int main()
{
	using namespace std;
	int rats = 101;
	int & rodents = rats;

	cout << "rats = " << rats;
	cout << ", rodents = " << rodents << endl;
	
	cout << "rats의 주소 = " << &rats;
	cout << ", rodents의 주소 = " << &rodents << endl;

	int bunnies = 50;
	rodents = bunnies;
	cout << "bunnies = " << bunnies;
	cout << ", rats = " << rats;
	cout << ", rodents = " << rodents << endl;

	cout << "bunnies의 주소 = " << &bunnies;
	cout << ", rodents의 주소 = " << &rodents << endl;
	return 0;
}

참조는 반드시 초기화를 해야하고, 특정 변수에 연결되면 그것을 고수해야하므로 const 포인터와 상당히 비슷함
즉, 참조는 대입문이 아닌 초기화 선언에 의해서만 설정할 수 있음


함수 매개변수로서의 참조

참조는 주로 함수의 매개변수로 사용됨

  • 함수에서 사용하는 변수의 이름을 함수를 호출한 프로그램에 있는 어떤 변수의 대용 이름으로 바꿈
  • 참조로 전달시 피호출 함수가 호출 함수의 변수를 사용할 수 있음
// swaps.cpp

#include <iostream>
void swapr(int & a, int & b);
void swapp(int * p, int * q);
void swapv(int a, int b);

int main()
{
	using namespace std;
	int wallet1 = 3000;
	int wallet2 = 3500;

	cout << "지갑 1 = " << wallet1 << "원";
	cout << "지갑 2 = " << wallet2 << "원\n";

	cout << "참조를 이용하여 내용들을 교환 : \n";
	swapr(wallet1, wallet2);
	cout << "지갑 1 = " << wallet1 << "원";
	cout << ", 지갑 2 = " << wallet2 << "원\n";

	cout << "포인터를 이용하여 내용들을 다시 교환 : \n";
	swapp(&wallet1, &wallet2);
	cout << "지갑 1 = " << wallet1 << "원";
	cout << ", 지갑 2 = " << wallet2 << "원\n";

	cout << "값으로 전달하여 내용 교환 시도 :\n";
	swapv(wallet1, wallet2);
	cout << "지갑 1 = " << wallet1 << "원";
	cout << ", 지갑 2 = " << wallet2 << "원\n";

	return 0;
}

void swapr(int & a, int & b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}

void swapp(int * p, int * q)
{
	int temp;

	temp = *p;
	*p = *q;
	*q = temp;
}

void swapv(int a, int b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}

참조로 전달하는 함수의 매개변수는 함수 호출이 넘겨주는 매개변수로 초기화됨
swapr 함수의 변수 a와 b는 각각 wallet1과 wallet2를 참조하여 대용 이름으로 작동함
따라서 a와 b를 교환시 wallet1과 wallet2가 교환됨


참조의 특성

// cubes.cpp

#include <iostream>
double cube(double a);
double refcube(double &ra);

int main()
{
	using namespace std;
	double x = 3.0;

	cout << cube(x);
	cout << " = " << x << "의 세제곱\n";
	cout << refcube(x);
	cout << " = " << x << "의 세제곱\n";
	return 0;
}

double cube(double a)
{
	a *= a * a;
	return a;
}

double refcube(double &ra)
{
	ra *= ra * ra;
	return ra;
}

매개변수를 값으로 전달할 경우 원본은 변경되지 않음
매개변수를 참조로 전달할 경우 참조된 변수가 영향을 받음

  • 이 때 원본 변수를 변경하고 싶지 않다면 const를 통한 상수 참조를 사용해야함
  • 간단한 함수를 작성할시에는 참조를 사용하는 것을 지양하고, 구조체나 클래스 등 덩치 큰 데이터를 다룰 때 사용하는 것이 권장됨

매개변수를 값으로 전달할 때와는 달리 참조 변수에는 표현식을 전달할 수 없음
C++은 실제 매개변수와 참조 매개변수가 일치하지 않고, 매개변수가 const 참조일 경우 임시 변수를 생성할 수 있음

  • 실제 매개변수가 올바른 데이터형이지만 lvalue가 아닐 때
    • lvalue : 참조가 가능한 데이터 객체
      변수 / 배열의 원소 / 구조체 멤버 / 참조 또는 역참조 포인터
      일반 상수 및 여러개의 항으로 이루어진 표현식은 lvalue가 아님
  • 실제 매개변수가 잘못된 데이터형이지만 올바른 데이터형으로 변환할 수 있을 때
  • 임시 변수는 함수가 호출되어있는 동안만 유지됨
  • 임시 변수를 생성하여 사용할 경우 매개변수로 전달되는 변수는 변경되지 않기 때문에 C++에서는 const 참조일때만 임시 변수 생성을 허용함

가능하면 const를 사용해야 함

  • 실수로 데이터 변경을 일으키는 프로그래밍 에러를 막을 수 있음
  • 원형에 const 사용시 함수가 constconst가 아닌 실제 매개변수를 모두 처리할 수 있음
  • 생략시에는 const가 아닌 데이터만 처리 가능
  • const참조 사용시 함수가 자신의 필요에 따라 임시 변수를 생성하여 사용할 수 있음

C++11에서는 rvalue참조 기능이 있음

  • double && rref = std::sqrt(36.00);의 형태와 같이 &&를 사용
  • 라이브러리 디자이너로 하여금 특정 기능을 더욱 효율적으로 구현할 수 있도록 돕기 위해 소개됨

구조체에 대한 참조

구조체에 대한 참조도 기본 데이터형의 변수에 대한 참조와 마찬가지로 구조체 매개변수 선언시 참조 연산자 &를 앞에 붙여서 사용함

// strc_ref.cpp

#include <iostream>
#include <string>
struct free_throws
{
	std::string name;
	int made;
	int attempts;
	float percent;
};

void display(const free_throws & ft);
void set_pc(free_throws & ft);
free_throws & accumulate(free_throws & target, const free_throws & source);

int main()
{
	free_throws one = {"Ifelsa Branch", 13, 14};
	free_throws two = {"Andor Knott", 10, 16};
	free_throws three = {"Minnie Max", 7, 9};
	free_throws four = {"Whily Looper", 5, 9};
	free_throws five = {"Long Long", 6, 14};
	free_throws team = {"Throwgoods", 0, 0};

	free_throws dup;
	set_pc(one);
	display(one);
	accumulate(team, one);
	display(team);

	display(accumulate(team, two));
	accumulate(accumulate(team, three), four);
	display(team);

	dup = accumulate(team, five);
	std::cout << "team 출력 : \n";
	display(team);
	std::cout << "대입 이후 dup 출력 : \n";
	display(dup);
	set_pc(four);

	accumulate(dup, five) = four;
	std::cout << "문제 소지가 있는 대입 이후 dup 출력 : \n";
	display(dup);
	return 0;
}

void display(const free_throws & ft)
{
	using std::cout;
	cout << "Name : " << ft.name << '\n';
	cout << "Made : " << ft.made << '\t';
	cout << "Attempts : " << ft.attempts << '\t';
	cout << "Percent : " << ft.percent << '\n';
}

void set_pc(free_throws & ft)
{
	if (ft.attempts != 0)
		ft.percent = 100.0f * float(ft.made)/float(ft.attempts);
	else
		ft.percent = 0;
}

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

구조체 초기화시 초기화되는 멤버의 수가 구조체의 멤버보다 적을 경우 남은 멤버들은 0으로 초기화됨
함수가 구조체를 변경하지 않는 경우 const를 사용함
참조는 값으로 전달할 때와 달리 복사본을 만들지 않으므로 시간과 용량을 절약할 수 있음

참조를 리턴할 시 임시 변수에 대한 참조를 하지 않도록 주의해야함

  • 함수에 매개변수로 전달된 참조를 리턴할시 이 문제를 피할 수 있음
  • 또는 new로 새롭게 할당하는 것과 비슷한 일을 참조를 통해 처리할 수 있음
    const free_throws & clone(free_throws & ft)
    {
      free_throws * pt;
      *pt = ft;
      return *pt;
    }
    free_throws & jolly = clone(three);
    

일반적으로 리턴형을 const참조로 만들시 모호한 에러를 줄일 수 있음
단, 경우에 따라 const를 쓰지 않는 것이 옳은 경우도 존재함


클래스 객체와 참조

일반적으로 C++은 클래스 객체를 함수에 전달할 때 참조를 사용함
즉, string / ostream / istream / ofstream / ifstream 등의 클래스의 객체를 매개변수로 취하는 함수들에 참조 매개변수를 사용할 수 있음

// strquote.cpp

#include <iostream>
#include <string>
using namespace std;
string version1(const string & s1, const string & s2);
const string & version2(string & s1, const string & s2);
const string & version3(string & s1, const string & s2);

int main()
{
	string input;
	string copy;
	string result;

	cout << "문자열을 입력하시오 : ";
	getline(cin, input);
	copy = input;
	cout << "입력한 문자열 : " << input << endl;
	result = version1(input, "***");
	cout << "바뀐 문자열 : " << result << endl;
	cout << "원래 문자열 : " << input << endl;

	result = version2(input, "###");
	cout << "바뀐 문자열 : " << result << endl;
	cout << "원래 문자열 : " << input << endl;

	cout << "원래 문자열 재설정\n";
	input = copy;
	result = version3(input, "@@@");
	cout << "바뀐 문자열 : " << result << endl;
	cout << "원래 문자열 : " << input << endl;
	return 0;
}

string version1(const string & s1, const string & s2)
{
	string temp;
	
	temp = s2 + s1 + s2;
	return temp;
}

const string & version2(string & s1, const string & s2)
{
	s1 = s2 + s1 + s2;
	return s1;
}

const string & version3(string & s1, const string & s2)
{
	string temp;
	
	temp = s2 + s1 + s2;
	return temp;
}

version1 함수의 경우 string version4(string s1, string s2와 같이 객체들을 직접 전달하는 것과 동일한 결과가 출력됨
단, 이 때의 s1과 s2는 새로 만들어지는 객체이기 때문에 version1과 같이 참조를 사용하는 것이 더 효율적임

result = version1(input, "***");에서 확인할 수 있듯이 const char *형인 "***"를 string 참조에 전달할 수 있음

  • string 클래스가 char *형을 string으로 변환하는 것을 정의하고 있음
  • const 참조 형식 매개변수의 특성대로 실 매개변수의 데이터형이 참조 매개변수의 데이터형과 일치하지 않을 경우 올바른 데이터형의 임시 변수를 만들어 참조를 전달함

지역변수에 대한 참조를 리턴하는 것은 안전하지 않음


또 하나의 객체 레슨 : 객체, 상속, 참조

상속(inheritance) : 한 클래스에서 다른 클래스로 기능을 전달하는 것을 가능하게하는 C++ 언어의 기능

  • ostream은 기초 클래스(base class), ofstream은 파생 클래스(derived class)
  • 파생 클래스는 기초 클래스의 메서드들을 상속하며, 따라서 ofstream의 객체는 기초 클래스의 기능들을 사용할 수 있음

데이터형 변환 없이 기초 클래스 참조가 파생 클래스 객체를 참조할 수 있음
따라서 기초 클래스 참조 매개변수를 가지는 함수를 정의하면, 해당 함수를 기초 클래스 객체 및 파생 객체 모두에 사용할 수 있음

// filefunc.cpp

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

void file_it(ostream & os, double fo, const double fe[], int n);
const int LIMIT = 5;

int main()
{
	ofstream fout;
	const char * fn = "ep-data.txt";
	fout.open(fn);
	if (!fout.is_open())
	{
		cout << fn << "파일을 열 수 없습니다. 끝.\n";
		exit(EXIT_FAILURE);
	}
	double objective;
	cout << "대물렌즈 초점거리를 mm 단위로 입력하십시오 : ";
	cin >> objective;
	double eps[LIMIT];
	cout << LIMIT << "가지 대안렌즈의 초점거리를 mm 단위로 입력하십시오 : \n";
	for (int i = 0; i < LIMIT; i++)
	{
		cout << "대안렌즈 #" << i + 1 << " : ";
		cin >> eps[i];
	}
	file_it(fout, objective, eps, LIMIT);
	file_it(cout, objective, eps, LIMIT);
	cout << "종료\n";
	return 0;
}

void file_it(ostream & os, double fo, const double fe[], int n)
{
	ios_base::fmtflags initial;
	initial = os.setf(ios_base::fixed);
	os.precision(0);
	os << "대물렌즈의 초점거리 : " << fo << " mm\n";
	os.setf(ios::showpoint);
	os.precision(1);
	os.width(17);
	os << "대안렌즈 초점거리";
	os.width(15);
	os << "확대배율" << endl;
	for (int i = 0; i < n; i++)
	{
		os.width(17);
		os << fe[i];
		os.width(15);
		os << int (fo/fe[i] + 0.5) << endl;
	}
	os.setf(initial);
}

ostream &형인 os 매개변수가 ostream객체인 coutofstream객체인 fout을 모두 참조함
setf() : 다양한 포맷팅 상태를 설정하는 메서드, 호출하기 전에 유효한 모든 포맷팅 설정들의 복사본을 리턴함

  • setf(ios_base::fixed) : 고정 소수점 표기를 사용하는 모드
  • setf(ios_base:showpoint) : 뒤따르는 숫자들이 0인 경우에도 뒤에 붙은 소수점을 표시
  • precision() : 객체가 고정 소수점 표기 모드에 놓여있는 경우 소수점의 오른쪽에 표시할 숫자들의 개수를 지정
  • width() : 다음 출력 동작에 사요할 필드 폭을 설정, 하나의 값만 디스플레이한 후 디폴트(0)으로 돌아감

각 객체는 자기 자신의 포맷팅 설정을 저장함!


참조 매개변수는 언제 사용하는가?

참조 매개변수를 사용하는 이유

  • 호출 함수에 있는 데이터 객체의 변경을 허용하기 위해
  • 전체 데이터 객체 대신에 참조를 전달하여 프로그램의 속도를 높이기 위해

함수가 전달된 데이터를 변경하지 않고 사용만 하는 경우

  • 데이터 객체가 기본 데이터형이나 작은 구조체라면 값으로 전달
  • 데이터 객체가 배열이라면 포인터밖에 사용할 수 없으며, const를 지시하는 포인터로 설정
  • 데이터 객체가 덩치 큰 구조체일 경우 const 포인터 혹은 const 참조를 사용하여 프로그램의 속도를 높임
  • 데이터 객체가 클래스 객체라면 const 참조를 사용, 클래스 객체 매개변수의 전달은 참조로 전달하는 것이 표준임

함수가 호출 함수의 데이터를 변경하는 경우

  • 데이터 객체가 기본 데이터형이면 포인터를 사용함
  • 데이터 객체가 배열인 경우 포인터밖에 사용할 수 없음
  • 데이터 객체가 구조체라면 참조 또는 포인터를 사용
  • 데이터 객체가 클래스 객체일 경우 참조를 사용

단, 위 사항들은 어디까지나 지침일 뿐이며 다른 방식을 선택할 수 있음



8.3 디폴트 매개변수

디폴트 매개변수 : 함수 호출에서 실제 매개변수를 생략했을시 대신 사용되는 값
함수 사용에 매우 큰 융통성을 부여함

// left.cpp

#include <iostream>
const int ArSize = 80;
char * left(const char * str, int n = 1);

int main()
{
	using namespace std;
	char sample[ArSize];
	cout << "문자열을 입력하십시오:\n";
	cin.get(sample, ArSize);
	char *ps = left(sample, 4);
	cout << ps << endl;
	delete [] ps;
	ps = left(sample);
	cout << ps << endl;
	delete [] ps;
	return 0;
}

char * left(const char * str, int n)
{
	if (n < 0)
		n = 0;
	char * p = new char[n+1];
	int i;
	for(i = 0; i < n && str[i]; i++)
		p[i] = str[i];
	while (i <= n)
		p[i++] = '\0';
	return p;
}

디폴트 값은 함수 원형에서 설정이 가능함
char * left(const char * str, int n = 1);의 형식으로 디폴트 값을 초기화 할 수 있음
단, 어떤 매개변수를 디폴트 매개변수로 만들기 위해서는 해당 매개변수의 오른쪽에 있는 모든 매개변수가 디폴트 매개변수여야 함



8.4 함수 오버로딩

함수 오버로딩(function overloading) : 함수의 다형(polymorphism)이라고도 부르며, 서로 다른 여러개의 함수가 하나의 이름을 공유함
오버로딩된 함수 중에서 어떠한 버전을 사용할 것인지를 컴파일러가 결정하게 됨

함수 오버로딩은 함수의 매개변수 리스트에 따라 결정되며, 이를 함수 시그니처(function signature)라고 함

  • 두 함수가 같은 개수의 매개변수를 가지고있으며 데이터형과 순서가 같다면 두 함수의 시그니처는 같으며, 이 때 변수의 이름은 달라도 상관없음
  • C++에서는 서로 다른 시그니처를 가지고 있는 함수들을 같은 이름으로 정의할 수 있음
  • 사용자가 해당 이름의 함수를 사용하면 컴파일러가 입력한 시그니처와 동일한 시그니처를 가지고있는 함수 원형을 찾아줌
  • 단, 대응하는 원형이 없는 호출의 경우 표준적인 데이터형 변환을 시도하나 함수의 원형이 여러가지라면 어느 것으로 변환할지 모호할 수 있기 때문에 에러로 간주하여 컴파일이 거부됨
  • 어떤 데이터형에 대한 참조와 해당 데이터형 자체는 같은 시그니처로 간주함
  • constconst가 아닌 변수는 구별됨
  • 함수의 데이터형이 다르더라도 시그니처가 같으면 공존할 수 없음

오버로딩 참조 매개변수에 경우 동종의 매개변수와 매치될 수 잇음

// 1)
void stove(double & r1);
// 2)
void stove(const double & r2);
// 3)
void stove(double && r3);

1번 함수의 매개변수인 r1은 변경 가능한 lvalue 매개변수와 매치됨
2번 함수의 매개변수인 r2는 변경 가능한 lvalueconst lvalue, rvalue 매개변수와 매치됨
3번 함수의 매개변수인 r3는 rvalue와 매치됨
이 때 오버로딩이 일어나면 변경 가능한 lvalue는 1번 함수로, const lvalue는 2번 함수, rvalue는 3번 함수와 각각 매치되어 더욱 정확한 매치가 이루어짐

오버로딩 예제

// leftover.cpp

#include <iostream>
unsigned long left(unsigned long num, unsigned ct);
char * left(const char * str, int n = 1);

int main()
{
	using namespace std;
	char * trip = "Hawaii!!";
	unsigned long n = 12345678;
	int i;
	char * temp;
	for (i = 1; i < 10; i++)
	{
		cout << left(n, i) << endl;
		temp = left(trip, i);
		cout << temp << endl;
		delete [] temp;
	}
	return 0;
}

unsigned long left(unsigned long num, unsigned ct)
{
	unsigned digits = 1;
	unsigned long n = num;

	if (ct == 0 || num == 0)
		return 0;
	while (n /= 10)
		digits++;
	if (digits > ct)
	{
		ct = digits - ct;
		while (ct--)
			num /= 10;
		return num;
	}
	else
		return num;
}

char * left(const char * str, int n)
{
	if (n < 0)
		n = 0;
	char * p = new char[n+1];
	int i;
	for (i = 0; i < n && str[i]; i++)
		p[i] = str[i];
	while (i <= n)
		p[i++] = '\0';
	return p;
}

두 종류의 left()함수를 오버로딩하여 사용함


함수 오버로딩은 언제 사용하는가

함수 오버로딩은 서로 다른 데이터형을 대상으로 하지만 기본적으로는 같은 작업을 수행하는 함수들에만 사용하는 것이 바람직함
디폴트 매개변수를 사용하는 함수로 대체 가능한 경우 대체하는 것이 메모리 절약 및 수정의 편리성 측면에서 유리함
따라서 서로 다른 데이터형의 매개변수를 요구하고, 디폴트 매개변수가 소용없을 때에만 함수 오버로딩을 사용해야 함

C++ 컴파일러는 이름 장식(name decoration) 또는 이름 맹글링(name mangling)이라는 기술을 사용하여 함수 원형에 지정되어있는 형식 매개변수의 데이터형을 기반으로 각각의 함수 이름을 암호화함



8.5 함수 템플릿

함수 템플릿 : 함수의 일반화 서술, 구체적인 데이터형을 포괄할 수 있는 일반형(generic type)으로 함수를 정의

  • 어떤 데이터형을 템플릿에 매개변수로 전달시 컴파일러가 해당 데이터형에 맞는 함수를 생성함
  • 구체적인 데이터형 대신 일반형으로 프로그래밍하므로 일반화 프로그래밍(generic programming)이라고 함
  • 템플릿을 매개변수화 데이터형(parameterized type)이라고도 함
    template <class AnyType>
    void Swap(AnyType &a, AnyType &b)
    {
      AnyType temp;
      temp = a;
      a = b;
      b = temp;
    }
    

    키워드 templateclass를 사용하며, class대신 typename을 사용할 수도 있음
    임의 데이터형의 이름은 C++의 이름 규칙만 준수하면 마음대로 지정할 수 잇음

// funtemp.cpp

#include <iostream>
template <class Any>
void Swap(Any &a, Any &b);

int main()
{
	using namespace std;
	int i = 10;
	int j = 20;
	
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "컴파일러가 생성한 int형 교환기를 사용하면\n";
	Swap(i,j);
	cout << "이제 i, j = " << i << ", " << j << ".\n";

	double x = 24.5;
	double y = 81.7;
	cout << "x, y = " << x << ", " << y << ".\n";
	cout << "컴파일러가 생성한 double형 교환기를 사용하면\n";
	Swap(x,y);
	cout << "이제 x, y = " << x << ", " << y << ".\n";
	return 0;
}

template <class Any>
void Swap(Any &a, Any &b)
{
	Any temp;
	temp = a;
	a = b;
	b = temp;
}

실제로 실행된 프로그램에서는 함수 템플릿을 사용하더라도 별개의 함수 정의가 생성되어, 최종적으로 산출되는 코드에는 어떠한 템플릿도 포함되지 않고 해당 프로그램을 위해 생성된 실제 함수들만 포함됨
대신 여러 개의 함수 정의를 더 간단하고 더 신뢰성 있게 생성할 수 있음

템플릿의 오버로딩

// twotemps.cpp

#include <iostream>
template <typename T>
void Swap(T &a, T &b);

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

void Show(int a[]);
const int Lim = 8;

int main()
{
	using namespace std;
	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "컴파일러가 생성한 int형 교환기를 사용하면\n";
	Swap(i,j);
	cout << "이제 i, j = " << i << ", " << j << ".\n";

	int d1[Lim] = {0,7,0,4,1,7,7,6};
	int d2[Lim] = {0,6,2,0,1,9,6,9};
	cout << "원본 배열 : \n";
	Show(d1);
	Show(d2);
	Swap(d1, d2, Lim);
	cout << "교환된 배열 : \n";
	Show(d1);
	Show(d2);
	return 0;
}

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)
{
	Any temp;
	for (int i = 0; i < n; i++)
	{
		temp = a[i];
		a[i] = b[i];
		b[i] = temp;
	}
}

void Show(int a[])
{
	using namespace std;
	cout << a[0] << a[1] << "/";
	cout << a[2] << a[3] << "/";
	for (int i = 4; i < Lim; i++)
		cout << a[i];
	cout << endl;
}

템플릿은 다양한 데이터형에 대해 같은 알고리즘을 적용하는 여러개의 함수가 필요할 때 사용하지만, 모든 데이터형이 항상 같은 알고리즘을 사용할 수는 없기 때문에 템플릿 정의를 오버로딩할 수 있음
일반적인 오버로딩과 마찬가지로 템플릿을 오버로딩할 때에도 확실하게 구분되는 함수 시그니처를 사용해야 함

모든 템플릿 매개변수가 템플릿 매개변수형일 필요는 없음


템플릿 제한

데이터형에 따라 연산이 되지 않는 경우가 있음
그러나 C++ 구문이 허용하지 않을지라도 템플릿 함수를 일반화하는 것이 때로는 이치에 맞음
C++이 플러스 연산자를 오버로드하도록 허용하여 특별한 구조체나 클래스 형태로 사용되도록 하는 것이 그 예


명시적 특수화

temp = a;
a = b;
b = temp;

위와 같이 이루어진 스왑하는 템플릿에 구조체를 넣는 경우, C++에서는 하나의 구조체를 다른 구조체에 대입할 수 있기 때문에 코드가 정상적으로 동작함
그러나 구조체 멤버의 일부만 교환하고 싶은 경우에는 코드가 달라져야 하지만, Swap()에 넘겨주는 매개변수는 그대로 두 개의 구조체에 대한 참조여야 하기 때문에 새로운 코드에 템플릿 오버로딩을 사용할 수 없음

이 때 명시적 특수화(explicit specialization)이라는 특수화된 함수 정의를 사용할 수 있음
컴파일러가 함수 호출에 정확히 대응하는 특수화된 정의를 발견시 템플릿 대신 해당 정의를 사용함

struct job{
	...
};

// 템플릿이 아닌 함수 원형
void Swap(job &, job &);

// 템플릿 원형
template <typename T>
void Swap(T &, T &);

// job형을 위한 명시적 특수화
template <> void Swap<job>(job &, job &);
// template <> void Swap(job &, job &)도 가능

3세대 특수화(ISO/ANSI C++ 표준)

  • 함수 이름이 하나 주어지면 사용자는 템플릿이 아닌 함수 / 템플릿 함수 / 명시적 특수화 템플릿 함수를 가질 수 있으며, 이들의 오버로딩 버전도 가질 수 있음
  • 명시적 특수화를 하기 위한 원형과 정의 앞에 template <>가 와야하며, 특수형의 이름을 서술해야함
  • 특수화는 템플릿을 무시하고, 템플릿이 아닌 함수는 특수화와 템플릿 둘 다 무시함
  • 즉, 위의 원형들이 공존할경우 컴파일러는 템플릿이 아닌 버전을 선택하고 템플릿 버전보다는 명시적 특수화 버전을 선택함
// twoswap.cpp

#include <iostream>
template <typename T>
void Swap(T &a, T &b);

struct job
{
	char name[40];
	double salary;
	int floor;
};

template <> void Swap<job>(job &j1, job &j2);
void Show(job &j);

int main()
{
	using namespace std;
	cout.precision(2);
	cout.setf(ios::fixed, ios::floatfield);
	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "컴파일러가 생성한 int형 교환기를 사용하면\n";
	Swap(i,j);
	cout << "이제 i, j = " << i << ", " << j << ".\n";

	job sue = {"Susan Yaffee", 73000.60, 7};
	job sidney = {"Sidney Taffee", 78060.72, 9};
	cout << "job 교환 전 : \n";
	Show(sue);
	Show(sidney);
	Swap(sue, sidney);
	cout << "job 교환 후 : \n";
	Show(sue);
	Show(sidney);
	return 0;
}

template <typename T>
void Swap(T &a, T &b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

template <> void Swap<job>(job &j1, job &j2)
{
	double t1;
	int t2;
	t1 = j1.salary;
	j1.salary = j2.salary;
	j2.salary = t1;
	t2 = j1.floor;
	j1.floor = j2.floor;
	j2.floor = t2;
}

void Show(job &j)
{
	using namespace std;
	cout << j.name << " : (" << j.floor << "층에 거주) "
		 << "$" << j.salary << endl;
}

오래된 컴파일러의 경우 오래된 특수화 형식을 사용해야 함

컴파일러가 특정 데이터형에 맞는 함수 정의를 생성하기 위해 템플릿을 사용할 때, 그 결과를 템플릿의 구체화(instantiation)라고 함
프로그램이 특정 데이터형 매개변수를 요구하는 함수를 사용한다는 사실을 컴파일러에게 알림으로써 그에 맞는 함수 정의를 만들 필요가 있다는 것을 컴파일러가 암시적으로 인식함
따라서 이러한 방식의 구체화를 암시적 구체화(implicit instantiation)라고 함

반면 컴파일러가 Swap<int>()와 같은 특정 구체화를 생성하도록 사용자가 직접 지시를 내리는 것을 명시적 구체화(explicit instantiation)라고 부름
명시적 구체화는 <>를 표기하여 데이터형을 나타내고, 선언 앞에 키워드 template를 사용함

하나의 프로그래밍 단위에서 동일한 데이터형에 대해 명시적 구체화와 명시적 특수화를 함께 사용해서는 안됨

  • 명시적 구체화의 경우 존재하는 템플릿을 사용하여 특정 데이터형에 맞는 함수 정의를 생성하라는 의미로 사용
  • 명시적 특수화의 경우 존재하는 템플릿을 사용하지 말고, 특정 데이터형에 맞게 특별히 명시적으로 정의된 함수 정의를 사용하라는 의미로 사용

암시적 구체화, 명시적 구체화, 명시적 특수화는 모두 특수화(specialization)이라고 함

template <class T>
void Swap(T &, T &); // 템플릿 원형

template <> void Swap<job>(job &, job &); // job을 위한 명시적 특수화

int main(void)
{
	template void Swap<char>(char &, char &); // char를 위한 명시적 구체화
	short a, b;
	...
	Swap(a, b); // short를 위한 암시적 템플릿 구체화를 사용
	job n, m;
	...
	Swap(n, m); // job을 위한 명시적 특수화를 사용
	char g, h;
	...
	Swap(g, h); // char를 위한 명시적 템플릿 구체화를 사용
	...
}

컴파일러는 어느 함수를 선택할까?

어떤 함수 호출에 대해 매개변수가 여러개일 때, 어떤 함수 정의를 사용할 것인지에 대한 전략을 오버로딩 분석(overload resolution)이라고 함

  1. 후보 함수들의 목록을 만듬
    이 때 후보들은 호출된 함수와 이름이 동일한 함수와 함수 탬플릿들임
  2. 여러 후보 함수들 중에서 계속 존속할 수 있는 함수들의 목록을 만듬
    계속 존속할 수 있는 함수란 매개변수의 개수가 일치하는 함수를 뜻하며, 이들에 대해 암시적 변환 절차가 이루어짐
  3. 가장 적당한 함수가 있는지 판단하여 그런 함수가 있으면 사용함
    없다면 함수 호출을 에러로 취급함

우선순위는 다음과 같음

  1. 매개변수가 정확히 대응하는 것, 일반 함수가 템플릿보다 높은 우선순위를 가짐
  2. 승급 변환(char / short -> int, float -> double 등)
  3. 표준 변환(int -> char, long -> double 등)
  4. 클래스 선언에서 정의되는 변환과 같은 사용자 정의 변환

C++은 정확한 대응을 만들기 위해 사소한 변환(trivial conversion)을 허용함

  • Type -> Type & / const Type / volatile Type
  • Type & -> Type
  • Type [] -> * Type
  • Type (argument-list) -> Type (*)(argument-list)
  • Type * -> const Type * / volatile Type *

정확히 대응하는 원형이 여러개 존재할시엔 컴파일러는 오버로딩 분석 절차를 완료하지 못하고 모호하다는 등의 단어가 포함된 에러 메시지를 내보냄
단, const가 아닌 데이터를 지시하는 포인터와 참조는 const가 아닌 포인터와 참조 매개변수에 대응함
따라서 포인터와 참조에 의해 지시되는 데이터일 경우 정확히 대응하는 하나의 함수를 선택할 수 있음

템플릿이 아닌 함수와 템플릿 함수가 모두 정확히 대응할 때에는 템플릿이 아닌 함수가 명시적 특수화를 포함하는 템플릿 함수보다 더 좋은 것으로 간주됨
두 개의 템플릿 함수가 모두 정확히 대응하는 경우에는 특수화된 템플릿 함수가 더 좋은 것으로 간주됨
가장 특수화되었다는 것은 그것을 사용할 데이터형을 컴파일러가 추론할 때 데이터형의 변환이 거의 일어나지 않는다는 것을 의미함

template <class Type> void recycle(Type t);  // #1
template <class Type> void recycle(Type * t); // #2

struct blot {int a; char b[10];};
blot ink = {25, "spots"};
...
recycle(&ink);

위와 같은 호출의 경우 #1에서의 Typeblot *, #2에서의 Typeblot으로 해석됨
즉 #2는 함수 매개변수가 Type을 지시하는 포인터라고 명시적으로 이미 지정했기 때문에 Typeblot로 직접 인식됨
#1은 함수 매개변수로 Type을 사용하기 때문에 Typeblot을 지시하는 포인터로 해석되어야 함
따라서 Type이 이미 포인터로 특수화되어있는 #2가 더 특수화된 것으로 간주됨

// tempover.cpp

#include <iostream>
template <typename T>
void ShowArray(T arr[], int n);

template <typename T>
void ShowArray(T * arr[], int n);

struct debts
{
	char name[50];
	double amount;
};

int main()
{
	using namespace std;
	int things[6] = {13, 31, 103, 301, 310, 130};
	struct debts mr_E[3] =
	{
		{"Ima Wolfe", 2400.0},
		{"Ura Foxe", 1300.0},
		{"Iby Stout", 1800.0}
	};
	double * pd[3];

	for (int i = 0; i < 3; i++)
		pd[i] = &mr_E[i].amount;

	cout << "Mr.E의 재산목록 : \n";
	ShowArray(things, 6);
	cout << "Mr.E의 채무목록 : \n";
	ShowArray(pd, 3);
	return 0;
}

template <typename T>
void ShowArray(T arr[], int n)
{
	using namespace std;
	cout << "템플릿 A\n";
	for (int i = 0; i < n; i++)
		cout << arr[i] << ' ';
	cout << endl;
}

template <typename T>
void ShowArray(T * arr[], int n)
{
	using namespace std;
	cout << "템플릿 B\n";
	for (int i = 0; i < n; i++)
		cout << *arr[i] << ' ';
	cout << endl;
}

thingsint형 배열 이름이므로 T를 int형으로 취하여 템플릿 A에 대응함 pddouble *형 배열 이름이므로 T를 double *형으로 취하여 템플릿 A에 대응할 수 있고, Tdouble로 취하여 템플릿 B에도 대응할 수 있음
그 중 배열의 내용이 포인터라는 특별한 가정을 하고있는 템플릿 B가 더 특수한 것으로 간주되어 pd는 템플릿 B를 사용함

// choices.cpp

#include <iostream>
template <typename T>
T lesser(T a, T b)
{
	return a < b ? a : b;
}

int lesser (int a, int b)
{
	a = a < 0 ? -a : a;
	b = b < 0 ? -b : b;
	return a < b ? a : b;
}

int main()
{
	using namespace std;
	int m = 20;
	int n = -30;
	double x = 15.5;
	double y = 25.9;

	cout << lesser(m, n) << endl;
	cout << lesser(x, y) << endl;
	cout << lesser<>(m, n) << endl;
	cout << lesser<int>(x, y) << endl;

	return 0;
}

lesser<>(m, n)과 같이 우선순위상 템플릿이 아닌 함수가 선택될 때 <>를 붙여 강제적으로 템플릿 함수를 선택하도록 만들 수 있음

여러 개의 매개변수를 사용하는 함수 호출이 여러개의 매개변수를 사용하는 원형들에 대응될 때, 컴파일러는 모든 매개변수에 대해 일일이 대응 여부를 조사해야하기 때문에 분석이 어려움
이 때 어떤 함수가 다른 함수보다 더 좋은 선택을 얻기 위해선 해당 함수가 적어도 모든 매개변수에 잘 대응해야하고, 다른 함수보다 적어도 어느 하나의 매개변수가 더 잘 대응해야함


템플릿 함수의 발전

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

위와 같은 예제가 있을 때, 사전에 미리 ft()가 어떻게 사용될지 알 수 없기 때문에 xpy타입에 대한 명확한 선택을 할 수 없음

decltype(x + y) xpy;
xpy = x + y;

이럴 때 C++11의 새로운 키워드인 decltype을 사용할 수 있음

  • decltype(expression) var의 형태로 사용
  • 1단계 : expression이 괄호가 없는 식별자라면 var는 식별자와 동일한 타입이 되며, const를 포함함
    double x = 5.5;
    decltype(x) w;
    
  • 2단계 : expression이 함수 호출일 경우 var는 함수 리턴형 타입이 됨
    long indeed(int);
    decltype (indeed(3)) m;
    
  • 3단계 : expression이 lvalue일 경우, varexpression 타입을 참조하며, 3단계를 적용하기 위해서는 괄호를 적용해야함
    double xx = 4.4;
    decltype((xx)) r = xx;
    decltype(xx) w = xx; // 3단계가 아닌 1단계
    
  • 4단계 : 위 3단계 중 어떠한 경우도 적용되지 않았을 경우 var는 expression과 동일한 타입이 됨
    int j = 3;
    int &k = j, &n = j;
    decltype(j+6) i1;  // i1은 int형
    decltype(k+n) i2;  // i2는 int형
    

    한가지 이상 선언시 typedefdecltype에 붙여 사용할 수 있음

template<clsss T1, class T2>
?type? gt(T1 x, T2 y)
{
	...
	return x + y;
}

이 경우 x와 y로부터 어떤 타입의 결과를 얻을지 사전에 알 수 없음
리턴 타입으로 decltype(x + y)을 사용 가능할 수 있을 것처럼 보이나, 리턴 타입을 선언할 시점에선 x와 y가 선언되지 않아 범위 안에 들어와있지 않음(컴파일러가 보고 사용할 수 있는 상태가 아님)

따라서 C++11은 함수를 내재(built-in)타입으로 선언하여 정의하는 새로운 구문을 허용
double h(int x, float y)의 원형은 auto h(int x, float y) -> double;로 매개변수 선언 이후로 리턴 타입을 이동시키는 것으로 대체 가능함
이 때 -> doubletrailing return type으로 불림

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

이렇게 할 경우 decltype은 매개변수 선언 이후에 오기 때문에 x와 y가 범위 내에 있게 되어 사용이 가능해짐



연습문제

  1. 함수 코드를 수행하는데 걸리는 시간이 짧고, 빈번하게 호출되는 함수

  2. a) void song(char *name, int times = 1);
    b) 정의는 수정할 필요 없음
    c) 있음, void song(char *name = "O, My Papa", int times = 1);

  3.  void iquote(int n)
     {
         cout << "\"" << n << "\"" << endl;
     }
    
     void iquote(double x)
     {
         cout << "\"" << x << "\"" << endl;
     }
    
     void iquote(const char * str)
     {
         cout << "\"" << str << "\"" << endl;
     }
    
  4. a)
    void print_box(const box & b)
    {
    using namespace std;
    cout << "maekr : " << b.maker << endl;
    cout << "height : " << b.height << endl;
    cout << "width : " << b.width << endl;
    cout << "length : " << b.length << endl;
    cout << "volume : " << b.volume << endl;
    }
    

    b)

    void set_volume(box & b)
    {
    b.volume = b.height * b.width * b.length;
    }
    
  5. 함수 원형을 다음과 같이 변경
    void fill(std::array<double, Seasons> & pa);
    void show(const std::array<double, Seasons> & da);
    main함수에서 fill함수 호출시 fill(expenses)로 변경

  6. a) 디폴트 매개변수와 함수 오버로딩 둘 다 가능함
    double mass(dobule density, double volume = 1.0);
    double mass(double density, double volume);, double mass(double density);
    b) 함수 오버로딩만 가능함
    void repeat(int num, const char * str);, void repeat(const char * str);
    c) 함수 오버로딩만 가능함
    int average(int a, int b);, double average(double a, double b);
    d) 두 함수가 같은 시그니처를 사용하므로 불가능함

  7.  template <class T>
     T Max(T &a, T &b)
     {
         return a > b ? a : b;
     }
    
  8.  template <> box Max(box b1, box b2)
     {
         return b1.volume > b2.volume ? b1.volume : b2.volume;
     }
    
  9. v1 : float형 / v2 : float &형 / v3 : float &형 / v4 : int형 / v5 : double

Tags:

Categories:

Updated: