본문 바로가기
프로그래밍/C++

C++ ] 가변 인자 템플릿(Variadic Templates) 활용

by eteo 2024. 9. 2.

 

 

C++ 11에서 도입된 가변 인자 템플릿이란 함수가 불특정 다수의 여러 인자를 받을 수 있게 해주는 기능이다. 가변 인자 템플릿은 C의 stdarg.h에 있는 가변 인자 매크로들과 비슷한 역할을 한다고 볼 수 있는데 그 사용법에 있어서는 큰 차이가 있다. 이에 대해 한번 알아보자.

 

 

 

1. 템플릿(Templates)이란?

템플릿은 C++에서 함수나 클래스를 정의할 때 그 타입을 일반화하여 코드 재사용성을 높이는 기능이다. 템플릿을 사용하면 특정 데이터 타입에 종속되지 않고, 다양한 타입에 대해 동일한 코드 구조를 사용할 수 있다.

 

템플릿을 선언할 때는 다음과 같은 구문을 사용한다. 여기서 T는 타입 매개변수로 함수나 클래스가 다양한 데이터 타입에 대해 동작하도록 일반화할 수 있게 해준다.

 

template <typename T>

 

 

예를 들어 다음과 같은 코드가 있다고 해보자.

#include <iostream>
#include <string>

using namespace std;

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    cout << add(3, 4) << endl;       // 출력: 7
    cout << add(3.5, 4.5) << endl;   // 출력: 8
    cout << add(string("Hello, "), string("world!")) << endl; // 출력: Hello, world!
    return 0;
}

 

위 코드에서 add 함수는 정수, 실수 등 다양한 타입의 덧셈을 처리할 수 있다. T는 타입 매개변수로, 함수가 호출될 때 실제 타입으로 대체된다. add(3, 4)가 호출될 때는 T가 int로, add(3.5, 4.5)가 호출될 때는 T가 double로, add(string("Hello, "), string("world!"))가 호출될 때는 T가 string으로 대체된다.

이 템플릿 함수가 다양한 데이터 타입에 대해 작동할 수 있는 이유는, 템플릿 함수에서 사용된 + 연산자와 출력 연산자 << 가 여러 데이터 타입에 대해 오버로딩되어 있기 때문이다.

 

 

 

❗템플릿 함수는 구현부와 선언부를 .cpp 파일과 .h 파일로 분리할 수 없다.

 

템플릿 함수는 컴파일 시점에 컴파일러가 템플릿 함수의 정의를 알고 있을 필요가 있다. 템플릿 함수가 특정 타입과 함께 호출되는 곳에 컴파일러는 사용된 실제 타입을 기반으로 템플릿을 인스턴스화하여 그 타입에 맞는 구체적인 함수 코드를 생성하고, 생성된 함수 코드는 일반 함수와 다를바 없이 컴파일 된다.

때문에 만약 구현부가 소스파일(.cpp)에 있다면 템플릿 인스턴스화가 올바르게 이루어지기 어렵다. 따라서 템플릿 함수의 구현을 헤더 파일(.h)에 넣으면 전처리 단계에서 헤더 파일이 포함되고, 이 헤더 파일을 포함하는 모든 소스 파일에서 템플릿 함수의 구현을 알 수 있게되어 올바르게 인스턴스화 된다.

 

 

 

 

2. 가변 인자 템플릿(Variadic Templates)이란?

가변 인자 템플릿은 인자의 개수가 정해지지 않은 템플릿을 의미한다. 이를 통해 함수나 클래스를 정의할 때 인자의 개수에 제한을 두지 않고 유연하게 정의할 수 있다. 가변 인자 템플릿은 ...을 사용하여 타입 매개변수를 정의하며 아래와 같이 선언된 경우 Args...는 0개 이상의 템플릿 인자를 받아들일 수 있다.

 

template<typename... Args>

 

