가변 길이 템플릿(Variadic template)
템플릿을 사용하면 가변 길이의 인자를 받을 수 있다.
Copy #include <iostream>
template <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
template <typename T, typename... Types>
void print(T arg, Types... args) {
std::cout << arg << ", ";
print(args...);
}
int main() {
print(1, 3.1, "abc");
print(1, 2, 3, 4, 5, 6, 7);
}
파라미터 팩(parameter pack)
typename...
을 템플릿 파라미터 팩(parameter pack) 이라고 부른다.
-> 템플릿 파라미터 팩의 경우 0개 이상의 템플릿 인자들을 나타낸다.
Copy template <typename T, typename... Types>
마찬가지로 함수에 인자로 ... 로 오는 것을 함수 파라미터 팩 이라고 부른다.
-> 템플릿 파라미터 팩과 마찬가지로, 0개 이상의 함수 인자를 나타낸다.
Copy void print(T arg, Types... args) {
다음과 같은 print 함수 호출을 생각해볼 때, C++ 컴파일러는 위에서 정의 된 2개의 print 함수 정의를 보면서 어떤 것을 사용할 것인지 선택한다.
-> 첫번째 함수는 1개의 인자만 받기 때문에, 제외되고 두번째 함수를 선택하게 된다.
Copy print(1, 3.1, "abc");
따라서 위 코드는 다음과 같이 처리된다. 그 다음 재귀적으로 인자 2개를 받는 print 를 호출하였다.
Copy void print(int arg, double arg2, const char* arg3) {
std::cout << arg << ", ";
print(arg2, arg3);
}
두번째 호출 된 print 함수는 다음과 같이 처리된다.
Copy void print(double arg, const char* arg2) {
std::cout << arg << ", ";
print(arg2);
}
마지막으로 print 함수는 첫번째 정의된 인자가 1개 있는 print 함수로 처리된다.
-> 앞서 말했듯 파라미터 팩은 0개 이상의 인자들을 나타내기 때문에, 현재 정의된 두개 함수 모두 호출될 수 있다.
-> 하지만 파라미터 팩이 없는 함수의 우선순위가 더 높기 때문에, 첫번째 함수가 호출된다 .
Copy void print(const char* arg) {
std::cout << arg << std::endl;
}
함수 정의 순서를 바꾼다면?
위 코드에서 print 함수 정의 순서를 바꾼다면 컴파일 에러가 발생한다.
C++ 컴파일러는 함수를 컴파일 시에, 자신의 앞에 정의되어 있는 함수들 밖에 보지 못하기 때문이다.
-> 따라서 void print(T arg, Types... args)
이 함수를 컴파일 할 때, void print(T arg)
이 함수가 존재함을 모르는 셈이다.
그렇게 된다면, 마지막에 print("abc") 의 오버로딩을 찾을 때, 파라미터 팩이 있는 함수를 택하게 되는데, 그 함수 안에서 print() 가 호출되어 컴파일 에러가 발생한다. (print() 를 정의하지 않았다)
Copy #include <iostream>
template <typename T, typename... Types>
void print(T arg, Types... args) {
std::cout << arg << ", ";
print(args...);
}
template <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
int main() {
print(1, 3.1, "abc");
print(1, 2, 3, 4, 5, 6, 7);
}
임의의 개수의 문자열을 합치는 함수
가변 길이 템플릿을 활용한 또 다른 예시로, 임의의 길이의 문자열을 합쳐주는 함수를 들 수 있다.
예를 들어서 std::string
에서 문자열을 합치기 위해서는 아래와 같이 사용해야 했다.
Copy concat = s1 + s2 + s3;
Copy concat = s1.operator+(s2).operator+(s3);
문제는 s2 를 더할 때 메모리 할당이 발생하고, s3 를 더할 때 메모리 할당이 또 한번 발생한다는 것이다.
-> 합쳐진 문자열의 크기는 미리 알 수 있기에 차라리 한 번에 필요한 만큼 메로리를 할당해버리는 것이 훨씬 낫다.
아래와 같이 코드를 변경한다면, 메모리 할당 1번으로 끝낼 수 있다.
Copy std::string concat;
concat.reserve(s1.size() + s2.size() + s3.size()); // 여기서 할당 1 번 수행
concat.append(s1);
concat.append(s2);
concat.append(s3);
위와 같은 동작을 하는 함수를 다음과 같이 정의할 수 있지만, StrCat
함수가 임의의 개수의 인자를 받아야 된다는 것이 걸린다.
-> 이 때, 가변 길이 템플릿을 사용하면 된다.
Copy std::string concat = StrCat(s1, "abc", s2, s3);
첫번째 시도
첫번째 시도는 재귀 호출을 통해서 문자열을 계속 더하는 식의 시도를 할 수 있다.
-> 위에서 파라미터 팩이 어떻게 동작하는지를 생각한다면 이해하기 쉽다.
Copy #include <iostream>
#include <string>
template <typename String>
std::string StrCat(const String& s) {
return std::string(s);
}
template <typename String, typename... Strings>
std::string StrCat(const String& s, Strings... strs) {
return std::string(s) + StrCat(strs...);
}
int main() {
// std::string 과 const char* 을 혼합해서 사용 가능하다.
std::cout << StrCat(std::string("this"), " ", "is", " ", std::string("a"),
" ", std::string("sentence"));
}
하지만 첫번쨰 시도는 하나의 문제가 있는데, 그것은 바로 std::string
의 operator+
를 매번 호출하는 셈이 되어, StrCat
의 전달된 인자가 5개라면, 메모리 할당이 최대 5번씩 일어날 수 있다는 것을 의미한다.
효율적으로 StrCat
을 구현하기 위해서는 합쳐진 문자열의 길이를 먼저 계산한 뒤에 메모리를 할당하고, 그 다음에 문자열을 붙이는 것이 좋다.
두번째 시도
그렇다면 먼저 합쳐진 문자열의 길이를 구한는 함수를 만들어야 할 것이다.
-> 물론 이 역시 가변길이 템플릿을 사용해보자
GetStringSize
함수는 그냥 임의의 개수의 문자열을 받아서 각각의 길이를 더한 것들을 리턴하게 된다.
-> 참고로 const char*
와 std::string
모두 잘 작동하게 하기 위해서 인자 1개만 받는 GetStringSize
의 오버로드를 각각의 경우에 대해서 준비했다.
Copy size_t GetStringSize(const char* s) {return strlen(s);}
size_t GetStringSize(const std::string& s) {return s.size();}
template<typename String, typename... Strings>
size_t GetStringSize(const String& s, Strings... strs) {
return GetStringSize(s) + GetStringSize(strs...);
}
그렇다면 수정된 StrCat 의 모습은 아래와 같을 것이다.
-> GetStringSize 를 통해 확인 된 size 값을 사용해 concat_str 개체의 메모리 공간을 미리 할당한다.
Copy template <typename String, typename... Strings>
std::string StrCat(const String& s, Strings... strs) {
// 먼저 합쳐질 문자열의 총 길이를 구한다.
size_t total_size = GetStringSize(s, strs...);
// reserve 를 통해 미리 공간을 할당해 놓는다.
std::string concat_str;
concat_str.reserve(total_size);
concat_str = s;
// concat_str 에 문자열들을 붙인다.
AppendToString(&concat_str, strs...);
return concat_str;
}
그 다음에는 이제 concat_str 뒤에 나머지 문자열들을 가져다 붙어야 한다. 이 과정을 수행하는 함수를 AppendToString이라고 하고 구현하면 다음과 같다.
Copy void AppendToString(std::string* concat_str) { return; }
template <typename String, typename... Strings>
void AppendToString(std::string* concat_str, const String& s, Strings... strs) {
concat_str->append(s);
AppendToString(concat_str, strs...);
}
Copy #include <cstring>
#include <iostream>
#include <string>
size_t GetStringSize(const char* s) { return strlen(s); }
size_t GetStringSize(const std::string& s) { return s.size(); }
template <typename String, typename... Strings>
size_t GetStringSize(const String& s, Strings... strs) {
return GetStringSize(s) + GetStringSize(strs...);
}
void AppendToString(std::string* concat_str) { return; }
template <typename String, typename... Strings>
void AppendToString(std::string* concat_str, const String& s, Strings... strs) {
concat_str->append(s);
AppendToString(concat_str, strs...);
}
template <typename String, typename... Strings>
std::string StrCat(const String& s, Strings... strs) {
// 먼저 합쳐질 문자열의 총 길이를 구한다.
size_t total_size = GetStringSize(s, strs...);
// reserve 를 통해 미리 공간을 할당해 놓는다.
std::string concat_str;
concat_str.reserve(total_size);
concat_str = s;
AppendToString(&concat_str, strs...);
return concat_str;
}
int main() {
// std::string 과 const char* 을 혼합해서 사용 가능하다.
std::cout << StrCat(std::string("this"), " ", "is", " ", std::string("a"),
" ", std::string("sentence"));
}
sizeof...
sizeof
연산자는 인자의 크기를 리턴하지만 파라미터 팩에 sizeof...
를 사용할 경우 전체 인자의 개수를 리턴하게 된다.
예를 들어 원소들의 평균을 구하는 함수를 생각해보자.
Copy #include <iostream>
// 재귀 호출 종료를 위한 베이스 케이스
int sum_all() { return 0; }
template <typename... Ints>
int sum_all(int num, Ints... nums) {
return num + sum_all(nums...);
}
template <typename... Ints>
double average(Ints... nums) {
return static_cast<double>(sum_all(nums...)) / sizeof...(nums);
}
int main() {
// (1 + 4 + 2 + 3 + 10) / 5
std::cout << average(1, 4, 2, 3, 10) << std::endl;
}
Fold Expression
C++11 에서 도입 된 가변 길이 템플릿은 매우 편리하지만 한 가지 단점이 존재한다. 재귀 함수 형태로 구성해야 하기 때문에, 반드시 재귀 호출 종료를 위한 함수를 따로 만들어야 한다는 것이다.
하지만 C++17 에서 새롭게 도입 된 Fold 형식을 사용한다면, 이를 훨씬 간단하게 표현할 수 있다.
단항 Fold 형식
Copy #include <iostream>
template <typename... Ints>
int sum_all(Ints... nums) {
return (... + nums);
}
int main() {
// 1 + 4 + 2 + 3 + 10
std::cout << sum_all(1, 4, 2, 3, 10) << std::endl;
}// Some code
아래 코드는 C++17 에서 추가된 Fold 형식으로,
위 코드는 아래와 같이 컴파일러에서 해석된다.
Copy return ((((1 + 4) + 2) + 3) + 10);
여기서 주의해야 할 점은 Fold 식을 사용할 때, 꼭 ()
로 감싸주어야 한다는 것이다.
Copy return ( ... + nums); // success
return ... + nmms; // compile error
이항 Fold 형식
Copy #include <iostream>
template <typename Int, typename... Ints>
Int diff_from(Int start, Ints... nums) {
return (start - ... - nums);
}
int main() {
// 100 - 1 - 4 - 2 - 3 - 10
std::cout << diff_from(100, 1, 4, 2, 3, 10) << std::endl;
}
아래 코드처럼 이항 Fold 형식을 사용할 수 있고,
Copy return (start - ... - nums);
위 코드는 아래와 같이 컴파일러에서 해석된다.
Copy return (((((100 - 1) - 4) - 2) - 3) - 10);
Fold 의 추가적인 기능
,
연산자를 사용하면 각각의 인자들에 대해 원하는 식을 실행할 수 있다.
Copy #include <iostream>
class A {
public:
void do_something(int x) const {
std::cout << "Do something with " << x << std::endl;
}
};
template <typename T, typename... Ints>
void do_many_things(const T& t, Ints... nums) {
// 각각의 인자들에 대해 do_something 함수들을 호출한다.
(t.do_something(nums), ...);
}
int main() {
A a;
do_many_things(a, 1, 3, 2, 4);
}
아래 코드처럼 Fold 형식을 사용할 수 있고,
Copy (t.do_something(nums), ...);
위 코드는 아래와 같이 컴파일러에서 해석된다.
Copy t.do_something(1);
t.do_something(3);
t.do_something(2);
t.do_something(4);