서론
다음 교재의 1장에 등장하는 생소한 문법 일부를 정리한다.
존 캐리 외 2인, 코딩테스트를 위한 자료구조와 알고리즘 with C++
다음의 세 개념을 설명한다.
'...'이 사용되는 함수에 대한 이해를 위함.
가변 템플릿에 사용되는 '...'의 이해를 위함.
함수 정의 부분에서 사용된 '->'연산자의 이해.
함수의 반환 타입을 명시적으로 적는 문법의 이해.
본론
특정한 매개변수를 가변 개수로 선언할 수 있도록 하는 기능이다.
가변 길이 매개변수는 '...'을 통하여 선언하며, 이렇게 생성된 매개변수를 제어하기 위해서는 <cstdarg>헤더에 정의되어 있는 함수들을 사용하게 된다.
gpt에게 부탁하여 작성한 다음 예시를 보라.
#include <iostream>
#include <cstdarg>
// 가변 인자를 받는 함수 정의
void printNumbers(int count, ...) {
va_list list;
va_start(list, count); // 가변 인자 리스트 초기화
for (int i = 0; i < count; ++i) {
int num = va_arg(list, int); // 다음 인자 가져오기
std::cout << num << ' ';
}
va_end(list); // 가변 인자 리스트 정리
}
int main() {
printNumbers(5, 10, 20, 30, 40, 50); // 출력: 10 20 30 40 50
return 0;
}
va_list 타입의 객체를 만들어서, va_start(), va_arg(), va_end()를 사용하여 함수가 받은 모든 인수에 접근하고 있다.
다만 추가로 알아야 할 것이 있다.
gpt가 작성한 코드에서는 함수의 매개변수가 (int count, ...)과 같은 형태로 작성되었지만, cppreference.com에서는 다른 방식으로 작성되어 있다.
int printx(const char* fmt...);
그리고 첨언하기를, C언어와의 호환성을 위하여 다음과 같은 스타일을 허용한다고 한다.
f(int n, ...)
즉, gpt는 C언어와의 호환성을 위해 허용된 방식으로 가변 인자 함수를 선언한 것이다.
가변 인자를 지금까지 알아본 방식만으로 선언할 수 있는 것은 아니다.
다음으로 정리할 Parameter pack(Variadic template)을 사용해서 다양한 개수의 인수를 받는 함수를 작성할 수 있다고 말한다.
또한 Parameter pack을 사용하는 것이, 더 나은 선택인 경우가 많다고 한다.
그러니... 이런 것이 있다고만 알아 두면 좋을 것 같다.
Parameter pack은 C++11부터 지원하는 문법이다.
이는 0개 이상의 템플릿 인수를 허용하는 템플릿 매개변수를 정의하는 규칙이다.
기본적인 정의 방법은 다음과 같다. (cppreference.com의 Parameter pack 페이지의 두번째 예시 코드)
template<class... Types>
void f(Types... args);
f();
f(1);
f(2, 1.0);
위에서 보여주는 것 처럼, 컴파일러가 추론 가능하다면 몇 개의 인수든 사용하여 함수를 호출할 수 있다. 이때 각각의 인수는 독립적인 타입으로 여겨지며, 실제로 여러개의 템플릿 매개변수를 선언한 것과 같다.
Parameter pack 페이지의 Pack expansion 부분을 보면 이해할 수 있을 것이다. 하단의 코드를 보라.
template<class... Us>
void f(Us... pargs) {}
template<class... Ts>
void g(Ts... args)
{
f(&args...); // “&args...” is a pack expansion
// “&args” is its pattern <- 패턴이란 말은, 모든 args의 구성요소에 모두 '&'를 연산했다는 것.
}
g(1, 0.2, "a"); // Ts... args expand to int E1, double E2, const char* E3
// &args... expands to &E1, &E2, &E3
// Us... pargs expand to int* E1, double* E2, const char** E3
느낌이 오는가? '...'연산자를 붙인 타입을 팩(Pack)이라 칭하는 것이고, 이 팩을 확장한다는 말은, 사용자가 입력한 값들의 타입들로 구체화 된다는 말이다.
함수 g는 다양한 타입의 여러 인수를 받았고, 입력받은 인수 각각의 타입으로 템플릿 타입인 팩 Ts를 확장(구체화)할 수 있다.
팩 확장은 함수 f를 호출하는 지점에서 발생한다. g가 매개변수로 갖고 있는 팩에 '...'연산자를 붙이는 것으로, 팩의 모든 구성요소에 '&'(주소참조연산자)를 붙여서 f를 호출한 것이다.
(g함수 주석 1번:)하단의 g(1, 0.2, "a")의 경우로 보면, 'TS...'는 "int E1, double E2, const char* E3"의 세 템플릿 매개변수로 확장된 것이다.
(g함수 주석 2번:)또한 "&args..."는 "&E1, &E2, &E3"로 확장되었다는 것이다.
Parameter pack 페이지의 하단에는 예시 코드가 있다.
예시 코드는, C언어에서 흔히 보던 printf()와 유사한 함수를 만들어 본 것이다.
#include
void tprintf(const char* format) // base function
{
std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function
{
for (; *format != '\0'; format++)
{
if (*format == '%')
{
std::cout << value;
tprintf(format + 1, Fargs...); // recursive call
return;
}
std::cout << *format;
}
}
int main()
{
tprintf("% world% %\n", "Hello", '!', 123);
}
출력 결과 : Hello world! 123
여기서 눈여겨 보아야 할 부분은 재귀호출이 일어나는 부분이다.
tprintf()함수의 두번째 인자가 T타입의 value 변수인 것이 보이는가? 그런데 재귀호출을 할 때 마다 매개변수 팩인 Fargs를 T value 자리에 넣고 있다.
즉, 재귀호출을 수행할 때 마다, 매개변수 팩의 구성요소가 하나씩 줄어드는 것이다. 어떻게? 팩 확장을 통하여, 실제로는 모든 구성요소(인수들)가 별도로 존재하는 것 처럼 동작하기 때문이다.
이를 좀 더 보기 편하게 설명하면 다음과 같다.
이제 Parameter pack, Pack expansion 대하여 어느 정도 이해했다고 할 수 있을 것이다.
한글로는 후행 반환 타입을 뜻한다. 말 그대로, 반환 타입을 함수의 이름 뒤에 작성하는 문법인데, 예시는 다음과 같다.
template<typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a+b) {
return a+b;
}
우선, decltype()는 타입 추론 키워드이다(declared type -> decltype). 함수처럼 동작하며, 입력받은 인자들의 연산 결과값이 가져야 할 타입을 추론하여 반환한다.
그렇다면 왜 decltype()를 일반적인 함수의 타입 자리에 작성할 수 없을까? 하단의 코드를 보면 이해가 될 것이다.
template<typename T1, typename T2>
decltype(a+b) add(T1 a, T2 b){
return a+b;
}
알겠는가? a와 b를 컴파일러가 아직 알지 못하는 상태에서 타입 추론을 지시하기 때문이다.
그러므로, 우리가 함수의 프로토타입을 선언했던 것 처럼, 함수의 반환 타입을 정의하는 코드를 조금 뒤로 옮길 필요가 있는 것이다.
추가로 복잡한 반환 타입을 보기 좋게 서술할 수 있다는 장점도 있다. (코드 가독성 향상.)