단, 템플릿 함수와 오버로딩된 함수가 둘 다 존재할 때, 컴파일러는 템플릿 함수보다 매개변수와 그 타입이 정확히 일치하는 구체적인 함수를 더 높은 우선순위로 선택하여 호출하게 된다.

 

 

 

 

3. 가변 인자 템플릿 활용 예시

가변 인자 템플릿은 재귀 호출을 구현해서 사용하는 것이 일반적인데 보통 오버로딩을 사용하여 재귀 호출의 종료 조건을 처리한다. 즉, 가변 인자 템플릿을 사용하는 경우 다음과 같이 두 가지 함수로 구성된다.

 

1. 재귀 호출의 종료를 위한 종료 조건 함수 : 인자의 개수가 하나만 남았거나 더 이상 없을 때 호출된다.

2. 재귀 호출을 수행하는 템플릿 함수 : 템플릿 매개변수 팩(Args...)을 받아들여 인자를 하나씩 처리하면서 나머지 인자들에 대해 재귀적으로 호출을 이어나가는 함수이다.

 

 

 

가변 인자 템플릿을 활용한 print 함수 구현

 

가변 인자 템플릿과 재귀 호출을 사용하여 Python의 print와 같은 기능을 하는 함수를 만들어 보자.

 

#include <iostream>

using namespace std;

// 인자가 0개일 때 호출되는 함수 (종료 조건)
void print() {
    cout << endl;
}

// 가변 인자 템플릿: 인자가 1개 이상일 때
template<typename T, typename... Args>
void print(const T& first, const Args&... args) {
    cout << first << " ";
    print(args...);
}

int main() {
    print("Hello,", "world!", 123, 4.56);
    // 출력: Hello, world! 123 4.56 

    return 0;
}

 

동작 방식을 살펴보면 다음과 같다.

  • 첫 번째 호출 : print("Hello,", "world!", 123, 4.56), fisrt = {"Hello,"}, args... = {"world!", 123, 4.56} 
  • 두 번째 호출 : print("world!", 123, 4.56), fisrt = {"world!"}, args... = {123, 4.56}
  • 세 번째 호출 : print(123, 4.56),  fisrt = {123}, args... = {4.56}
  • 네 번째 호출 : print(4.56),  fisrt = {4.56}, args... = {}
  • 다섯 번째 호출 : print(), 종료

각 호출에서 인자의 개수가 하나씩 줄어들면서 인자만 남지 않게 되면 종료 조건 함수가 호출되어 재귀가 종료되고 최종 결과가 만들어진다.

 

 

 

 

가변 인자 템플릿을 활용한 join 함수 구현

 

다음은 Python의 os.path.join()함수를 모의해 구현해보자. join 함수는 여러 문자열이 인자로 들어왔을 때 그 사이에 경로 구분자를 넣어 합쳐진 문자열을 리턴한다.

 

#include <iostream>
#include <string>

using namespace std;

// 인자가 1개일 때 호출되는 함수 (종료 조건)
string join(const string& arg) {
    return arg;
}

// 가변 인자 템플릿: 인자가 2개 이상일 때
template<typename... Args>
string join(const string& first, const Args&... args) {
    return first + "\\" + join(args...);
}

int main() {
    string result = join("C:", "myFolder", "mySubFolder", "myText.txt");
    cout << result << endl; // 출력 "C:\myFolder\mySubFolder\myText.txt"

    return 0;
}

 

동작 방식을 살펴보면 다음과 같다.

  • 첫 번째 호출 : join("C:", "myFolder", "mySubFolder", "myText.txt"), fisrt = {"C:"}, args... = {myFolder", "mySubFolder", "myText.txt"} 
  • 두 번째 호출 : join("myFolder", "mySubFolder", "myText.txt"), fisrt = {"myFolder"}, args... = {"mySubFolder", "myText.txt"}
  • 세 번째 호출 : join("mySubFolder", "myText.txt"),  fisrt = {"mySubFolder"}, args... = {"myText.txt"}
  • 네 번째 호출 : join("myText.txt"), 종료