Advanced C++

[C++] 가변 길이 템플릿 Variadic Template

로파이 2021. 2. 21. 17:44

가변 길이 템플릿 (Variadic Template)

템플릿 매개변수에 대해 그 수를 제한하지 않고 템플릿 함수를 정의할 수 있다.

 

특징

1. 제한하지 않는 매개 변수 수

2. 매개 변수 타입은 당연히 임의적이므로 모두 달라도 된다.

3. 함수 구현 내용에 매개 변수 타입에 따른 처리가 필요하다. (string, int, float, 구조체, 일반 class 등등)

4. 재귀적인 호출로 가변 인자 템플릿 문제를 해결한다.

 

모두의 코드 C++ modoocode.com/290가변 인자 템플릿의 간단한 예제로 소개되어 있는 print 함수는 Python 함수의 print 함수를 구현하고 있다.

/*
	가변 길이 템플릿
	2 이상의 인자를 가지는 가변길이 템플릿 함수를 호출했을 떄,
	첫번째 인자와 나머지 인자들로 분리하여 함수를 호출
    args의 인자 갯수는 0개 여도 된다.
*/
template<typename T>
void print(T arg)
{
	cout << arg << endl;
}

template <typename T, typename... Types>
void print(T arg, Types... args) {
	std::cout << arg << ", ";
	print(args...);
}

1. 템플릿 파라미터 팩

 

template<typename T, typename... Types>

 

템플릿 매개변수 중 T는 추론되는 첫번째 인자고 나머지 인자들은 파라미터 팩으로 추론하겠다는 의미이다.

 

2. 함수 파라미터 팩

 

void print(T arg, Types... args)

 

함수 매개변수 중 T는 추론되는 첫번째 인자고 나머지 인자들은 함수 파라미터 팩으로 분리된다.

 

3. 파라미터 팩을 매개변수로 하는 함수 호출

 

void print(args...)

 

위 함수는 다시 재귀적으로 print 함수를 호출하고 args의 인자 중 첫번째 인자 T와 나머지, 다시 함수 파라미터 팩으로 분리된다.

 

4. 인자가 1개인 기저 템플릿 함수

 

void print(T arg)

 

처음 N개의 인자가 있었다면, func(p0, p1...pn) -> func(p1, p2...pn) -> ... -> func(pn) 까지 호출된다.

마지막 함수는 인자 한 개를 받으므로 첫번째 void print(T arg) 함수가 참조되어 호출된다.

이를 가변 길이 템플릿 함수의 기저 케이스라고 할 수 있다.

기저 케이스가 항상 참조될 수 있도록 파라미터 팩을 가지는 가변인자 길이 템플릿보다 앞서 정의되어야 한다.

 

print 함수는 다음과 같이 출력된다.

print(1.3, 4.5, 3.1, "abc");
// 1.3 4.5 3.1 "abc"

 

가변 인자 길이 템플릿은 함수 매개 변수가 다양하게 다수 필요할때 사용되어 질 수 있다.

예제) 쿼리를 저장하는 데이터 베이스를 구축하는 코드 

class Entry {
public:
	string key;
	int data;
};

class Database {
public:
	class Query {
		friend class Database;
	private:
		string client_name;
		unordered_map<string, int> entries;
	public:
		Query() {}
		void AddEntry(const Entry& e)
		{
			entries.insert(make_pair(e.key, e.data));
		}
		int FindData(const string &key)
		{
			auto found = entries.find(key);
			if (found == entries.end())
				return -1;
			return found->second;
		}
	};
private:
	unordered_map<string, Query*> db;
	Query* buffer = nullptr;
public:
	Database() {}
	~Database()
	{
		for (auto it = db.begin(); it != db.end(); it++)
			delete it->second;
		db.clear();
		if (buffer)
		{
			buffer = nullptr;
		}
	}
	// 기저 케이스에 대한 함수 오버로딩
	void AddQuery(const string& arg)
	{
		buffer->client_name = arg;
		db.insert(make_pair(buffer->client_name, buffer));
	}
	void AddQuery(const Entry& arg)
	{
		buffer->AddEntry(arg);
	}
	// 가변 길이 템플릿을 이용한 쿼리 추가
	// 인자 1개 이상 함수 오버로딩
	template<typename T, typename... Types>
	void AddQuery(const T &arg, const Types&... args)
	{
		if(!buffer)
		{
			buffer = new Query;
		}

		// 기저 케이스 호출
		AddQuery(arg);
		// 재귀함수 호출
		AddQuery(args...);
		
		if (buffer)
		{
			buffer = nullptr;
		}
	}
	Query* FindQuery(const string& key)
	{
		auto found = db.find(key);
		if (found == db.end())
			return nullptr;
		return found->second;
	}
};

기저 함수를 템플릿화 하지 않고 함수 오버로딩으로 구현하였는데, 

템플릿화 하였을 때, 템플릿 매개변수 T에 대한 T -> string 혹은 T-> Entry에 대한 형변환이 필요되어 번거로운 일이 발생한다. 템플릿 매개변수는 임의적인 타입으로 형변환이 거의 불가능하기 때문에 함수 오버로딩으로 데이터 타입에 따라 적절히 함수를 호출할 수 있도록 해준다.

 

사용자 코드에서 다음과 같이 호출되었을 때, AddQuery()가 한 번 나타나게 되고 적절한 쿼리 데이터가 추가된다.

Database db;
vector<string> clients = { "Pete", "Mason","Yeng","Nguyen","Tom" };

Entry idEntry = {"id", 0};
Entry priorityEntry = { "priority", 0 };
Entry validEntry = { "valid", 0 };
for (int id = 1; id < 5; id++)
{
  idEntry.data = 1000 + id;
  priorityEntry.data = id;
  validEntry.data = true;
  db.AddQuery(clients[id - 1], idEntry, priorityEntry, validEntry);
}

 

예제 코드 결과

	int data;
	Database::Query* q = nullptr;
	if (q = db.FindQuery("Mason"))
	{
		if ((data = q->FindData("id")) != -1)
		{
			cout << "Mason's id is " << data << endl;
		}
		if ((data = q->FindData("priority")) != -1)
		{
			cout << "Mason's priority is " << data << endl;
		}
		if ((data = q->FindData("valid")) != -1)
		{
			cout << "Mason's valid is " << data << endl;
		}
	}