heron

서론

다음 교재의 1장에 등장하는 생소한 문법 일부를 정리한다.

존 캐리 외 2인, 코딩테스트를 위한 자료구조와 알고리즘 with C++

다음의 세 개념을 설명한다.

  1. Variable argument

    '...'이 사용되는 함수에 대한 이해를 위함.

  2. Parameter pack

    가변 템플릿에 사용되는 '...'의 이해를 위함.

  3. Trailing return type(후행 리턴 타입)

    함수 정의 부분에서 사용된 '->'연산자의 이해.

    함수의 반환 타입을 명시적으로 적는 문법의 이해.

본론

1.   Variable argument

특정한 매개변수를 가변 개수로 선언할 수 있도록 하는 기능이다.

가변 길이 매개변수는 '...'을 통하여 선언하며, 이렇게 생성된 매개변수를 제어하기 위해서는 <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()를 사용하여 함수가 받은 모든 인수에 접근하고 있다.

다만 추가로 알아야 할 것이 있다.

추가로 알아야 할 것 1: C언어 호환성

gpt가 작성한 코드에서는 함수의 매개변수가 (int count, ...)과 같은 형태로 작성되었지만, cppreference.com에서는 다른 방식으로 작성되어 있다.

int printx(const char* fmt...);

그리고 첨언하기를, C언어와의 호환성을 위하여 다음과 같은 스타일을 허용한다고 한다.

f(int n, ...)

즉, gpt는 C언어와의 호환성을 위해 허용된 방식으로 가변 인자 함수를 선언한 것이다.

추가로 알아야 할 것 2: Parameter pack을 통한 대체

가변 인자를 지금까지 알아본 방식만으로 선언할 수 있는 것은 아니다.

다음으로 정리할 Parameter pack(Variadic template)을 사용해서 다양한 개수의 인수를 받는 함수를 작성할 수 있다고 말한다.

또한 Parameter pack을 사용하는 것이, 더 나은 선택인 경우가 많다고 한다.

그러니... 이런 것이 있다고만 알아 두면 좋을 것 같다.


2.   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);

위에서 보여주는 것 처럼, 컴파일러가 추론 가능하다면 몇 개의 인수든 사용하여 함수를 호출할 수 있다. 이때 각각의 인수는 독립적인 타입으로 여겨지며, 실제로 여러개의 템플릿 매개변수를 선언한 것과 같다.

Pack expansion

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"로 확장되었다는 것이다.

Example

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 자리에 넣고 있다.

즉, 재귀호출을 수행할 때 마다, 매개변수 팩의 구성요소가 하나씩 줄어드는 것이다. 어떻게? 팩 확장을 통하여, 실제로는 모든 구성요소(인수들)가 별도로 존재하는 것 처럼 동작하기 때문이다.

이를 좀 더 보기 편하게 설명하면 다음과 같다.

Example - tprintf()의 재귀호출 과정
  1. tprintf("% world% %\n", "Hello", '!', 123)
  2. '%'를 만나서, T value인 "Hello"를 출력한다.
  3. 재귀호출: tprintf(format + 1, '!', 123)
  4. 다음 '%'를 만나기 전까지 format의 문자를 하나씩 출력한다. (즉, "world"를 출력한다.)
  5. '%'를 만나서 현재의 T value인 '!'를 출력한다.
  6. 재귀호출: tprintf(format +1, 123)
  7. '%'를 만나서 현재의 T value인 123을 출력한다.
  8. 재귀호출: tprintf(format + 1)
  9. format의 마지막 값인 '\n'을 출력하고 함수 종료.

이제 Parameter pack, Pack expansion 대하여 어느 정도 이해했다고 할 수 있을 것이다.


3.   Trailing return type

한글로는 후행 반환 타입을 뜻한다. 말 그대로, 반환 타입을 함수의 이름 뒤에 작성하는 문법인데, 예시는 다음과 같다.

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를 컴파일러가 아직 알지 못하는 상태에서 타입 추론을 지시하기 때문이다.

그러므로, 우리가 함수의 프로토타입을 선언했던 것 처럼, 함수의 반환 타입을 정의하는 코드를 조금 뒤로 옮길 필요가 있는 것이다.

추가로 복잡한 반환 타입을 보기 좋게 서술할 수 있다는 장점도 있다. (코드 가독성 향상.)