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]이 생소하실 수 있는데, 하나하나 따져보자면
- arr이라는 int형 변수를 만듭니다.
- 그런데 int (&arr)이니까 int의 참조형식입니다.
- int (&arr)[N]이니까 arr은 int형 배열의 참조입니다. 배열 요소의 크기는 N입니다.
- 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과 결합하면 이 또한 엄청난 시너지를 내는 것을 알 수 있습니다.
혹시 궁금한 점이 있으면 댓글로 질문해 주세요. 다음엔 더 편리하고 혁신적인 기능을 소개해 드리겠습니다.
'C++' 카테고리의 다른 글
나만의 C++ 프로그래밍 가이드 (0) | 2022.10.01 |
---|---|
[C++17] 템플릿 인자를 생략한다? Deduction Guide (0) | 2020.03.21 |
[C++20] printf() 쓰지 마세요! std::format() (2) | 2020.03.21 |
C++17 주요 기능 정리 (1) - 유용한 기능 편 (0) | 2020.03.21 |
[C++11] std::array vs. C-array (0) | 2020.03.21 |