C++

[C++11] 템플릿으로 배열을 함수에 넘기기

카루-R 2020. 3. 21. 10:01
반응형

C언어에서, 배열을 함수로 전달하려면 어떻게 해야 했죠?

네. 맞습니다. 바로 포인터를 이용하는 것인데요,

void func(int arr[], int size);
void func(int *arr, int size);

위와 같이 arr은 포인터 (첫번째줄도 사실 포인터입니다) 로 받고, 배열의 크기는 알 수 없으므로 따로 정수형의 size매개변수를 따로 받았죠.

하지만 이 방식에는 단점이 있습니다. 일단 배열의 사이즈를 모르기 때문에 직접 size로 넘겨야하는 게 가장 큰 문제인데요,

C++에서는 템플릿을 통해 문제를 해결합니다.

template<std::size_t N>
void func(int (&arr)[N]);

다른 타입을 받고 싶다면 int부분을 수정해주면 됩니다.

int (&arr)[N]이 생소하실 수 있는데, 하나하나 따져보자면

  1. arr이라는 int형 변수를 만듭니다.
  2. 그런데 int (&arr)이니까 int의 참조형식입니다.
  3. int (&arr)[N]이니까 arr은 int형 배열의 참조입니다. 배열 요소의 크기는 N입니다.
  4. template에서 배열 요소의 크기가 N이므로 컴파일러에서 배열 요소의 크기를 구해 직접 넘겨줍니다.

 

자 무슨 말인지 볼까요?

#include <iostream>

template<typename T, std::size_t N>
void func(T (&arr)[N]) {
    std::cout << N << std::endl;
}

int main() {
  int a[] = {1, 2, 3, 4, 5};
  func(a);
}

 여기서 func는 아무 배열이나 일단 전달을 받으면 그 배열의 요소를 출력해주는 함수입니다.

func(a)를 호출하면

T (&arr)[N]에 a가 전달이 되는데, 이때 템플릿에 의해

T가 int로, N이 5로 정해집니다.

이제부터 func함수에서 arr매개변수를 어떻게 쓸 수 있는지 보겠습니다.

 

static_assert, 배열 크기 체크

템플릿 인자들이 컴파일 타임에 모두 결정되는 건 아시죠? 따라서 N도 컴파일 타임에 결정됩니다.

따라서, static_assert를 통해 컴파일 타임에 배열 크기를 확인할 수 있어요.

template<typename T, std::size_t N>
void func(T (&arr)[N]) {
    static_assert(N > 1, "배열의 크기는 1보다 커야 합니다.");
}
In instantiation of 'void func(T (&)[N]) [with T = int; long unsigned int N = 1]':
error: static assertion failed: 배열의 크기는 1보다 커야 합니다.
      |     static_assert(N > 1, "배열의 크기는 1보다 커야 합니다.");
      |                   ~~^~~

이렇게 컴파일 오류가 뜨고, 그 원인까지 명확히 알 수 있습니다! 런타임 도중에 if문에 assert()에

에휴... 그렇게 삽질하느니 아예 이렇게 명시적으로 제한시킬 수 있으니 굉장히 편리합니다.

 

범위기반 for문

배열에 대한 참조이므로 range-base for loop, 범위기반 for문도 당연히 사용할 수 있습니다.

아래의 func()는 배열의 모든 내용을 출력해주는 함수입니다.

template<class T, std::size_t N>
void func(T (&arr)[N]) {
    for (auto&& a : arr) 
        std::cout << a << std::endl;
}

아래는 배열을 초기화시키는 함수입니다.

template<class T, std::size_t N>
void func(T (&arr)[N]) {
    for (auto& a : arr) 
        a = T{}; // 기본값을 가진 임시객체 대입
}

 

C 스타일 문자열

static_assert 다음으로 굉장히 유용합니다. 이건 char [] 배열에도 적용되기 때문에,

C스타일 배열을 사용할 때 굉장한 이점을 불러옵니다.

strcpy_s, strncpy 등등 귀찮은 것 필요 없이 배열 크기를 알아서 정해주니까요.

#include <iostream>
#include <cstring> // std::strncpy()

template<std::size_t N>
void func(char (&dest)[N], const char* src) {
    std::strncpy(dest, src, N - 1);
    dest[N - 1] = '\0';
}

int main()
{
  char arr[20];
  const char* str = "This is a sample string.";
  func(arr, str);
  std::cout << arr << std::endl;
}
This is a sample st

 arr의 크기를 자동으로 파악하여 복사를 중단했습니다. 위 코드는 strncpy함수를 랩핑(wrapping)한 것입니다. 이로써 배열 크기 입력을 생략할 수 있습니다.

단, strncpy를 사용할 땐 char 배열 마지막에 꼭 널 문자(\0)를 삽입해야 합니다.

   7 |    dest[N - 1] = '\0';

이 부분을 주석 처리하면

This is a sample stN�

이렇게 뒷부분의 쓰레기값까지 출력되는 걸 볼 수 있습니다.

그렇다면 gets()를 랩핑할, 문자열을 안전하게 입력받는 함수를 만들어볼까요?

#include <iostream>

namespace secure {
    template<std::size_t N>
    inline void gets(char (&dest)[N]) {
        std::fgets(dest, N, stdin);
    }
}

int main()
{
    char buf[20];
    secure::gets(buf);
    std::cout << buf << std::endl;
}

std::fgets()는 에 포함이 되어있는데, 이건 또 에 include되어 있습니다.

gets()라는 동일한 이름을 사용하기 위해 secure이란 namespace를 만들었습니다.

(INPUT >>>) My String.
            My String.

20글자가 넘지 않으니 입력한 대로 그대로 나왔습니다.

(INPUT >>>) Goyang Global High School, also known as GGHS, is...
            Goyang Global High

20글자가 넘었습니다. 공백과 널 문자를 고려하여 입력이 끊겼습니다.

 

C++20 Concept과 결합하면 강력해진다

#include <iostream>
#include <concepts>

template <std::integral T, std::size_t N>
void func(const T (&arr)[N]) {
    for (auto& a : arr) 
        if (a % 2 == 1) 
            std::cout << a << std::endl;
}

int main()
{
    int arr[20](1, 3, 2, 5, 55, 2, 1, 4, 6, 8, 2, 5, 3);
    func(arr);
}

 참고로 C++20에서는 배열을 소괄호로 초기화하는 등의 Direct Initialization을 지원하나, 이 부분은 축소 변환등 할 말이 많으므로 따로 포스팅을 하겠습니다.

아무튼, 위의 코드는 integral, 즉 정수만 배열로 받고 홀수만 출력하는 예제입니다. 현재 GCC 최신 버전에서 작동합니다.

이렇게 concept과 결합하면 이 또한 엄청난 시너지를 내는 것을 알 수 있습니다.

 


혹시 궁금한 점이 있으면 댓글로 질문해 주세요. 다음엔 더 편리하고 혁신적인 기능을 소개해 드리겠습니다.

반응형