일반적인 상황에서 함수는 인자의 자료형과 개수가 고정되어 있다.
Function(int _A)
{
}
위와 같이 함수가 선언, 정의되어 있다면 해당 함수는 int형의 인수를 받으며, 다른 자료형을 인수에 대입하게되면 컴파일 오류가 나거나, 자동으로 int형으로 타입캐스팅이 되어버린다.
물론, 인수로 1개를 초과하는 데이터를 전달할 수도 없다.
하지만, C++에서는 코드의 재활용성을 높이기 위해 템플릿이라는 문법을 제공해주고 있고, 이를 활용하면 하나의 함수에서 다양한 타입의 파라미터를 사용할 수 있게 된다.
template <typename T>
Function(T _A)
{
}
위와 같이 템플릿을 사용한다면, 하나의 함수로 다양한 자료형에 대해 대응할 수 있게되며 코드의 양을 비약적으로 줄일 수 있다.
하지만, 템플릿을 사용한다고 하더라도 대입할 수 있는 인자의 개수는 고정되어 있다.
Function<int>(3, 5, 7);
위와 같이, 선언한 것과 파라미터의 개수가 달라지게 되면 당연히 컴파일 오류가 발생하게 된다.
하지만, 이를 가능하게 만들어주는 문법이 있다. 그 것이 바로 가변인자 템플릿이다.
가변 인자 템플릿
가변 인자 템플릿이란, 파라미터의 개수를 고정하지 않고 가변적으로 사용할 수 있도록 도와주는 문법이다.
생각해보면, printf 같은 함수도 인자의 개수가 고정되어 있지 않고, 원하는 만큼 인수를 전달할 수 있었다.
(실제로는 printf는 C스타일의 가변인자를 사용하였고, C++스타일인 가변 인자 템플릿과는 다르다.)
그렇다면, 어떻게 사용하는지 먼저 확인해보자.
template<typename... Var>
void Function(Var... Arg)
{
}
이런 방식으로 사용하게 된다.
언뜻 보면, 일반적인 템플릿과 똑같아보인다.
하지만, 잘 보면 문법이 다소 다르다.
Var을 보면 typename... 으로 ...이 붙은 채로 작성되어있다.
이 typename...은 여러 개의 인자를 가변적으로 받겠다는 의미이며, 이는 파라미터 팩이라고 칭한다.
#include <string>
template<typename... Var>
void Function(Var... Arg)
{
}
int main()
{
Function<int, long long, float, double>(1, 2, 1.1f, 1.2l);
Function<char, std::string>('A', "ABC");
return 0;
}
위처럼, 인자를 임의로 대입하여도 컴파일 오류가 발생하지 않고, 정상적으로 실행이 된다.
이제, 인자로 받은 함수들을 모두 콘솔에 출력하는 기능을 만들어보자.
가변 인자를 다루는 방법은 크게 두 가지 방법이 있다.
1. 재귀함수
2. 폴드 표현식
재귀함수는 C++ 11부터 사용이 가능한 문법이다.
반면, 폴드표현식은 C++ 17부터 사용이 가능한 문법이다.
언어 버전을 확인하고, 문법을 취사하여 사용하도록 하자.
가변 인자 템플릿 - 재귀함수
먼저, 재귀함수를 활용하는 방법이다.
재귀함수를 사용하여 가변인자 템플릿을 활용하기 위해선 고정 인자가 하나 필요하다.
고정인자가 뭔지는 아래 코드를 보면 바로 알 수 있다.
template<typename T, typename... Types>
void Print(T _FixArg, Types... _VarArgs)
{
}
보면, 앞에 T _FixArg라는 파라미터를 하나 고정으로 두고, 두 번째 파라미터부터 가변 인자를 받고 있다.
이 때, _FixArg가 고정인자이다.
고정 인자는 1개가 될 수도 있고, 2개가 될 수도 있고, 더 많을 수도 있다.
그런데, 고정 인자가 왜 필요하다는걸까?
일단, 아래 코드를 보자.
template<typename T>
void Print_1(T _FixArg)
{
std::cout << _FixArg << " ";
}
template<typename T1, typename... Types>
void Print_1(T1 _FixArg , Types... _VarArgs)
{
std::cout << _FixArg << " ";
Print_1(_VarArgs...);
}
먼저 아래에 있는 가변 인자를 사용하는 함수를 보면, 내부에서 고정 인자를 출력한 뒤, 나머지 가변 인자에 대해 Print_1을 재귀적으로 호출하고 있다.
그 위의 함수를 보면, 가변 인자를 사용하는 아래 함수와 동일하게 인자를 출력해주고 있지만, 가변인자를 사용하지 않고 있다. 함수의 이름은 동일하게 선언하여 오버로딩이 되어있는 상태이다.
이제, 재귀함수가 호출되는 과정을 살펴보자.
이 함수를 메인함수에서 아래와 같이 실행했다고 가정해보자.
int main()
{
Print_1(1, 1.1f, 1.2l, 'A');
return 0;
}
아래는 재귀함수가 호출되는 과정이다.
// Print_1(1, 1.1f, 1.2l, 'A'); << 최초 인수
//_FixArg == 1, VarArgs... -> (1.1f, 1.2l, 'A')
template<typename T1, typename... Types>
void Print_1(T1 _FixArg , Types... _VarArgs)
{
std::cout << _FixArg << " "; // _FixArg인 1이 출력됨
Print_1(_VarArgs...);
}
//Print_1(1.1f, 1.2l, 'A') << 두 번째로 호출되었을 때 인수
//_FixArg == 1.1f, VarArgs... -> (1.2l, 'A')
template<typename T1, typename... Types>
void Print_1(T1 _FixArg , Types... _VarArgs)
{
std::cout << _FixArg << " "; // _FixArg인 1.1f가 출력됨
Print_1(_VarArgs...);
}
//Print_1(1.2l, 'A') << 세 번째로 호출되었을 때 인수
//_FixArg == 1.2l, VarArgs... -> ('A')
template<typename T1, typename... Types>
void Print_1(T1 _FixArg , Types... _VarArgs)
{
std::cout << _FixArg << " "; // _FixArg인 1.2l이 출력됨
Print_1(_VarArgs...);
}
//Print_1('A') << 네 번째로 호출되었을 때 인수
//_FixArg == 'A', VarArgs... -> 없음
//이 때, 어떤 함수가 호출될까?
//가변인자를 사용하지 않는 Print_1 함수가 호출된다.
//두 함수가 오버로딩되어 있다면, 가변 인자를 사용하지 않는 함수가 우선적으로 호출된다.
template<typename T>
void Print_1(T _FixArg)
{
std::cout << _FixArg << " "; //'A'를 출력함
}
//이후, 재귀함수를 더 이상 호출하지 않기 떄문에 출력은 끝이 난다.
아래는 동일한 내용을 그림으로 표현한 것이다.
기본적으로, 재귀함수를 돌면서 값을 출력해주고 있기 때문에, 고정인자가 없다면 재귀 함수가 끝나지 않는다.
(인자의 개수가 줄어들지 않기 때문에)
또한, 고정인자를 사용하였더라도, 가변인자가 없는 함수를 오버로딩하지 않았다면, 재귀함수의 종료 시점이 존재하지 않아 마지막 문자를 계속 출력하다가 스택 오버플로우가 발생할 것이다.
가변인자가 없는 함수에선 재귀호출을 하고 있지 않기 때문에, 가변인자가 없는 함수가 호출되는 시점이 재귀함수의 종료 시점이라고 파악할 수 있는 것이다.
실행하면 콘솔 창에선 아래와 같이 인자들이 순서대로 출력되는 것을 볼 수 있다.
이 번엔, 인자들을 모두 더하는 함수를 만들어보자.
간단하게 아래와 같이 만들어 볼 수 있다.
template<typename T>
int Sum(T _FixArg)
{
return _FixArg;
}
template<typename T1, typename... Types>
int Sum(T1 _FixArg, Types... _VarArgs)
{
int Result = _FixArg + Sum(_VarArgs...);
return Result;
}
int main()
{
std::cout << Sum(1, 2, 3, 4);
return 0;
}
실행결과는 아래와 같다.
정상적으로 모든 인자를 더해준 값이 출력되었다.
그런데, 여기서 주의할 점이 하나 있다. 너무나 당연한 말이지만, 여러 자료형에 대해서 동일한 작업을 반복하는 만큼 연산자가 어떻게 오버로딩 되어있는가를 확실히 파악해야 한다.
예를 들어, A라는 클래스가 있고, B라는 클래스가 있는 상태에서
Sum(A, B, 1, 4, 2.5f, 'A') 이런식으로 호출한다면, A와 B의 합이 우리가 원하는 결과와 같은지 확실하게 확인해야 한다.
또, 2.5f와 'A'의 합은 우리 의도와 맞는지, 정확히 생각하고 실행해야 한다.
그렇기 때문에, 가변인자 템플릿을 사용하는 함수라면 함수의 내부 동작이 어떻게 이루어지는지 파악하고서 올바른 자료형을 판단해서 사용하여야 한다.
(사실, 특정 자료형에 대해서만 의도대로 작동하는 함수라면, 그건 가변인자 템플릿으로 만들지 않는게 맞다.)
가변 인자 템플릿 - 폴드 표현식
폴드 표현식은 고정 인자를 사용하지 않고, 재귀 함수를 사용하지 않고 가변 인자에 대응하는 방법이다.
C++ 17부터 사용할 수 있게 되었다.
먼저 폴드 표현식의 코드부터 보자.
아래는 모든 가변 인자를 더한 값을 반환하는 함수의 코드이다.
template<typename ...Types>
int Sum(Types... _VarArgs)
{
return (_VarArgs + ...);
}
int main()
{
std::cout << Sum(1, 2, 3, 4);
return 0;
}
아주 단순하고 간단하고 짧아졌다!
보면, 함수를 오버로딩 하지도 않았고 재귀함수를 사용하지도 않았고 고정인자를 사용하지도 않았다.
내부의 코드도 단 1줄이 되었고, 가독성도 상당히 좋아졌다.
내부의 (_VarArgs + ...); 이 의미가 모든 인자를 더하라는 뜻이 된다.
그런데, 뺄셈의 경우 아래와 같은 문제가 발생할 수 있다.
template<typename ...Types>
int Sub_1(Types... _VarArgs)
{
return (_VarArgs - ...);
}
template<typename ...Types>
int Sub_2(Types... _VarArgs)
{
return (... - _VarArgs);
}
int main()
{
std::cout << Sub_1(1, 2, 3, 4);
std::cout << std::endl;
std::cout << Sub_2(1, 2, 3, 4);
return 0;
}
위의 코드를 실행해보면?
서로 다른 값이 출력된다.
...의 위치를 앞에 두느냐 뒤에 두느냐에 따라 값이 달라지는 것이다.
왜 그러는 것일까?
폴드 계산식은 말 그대로, 접었다는 뜻이다.
(_VarArgs - ...)을 전개해보면 아래와 같다.
즉, 가변 인자를 모두 전개한 뒤, 마지막 인자들부터 차례대로 빼주고 있다는 것이다.
위의 코드의 인수인 1, 2, 3, 4를 기준으로 본다면
1 - (2 - (3 - 4)) 를 호출하고 있는 것이다.
반면, (... - _VarArgs) 는 어떨까?
이렇게, 앞에서부터 갚을 빼며 전개한다.
즉, ...을 이항연산에서 앞에 두느냐 뒤에 두느냐에 따라 계산 순서가 달라지는 것이다.
덧셈, 곱셈같은 경우는 교환법칙이 성립하기 때문에 순서가 중요하지 않지만, 뻴셈, 나눗셈 등의 경우엔 교환법칙이 성립하지 않기 때문에 주의를 하여 사용해야 한다.
폴드 표현식은 여러 가지 사용법이 있다.
아래와 같이도 사용 가능하다.
template<typename ...Types>
int Sub_3(Types... _VarArgs)
{
return (1 - ... - _VarArgs);
}
template<typename ...Types>
int Sub_4(Types... _VarArgs)
{
return (_VarArgs - ... - 1);
}
위의 함수는 맨 앞에 1이라는 인자가 있다고 가정하고 계산을 진행하는 경우이며,
아래 함수는 맨 뒤에 1이라는 인자가 있다고 가정하고 계산을 진행하는 것이다.
출력을 하고 싶을 땐, 아래와 같이 사용할 수도 있다.
template<typename ...Types>
void PrintAll(Types... _VarArgs)
{
(std::cout << ... << _VarArgs);
}
여기까지 가변 인자 템플릿에 대해 알아보았다.
'C++ > C++' 카테고리의 다른 글
C++ - L-Value vs R-Value (1) | 2024.06.09 |
---|---|
C++ - 얕은 복사 vs 깊은 복사 (1) | 2024.04.18 |
C++ - 코루틴 (coroutine) (0) | 2024.04.14 |
C++ - string_view (읽기 전용 문자열 컨테이너) (0) | 2024.04.11 |
C++ - SSO (Small String Optimization) (0) | 2024.04.11 |