C++/[ecourse] C++ Template
8. 가변인자 템플릿
헛둘이
2022. 10. 6. 17:10
가변인자 템플릿
- n개의 인자를 받는 템플릿
- 0개일 수도 있고, 여러 개일 수도 있음
- 인자와 타입의 개수가 정해지지 않은 형태의 템플릿
- tuple의 경우 가변인자 클래스 템플릿으로 되어 있음
- 인자를 0개를 줘도, 1개를 줘도, n개를 줘도 에러가 발생하지 않는다.
// 가변인자 클래스템플릿
template<typename ... Types>
class xtuple
{
};
// 가변인자 함수템플릿
template<typename ... Types>
void foo(Types ... args)
{
}
- 타입의 경우 1개가 아니므로 관례상 Types로 통일
- 받는 인자쪽도 arg가 아닌 복수형 args로 통일
Parameter Pack
- C++11부터 지원
- 가변인자를 받는 함수의 경우 Parameter Pack을 통해 args를 사용할 수 있다.
- 인자가 몇 개 있는지 알고 싶으면 sizeof... 를 사용하면 됨
- sizeof...에는 타입도 넣을 수 있다.
sizeof...(Types);
sizeof...(args);
Parameter Pack을 통해 다른 함수로 인자를 보낼 경우
- goo(args) 이렇게 보낼 순 없으나 goo(args...)로 풀 수 있음
- goo(args...) == goo(1, 3.4, "AAA")
Pack 심화
template<typename ... Types>
void foo(Types ... args)
{
int x[] = { (++args)...}; // pack expansion
for (auto n : x)
std::cout << n << std::endl;
}
int main()
{
foo(1, 2, 3);
}
- int x[] = (++args)...; 는 ... 앞에 걍 파라미터 팩이 오는 게 아니라 파리미터팩을 사용한 패턴이 옴
- int x[] = { hoo(args)... }; 이것은 함수를 이용한 패턴인데, ...을 함수 인자 내부에 쓰는 게 아닌 밖에 쓴다.
- 함수 자체를 패턴이라고 생각하는 게 편할듯
int print(int a) {
std::cout << a << std::endl;
return 0;
}
template<typename ... T>
void test(T ... args)
{
print(args)...;
// 팩을 확장할땐 함수 인자에서 하거나, 배열 만들때 확장이 됨 {...}
// Pack Extension은 아무데서나 할 수 있는게 아님
int x[] = {0, print(args)...};
// 인자가 없는 경우 이렇게 사용이 가능하다.
int x[] = {0, (print(args), 0)... };
// print가 void를 리턴하는 경우 이렇게 사용하면 에러가 나지 않음
initializer_list<int> e = ~~
// 전체 수식의 결과는 콤마 뒤에 있는 값을 쓰겠다는 뜻
// 이니셜라이저리스트에서 받으면 아무것도 보내지 않아도 에러가 나지 않음
}
int main()
{
test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
- 결과적으로 비교적 깔끔하지 않은데 C++17에서 지원하는 폴드 익스프레션을 통해 보완 가능함
Types에 대한 Pack Expansion
- args 뿐만 아니라 Types도 Pack Expansion이 가능하다.
#include <iostream>
#include <algorithm>
#include <tuple>
template<typename ... Types>
void foo(Types ... args)
{
// ...의 위치를 잘 파악해야 함
std::pair<Types...> p1;
std::tuple<Types...> t1;
std::tuple<std::pair<Types...>> t2; // tuple<pair<int, double>> 이므로 문제 없음
std::tuple<std::pair<Types>...> t3; // tuple<pair<int>, pair<double>> pair가 인자 2개를 받아야 하므로 error!
std::tuple<std::pair<int, Types>...> t4; // tuple<pair<int, int>, pair<int, double>> 이상 없음!
std::pair<std::tuple<Types...>> p2; // pair<tuple<int, double>> 인자가 1개이므로 error!
std::pair<std::tuple<Types>...> p3; // pair<tuple<int>, tuple<double>> pair의 인자가 2개이므로 문제 없음
}
int main()
{
foo(1, 3.4);
}
- ...의 위치가 괄호 안이냐 밖이냐에 따라 어떻게 Expansion할 것인지 의미가 달라짐
// Types == (int, double)
// std::pair<Types...> 처럼 안에서 ...을 적어주면 안에서 Expansion
std::tuple<std::pair<Types...>> -> std::tuple<std::pair<int, double>>
// std::pair<Types>... 처럼 밖에서 ...을 적어주면 Types의 요소를 하나씩 ... 안에 담아서 Expansion
std::tuple<std::pair<Types>...> -> std::tuple<std::pair<int>, std::pair<double>>
args의 각 요소를 꺼내는 방법
#include <iostream>
#include <algorithm>
#include <tuple>
template<typename ... Types>
void foo(Types ... args)
{
//각 요소를 꺼내는 방법
//int x[] = { args... }; //그러나 다른 타입이 있을 수 있으므로 이러면 안됨
//tuple 사용법
std::tuple<Types...> tp(args...);
std::cout << std::get<0>(tp) << std::endl;
std::cout << std::get<1>(tp) << std::endl;
std::cout << std::get<2>(tp) << std::endl;
}
int main()
{
foo(1, 3.4, "AAA");
}
- tuple의 각 요소를 꺼내는 방법
- std::get<인자위치>(tuple)을 사용해서 꺼낼 수 있다.
클래스 템플릿의 재귀호출
#include <iostream>
#include <algorithm>
#include <tuple>
void foo() {}
template<typename T, typename ... Types>
void foo(T value, Types ... args)
{
std::cout << value << std::endl;
// foo의 재귀호출
foo(args...);
//마지막 불리는건 템플릿 foo가 아니라 일반함수 foo
// 이 방식의 단점은 함수가 많이 만들어짐
// foo(1, 3.4, "AAA")
// foo(3.4, "AAA")
// foo("AAA")
// foo() <- 일반함수 foo
}
- foo에서 foo를 호출하는데 인자로 args만 넘기므로 현재 맨 앞의 인자를 뺀 나머지를 넘기는 꼴이 된다.
- 계속 인자가 한 개씩 줄다가 마지막엔 foo()를 호출하는데 이는 5번째줄에 선언된 void foo() {}를 호출하게 됨
- 이 방식의 단점은 불리는 foo가 인자가 다 다르므로 전부 다 다른 함수라고 봐야 함(함수 오버로딩)
- 결국 다 다른 함수라는 뜻이므로 함수가 불필요하게 많아질 수 있음
Fold Expression
- 이항연산자를 사용해서 Parameter Pack의 요소에 연산 수행
#include <iostream>
#include <algorithm>
#include <tuple>
void foo() {}
template<typename ... Types>
int foo(Types ... args)
{
int x[] = { args... }; // pack expansion
int n = (args + ...); // // c++17부터 지원되는 fold expression
// 반드시 괄호로 묶어줘야 함
int n = (... + args); // (((1 + 2) + 3) + 4)
int n = (10 + ... + args); //초기값 10을 주는 형태 (((10 + 1) + 2) + 3) + 4
// 반대도 마찬가지
return n;
}
int main()
{
int n = foo(1, 2, 3, 4);
std::cout << n << std::endl;
}
- 괄호를 통해 Parameter Pack을 언팩할 수 있다는 점에서 눈여겨볼 필요가 있음
- C++17부터 지원
- 위 코드를 응용해서 아래와 같이 사용할 수 있다.
template<typename ... Types>
void foo(Types ... args)
{
(std::cout << ... << args);
}
- std::cout은 초기값으로 놓고 연산자는 <<으로 처리
- args의 모든 요소를 cout으로 출력함
- 이전의 템플릿 특수화버전에선 n개의 인자를 갖는 함수만 특정해야 했었음
- 근데 이렇게 하면 모든 인자에 대해 받을 수 있으므로 범용성이 더 좋음
template<typename T>
struct result_type
{
typedef T type;
};
template<typename R, typename ... Types>
struct result_type<R(Types...)>
{
typedef R type;
};
template<typename T>
void foo(const T& t)
{
typename result_type<T>::type ret;
std::cout << typeid(ret).name() << std::endl;
}
인자가 제대로 처리되지 않았을 때
- 받는 인자가 함수가 아니라 정수나 실수의 경우 에러를 발생시키는 방법
template<typename T>
struct result_type
{
static_assert(std::is_function<T>::value, "error");
};
- Primary template에서 T를 검사해서 이렇게 처리할 수 있다.
- 이외에도 primary template에서 선언만 제공해서 에러를 띄우는 경우들도 많이 보임
- 흔히 사용하는 기법
함수 인자타입 구하기
double hoo(short a, int b, char ch) { return 0; }
template<size_t N, typename T>
struct argument_type
{
typedef T type;
};
template<typename R, typename A1, typename ... Types>
struct argument_type<0, R(A1, Types...)>
{
typedef A1 type;
};
template<size_t N, typename R, typename A1, typename ... Types>
struct argument_type<N, R(A1, Types...)>
{
typedef typename argument_type<N-1, R(Types...)>::type type;
};
template<typename T>
void foo(const T& t)
{
typename argument_type<2, T>::type ret;
std::cout << typeid(ret).name() << std::endl;
}
- 인자 타입을 구하는 방법
- 기존 R(A1, A2)의 방법에서 가변인자를 적용해서 모든 함수타입에 적용 가능하도록 변경
- 2를 넘기게 되면 아래와 같은 로직으로 진행됨
- typedef typename argument_type<N-1, R(Types...)>::type type;에서 N-1이 0이 될 때까지 재귀호출
- N-1하면서 Types만 넘기므로 N이 0이되는 순간 N번째 인자는 A1이 되고 0 특수화 버전에서 A1을 반환하게 됨