Develog
static_assert와 non-type template 본문

1. non-type template
C++에서 제네릭 함수를 구현하기 위해 사용되는 template은 아래와 같이 호출 시 타입을 지정하여 호출하는 것이 기본적인 사용법입니다.
template<typename T>
T sum(T n1, T n2) {
return (n1 + n2);
}
int main() {
int x = sum<int>(1, 2);
printf("%d\n", x);
return 0;
}
제네릭은 사용자가 지정한 타입에 맞게 유동적으로 연산을 수행할 수 있으며 코드의 재활용성을 높이고, 다양한 상황에 맞는 유연한 대응이 가능해 활용도가 높은 고급 기능 중 하나입니다.
non-type template은 위 코드와 같이 제네릭 함수를 사용할 때 < > 기호를 사용해 지정할 타입을 명시해 주는 것 대신, 암시적으로 인자를 통해 어떠한 타입이 사용되는지 유추할 수 있도록 해줍니다.
단, non-type template의 인자로 활용할 수 있는 타입은 미리 지정되어 있습니다.
(int/size_t, enum, pointer/reference, nullptr, bool, (c++ 20) float 등)
이 non-type template은 아래와 같이 배열을 인덱스를 사용해 다루고자 할 때 유용합니다.
template <size_t Index, typename T, size_t Length>
T& get(T (&arr)[Length]) {
if (Index >= Length) {
throw std::out_of_range{ "Out-of-bounds" };
}
return arr[Index];
}
int main() {
int fib[] {1, 1, 2, 0};
printf("%d %d %d\n", get<0>(fib), get<1>(fib), get<2>(fib));
return 0;
}
위 코드에서 get 제네릭 함수를 호출할 때, < > 사이에 int라는 타입의 명칭이 아닌 인덱스 값 그 자체를 정수형으로 제공하였습니다.
fib 배열에 대한 참조를 제공함과 동시에 인덱스의 값을 제공하였고, 배열의 길이는 별도로 명시하지 않았습니다.
C++ 컴파일러는 해당 코드를 해석할 때, 인자로 제공된 fib와 인덱스의 정수형 값을 참고하여 int fib[0]을 사용하겠다는 의도를 유추하게 됩니다.
위의 get( ) 메서드의 변화 과정을 단계별로 살펴보면 다음과 같습니다.
1. 제네릭 함수를 사용하지 않고 구현한다면,
int& get(int (&arr)[10], size_t index) {
if (index >= 10) {
throw std::out_of_range{ "Out of bounds" };
}
return arr[index];
}
배열의 길이는 항상 10으로 고정되며, 타입 또한 int 타입만 받을 수 있는 get( ) 메서드입니다.
int 타입이 아닌 size_t 타입이 인자로 제공된다면 그에 대한 대응을 따로 준비해야 하는 불편함이 존재합니다.
2. 템플릿을 적용한다면, (배열의 길이는 non-type)
template <typename T, size_t length>
T& get(T (&arr)[length], size_t index) {
if (index >= length) {
throw std::out_of_range{ "Out of bounds" };
}
return arr[index];
}
제네릭 매서드로 구현하였으며, 배열의 길이는 non-type으로써 추론할 수 있도록 수정한 get( ) 메서드입니다.
이제 int 타입 외에 다른 타입도 인자로 받을 수 있으며, length를 이용하여 다양한 크기의 배열을 다룰 수 있도록 유연성을 높였습니다.
하지만 인덱스 값은 non-type이 아니기 때문에 아래와 같이 메서드를 사용해야 하므로 아직까지는 불완전합니다.
get<int>(fib, 1);
3. 온전히 non-type 템플릿으로 구현한다면,
template <size_t index, typename T, size_t length>
T& get(T (&arr)[length]) {
if (index >= length) {
throw std::out_of_range{ "Out of bounds" };
}
return arr[index];
}
get( ) 메서드를 배열의 길이, 타입까지 전부 추론할 수 있도록 수정했습니다.
이제 타입을 지정해 줄 필요가 없으며, 아래와 같이 인덱스를 이용하여 배열의 값을 조회할 수 있습니다.
get<1>(fib)
위의 get( ) 메서드 선언 부분을 살펴보면 소괄호 안에 별도로 작성되어 있던 size_t index 부분이 template < > 안으로 옮겨진 것을 알 수 있습니다.
2. static_assert
2.1. concpets
C++ 20 이상의 버전에는 concepts라는 이름의 기능을 제공하고 있습니다.
이는 템플릿을 이용하여 제네릭 메서드를 구현해 사용하는 과정에서 타입 추론 에러가 발생했을 때를 대비하여 사용자가 보다 편하고 빠르게 에러를 추적하고, 문제가 되는 부분을 수정할 수 있도록 사용하는 기능 중 하나입니다.
concepts는 타입 오류로 인하여 에러가 발생할 수 있는 코드를 컴파일 단계에서 알려줄 수 있습니다.
어떠한 타입의 인자가 전달될 것인지를 미리 제한을 걸어 런타임 단계가 아니더라도 에러 발생 여부를 추론할 수 있도록 설계하는 방식입니다.
이와 더불어 에러 메시지의 가독성을 높여주는 기능도 함께 수행하기 때문에 C++ 제네릭 메서드를 다룰 때 매우 중요한 자리를 차지한다고 합니다.
2.2. C++ 20 이상이 아니라면?
앞서 언급한 concepts는 상당히 솔깃한 기능이겠지만 안타깝게도 C++ 20 이상에서만 사용할 수 있는 비교적 최신 기능입니다.
2025년 7월 기준, C++ 17처럼 이보다 낮은 버전이 아직 보편적으로 활용되고 있는 만큼, concepts처럼 최신 기능들은 개발 환경에 따라 사용이 불가능할 수 있습니다.
이러한 문제점의 대안으로써 stdexcept 라이브러리를 통해 제공되는 static_assert( )를 활용해볼 수 있습니다.
2.3. get( ) 메서드 수정하기
앞서 다루었던 get( ) 메서드를 다시 살펴보면, if( ) 문을 사용하여 배열의 길이를 넘어선 접근에 대한 에러 처리를 구현했습니다.
template <size_t index, typename T, size_t length>
T& get(T (&arr)[length]) {
if (index >= length) {
throw std::out_of_range{ "Out of bounds" };
}
return arr[index];
}
기능적으로 봤을 때 이 또한 올바르게 동작은 하겠지만, static_assert( )를 사용하면 더 간단히 코드를 작성할 수 있습니다.
template <size_t Index, typename T, size_t Length>
T& get(T (&arr)[Length]) {
static_assert(Index < Length, "Out-of-bounds access"); // (False -> error msg)
return arr[Index];
}
주석을 참고하면 알 수 있듯이, 1번째 인자는 bool 타입으로 조건을 판별할 수 있는 식이 와야 합니다.
만약 그 식의 연산 결과가 false라면 2번째 인자로 제공된 메시지를 활용해 에러를 발생시키는 방식입니다.
static_assert( )은 런타임이 아닌 컴파일 타임에서 에러를 잡아낼 수 있습니다.
이 덕분에 아래와 같이 get( ) 메서드를 호출해 사용하게 된다면 IDE 상에서 즉시 에러를 볼 수 있을 것입니다.
int main() {
printf("Hello, world!\n");
printf("\n");
int fib[] {1, 1, 2, 0};
printf("%d %d %d ", get<0>(fib), get<1>(fib), get<2>(fib));
printf("%d", get<4>(fib)); // ERROR!!
}
수고하셨습니다!
'Technology > C++' 카테고리의 다른 글
| const와 constexpr (0) | 2025.08.06 |
|---|---|
| C++의 포인터 (0) | 2025.08.03 |
| strcpy() 와 strncpy() (0) | 2025.07.14 |
| C++의 함수 프로토타입 (0) | 2024.06.26 |
| 포인터와 배열, 포인터 산술 (0) | 2024.05.18 |