C++ 코드를 작성하다 보면 같은 타입을 여러 곳에서 반복해서 사용하거나, 함수 포인터처럼 선언이 복잡한 타입을 다뤄야 할 때가 있습니다.
예를 들어 다음과 같은 함수 포인터 선언은 한눈에 의미를 파악하기 쉽지 않습니다.
int (*function)(char, double);
C++에서는 이러한 타입에 새로운 이름을 붙여 코드를 간결하게 표현할 수 있습니다. 이를 타입 별칭(Type Alias)이라고 합니다.
이번 글에서는 타입 별칭을 만드는 typedef와 using의 문법을 살펴보고, 함수 포인터와 별칭 템플릿에서 어떻게 활용하는지 알아보겠습니다.
목차
- 타입 별칭이란?
- typedef로 타입 별칭 만들기
- using으로 타입 별칭 만들기
- typedef와 using 비교하기
- 함수 포인터 타입에 별칭 붙이기
- using과 별칭 템플릿
- 타입 별칭은 새로운 타입일까?
- 포인터 별칭과 const 주의하기
- typedef와 using 중 무엇을 사용해야 할까?
- 정리
타입 별칭이란?
타입 별칭(Type Alias)은 기존 타입에 다른 이름을 붙이는 기능입니다.
다음 코드에서는 int* 타입에 IntPtr이라는 이름을 붙였습니다.
using IntPtr = int*;
이제 IntPtr을 타입 이름처럼 사용할 수 있습니다.
IntPtr numberPointer = nullptr;
위 선언은 다음 코드와 같은 의미입니다.
int* numberPointer = nullptr;
타입 별칭을 사용하면 길거나 복잡한 타입을 짧은 이름으로 표현할 수 있습니다. 또한 타입의 구체적인 형태보다 코드에서 담당하는 역할을 이름으로 나타낼 수도 있습니다.
using PlayerId = unsigned int; PlayerId playerId = 10;
playerId를 단순히 unsigned int로 선언하는 것보다, 해당 값이 플레이어를 식별하는 번호라는 의미가 조금 더 명확하게 드러납니다.
typedef로 타입 별칭 만들기
C++11 이전에는 일반적으로 typedef를 사용해 타입 별칭을 선언했습니다.
typedef int* IntPtr;
typedef 다음에 실제 타입을 작성하고, 마지막에 새롭게 사용할 별칭을 작성합니다.
typedef 실제 타입 별칭;
다음과 같이 여러 종류의 타입에 별칭을 붙일 수 있습니다.
typedef unsigned int PlayerId; typedef float RealNumber; typedef const char* Message; PlayerId playerId = 10; RealNumber speed = 5.0f; Message text = "Hello";
typedef라는 이름에는 타입을 정의한다는 표현이 포함되어 있지만, 실제로 새로운 타입을 만드는 것은 아닙니다. 기존 타입을 가리키는 또 다른 이름을 선언합니다.
using으로 타입 별칭 만들기
C++11부터는 using을 사용해서도 타입 별칭을 만들 수 있습니다.
using IntPtr = int*;
using 다음에 새롭게 사용할 별칭을 먼저 작성하고, = 오른쪽에 실제 타입을 작성합니다.
using 별칭 = 실제 타입;
typedef로 작성했던 앞의 예제를 using으로 바꾸면 다음과 같습니다.
using PlayerId = unsigned int; using RealNumber = float; using Message = const char*; PlayerId playerId = 10; RealNumber speed = 5.0f; Message text = "Hello";
별칭이 항상 =의 왼쪽에 있고 실제 타입이 오른쪽에 있으므로, 복잡한 타입에서도 선언의 의미를 비교적 쉽게 파악할 수 있습니다.
typedef와 using 비교하기
typedef와 using은 모두 기존 타입에 별칭을 붙일 수 있습니다.
typedef int* IntPtr; using IntPtr = int*;
두 코드는 모두 IntPtr을 int* 타입의 별칭으로 선언합니다.
다만 별칭을 작성하는 위치가 다릅니다.
| 구분 | typedef | using |
|---|---|---|
| 기본 문법 | typedef 실제 타입 별칭; |
using 별칭 = 실제 타입; |
| 별칭의 위치 | 선언의 마지막 또는 중간 | =의 왼쪽 |
| 도입 시기 | C++ 초기부터 지원 | C++11부터 지원 |
| 별칭 템플릿 | 직접 지원하지 않음 | 지원함 |
| 가독성 | 복잡한 타입에서는 해석하기 어려울 수 있음 | 별칭과 실제 타입이 명확하게 구분됨 |
단순한 타입에서는 두 문법의 차이가 크지 않습니다.
typedef int Number; using Number = int;
하지만 함수 포인터처럼 선언이 복잡해지면 using의 구조가 더 쉽게 읽힐 수 있습니다.
함수 포인터 타입에 별칭 붙이기
함수 포인터는 함수의 주소를 저장하는 포인터입니다.
다음 선언은 char와 double을 매개변수로 받고 int를 반환하는 함수의 주소를 저장할 수 있는 함수 포인터를 만듭니다.
int (*function)(char, double);
함수 포인터 선언에서는 변수 이름인 function이 반환형과 매개변수 목록 사이에 위치합니다. 이러한 선언 형태에 익숙하지 않다면 코드를 읽기 어려울 수 있습니다.
typedef를 사용하면 다음과 같이 함수 포인터 타입에 별칭을 붙일 수 있습니다.
typedef int (*FunctionType)(char, double);
여기에서도 별칭인 FunctionType이 선언의 중간에 들어갑니다.
같은 타입 별칭을 using으로 선언하면 다음과 같습니다.
using FunctionType = int (*)(char, double);
=의 왼쪽에는 별칭이, 오른쪽에는 실제 함수 포인터 타입이 배치됩니다.
using FunctionType = int (*)(char, double);
별칭 반환형 매개변수 목록
이제 FunctionType을 사용해 함수 포인터 변수를 선언할 수 있습니다.
#include <iostream>
using FunctionType = int (*)(char, double);
int Calculate(char character, double value)
{
return static_cast<int>(character + value);
}
int main()
{
FunctionType function = Calculate;
int result = function('A', 10.0);
std::cout << result << '\n';
}
Calculate 함수는 FunctionType에서 요구하는 반환형과 매개변수 형식에 대응합니다. 따라서 함수의 주소를 function에 저장할 수 있습니다.
함수 포인터에 저장된 함수를 호출할 때도 일반 함수와 같은 호출 구문을 사용할 수 있습니다.
int result = function('A', 10.0);
타입 별칭을 사용하지 않았다면 같은 변수는 다음과 같이 선언해야 합니다.
int (*function)(char, double) = Calculate;
타입 별칭을 사용하면 복잡한 함수 포인터 선언을 한 번만 작성하고, 이후에는 일반적인 타입 이름처럼 사용할 수 있습니다.
using과 별칭 템플릿
using은 템플릿과 함께 별칭 템플릿(Alias Template)을 만들 수 있다는 장점이 있습니다.
다음 코드는 std::vector에 List라는 별칭을 붙이는 예제입니다.
#include <vector> template<typename T> using List = std::vector<T>;
List를 사용할 때 저장할 요소의 타입을 템플릿 인자로 전달합니다.
List<int> numbers; List<float> values;
위 코드는 각각 다음 타입을 사용한 것과 같습니다.
std::vector<int> numbers; std::vector<float> values;
별칭 템플릿은 타입의 일부만 바꾸면서 나머지 구성을 반복해서 사용해야 할 때 유용합니다.
예를 들어 문자열을 키로 사용하는 std::unordered_map에 다음과 같은 별칭을 만들 수 있습니다.
#include <string> #include <unordered_map> template<typename T> using StringMap = std::unordered_map<std::string, T>; StringMap<int> scores; StringMap<float> attributes;
StringMap<int>에서는 값의 타입이 int가 되고, StringMap<float>에서는 값의 타입이 float가 됩니다. 키 타입인 std::string은 그대로 유지됩니다.
이와 같은 별칭 템플릿은 typedef만으로는 직접 선언할 수 없습니다.
별칭 템플릿은 템플릿 자체를 새로운 타입으로 만드는 기능이 아닙니다. 전달받은 템플릿 인자를 이용해 기존 타입의 별칭을 만드는 문법입니다.
타입 별칭은 새로운 타입일까?
타입 별칭은 새로운 타입을 만드는 기능이 아닙니다. 기존 타입에 다른 이름을 추가할 뿐입니다.
using PlayerId = unsigned int; PlayerId playerId = 10; unsigned int number = playerId;
PlayerId는 여전히 unsigned int와 같은 타입이므로 두 변수 사이에 값을 그대로 대입할 수 있습니다.
이러한 특징 때문에 같은 원본 타입에 서로 다른 별칭을 붙이더라도 컴파일러는 별개의 타입으로 구분하지 않습니다.
using PlayerId = unsigned int; using ItemId = unsigned int; PlayerId playerId = 10; ItemId itemId = playerId;
PlayerId와 ItemId는 이름과 의미는 다르지만 모두 unsigned int의 별칭입니다. 따라서 위 대입은 허용됩니다.
타입 별칭은 코드의 의미와 가독성을 개선할 수 있지만, 서로 다른 별칭 사이의 잘못된 대입을 컴파일러가 차단해 주지는 않습니다. 컴파일러가 구분하는 새로운 타입이 필요하다면 별도의 클래스나 구조체 등을 정의해야 합니다.
포인터 별칭과 const 주의하기
포인터 타입에 별칭을 붙이면 const가 적용되는 대상을 혼동하기 쉽습니다.
먼저 int* 타입에 IntPtr이라는 별칭을 붙이겠습니다.
using IntPtr = int*;
이 별칭 앞에 const를 붙여 변수를 선언합니다.
const IntPtr pointer = nullptr;
코드만 보면 const int*와 같을 것처럼 느껴질 수 있습니다. 하지만 IntPtr은 int* 전체를 나타내는 하나의 타입 이름입니다.
따라서 위 선언은 개념적으로 다음 코드와 같습니다.
int* const pointer = nullptr;
const가 포인터 타입 전체에 적용되므로 pointer는 다른 주소를 가리킬 수 없는 상수 포인터가 됩니다.
int number = 10; int other = 20; using IntPtr = int*; IntPtr pointer = &number; pointer = &other; // 가능 const IntPtr constPointer = &number; // constPointer = &other; // 오류
다만 포인터가 가리키는 정수는 const가 아니므로 값을 변경할 수 있습니다.
*constPointer = 30;
정수 값을 수정할 수 없는 포인터를 표현하려면 가리키는 대상의 타입에 const를 포함한 별칭을 별도로 선언할 수 있습니다.
using ConstIntPtr = const int*; int number = 10; ConstIntPtr pointer = &number; // *pointer = 20; // 오류
| 선언 | 실제 타입 | 의미 |
|---|---|---|
IntPtr pointer |
int* pointer |
일반 정수 포인터 |
const IntPtr pointer |
int* const pointer |
다른 주소를 가리킬 수 없는 포인터 |
ConstIntPtr pointer |
const int* pointer |
가리키는 정수를 수정할 수 없는 포인터 |
포인터 별칭을 사용할 때는 별칭이 포인터 기호까지 포함하고 있는지 확인하고, const가 포인터와 대상 중 어디에 적용되는지 구분해야 합니다.
typedef와 using 중 무엇을 사용해야 할까?
기존 코드나 외부 라이브러리에서는 typedef를 자주 볼 수 있습니다. C 언어와 호환되는 코드에서도 typedef가 널리 사용됩니다.
typedef unsigned int PlayerId;
따라서 기존 코드를 읽기 위해서는 typedef 문법도 알아두어야 합니다.
하지만 C++11 이상을 사용하는 새로운 C++ 코드에서는 일반적으로 using을 사용하는 것이 좋습니다.
using PlayerId = unsigned int;
using은 별칭과 실제 타입이 =을 기준으로 명확하게 구분되고, 함수 포인터처럼 복잡한 타입에서도 비교적 읽기 쉽습니다. 또한 typedef에서 직접 지원하지 않는 별칭 템플릿도 선언할 수 있습니다.
- 기존 코드의
typedef는 문법과 의미를 파악할 수 있어야 합니다. - 새로운 C++ 코드에서는 특별한 이유가 없다면
using을 우선 사용할 수 있습니다. - 한 프로젝트 안에서는 정해진 코딩 규칙에 따라 문법을 일관되게 사용하는 것이 좋습니다.
정리
- 타입 별칭은 기존 타입에 새로운 이름을 붙이는 기능입니다.
typedef와using을 사용해 타입 별칭을 선언할 수 있습니다.typedef는 실제 타입과 별칭이 하나의 선언 안에 배치됩니다.using은 별칭이=의 왼쪽에, 실제 타입이 오른쪽에 배치됩니다.- 함수 포인터처럼 선언이 복잡한 타입에서는
using의 의미를 더 쉽게 파악할 수 있습니다. using은 템플릿과 함께 별칭 템플릿을 만들 수 있습니다.typedef만으로는 별칭 템플릿을 직접 선언할 수 없습니다.- 타입 별칭은 새로운 타입을 만들지 않습니다.
- 같은 원본 타입에 붙인 서로 다른 별칭은 컴파일러가 별개의 타입으로 구분하지 않습니다.
- 포인터 별칭에
const를 사용하면 포인터가 가리키는 대상이 아니라 포인터 자체에 적용될 수 있습니다. - 기존 코드에서는
typedef를 만날 수 있으므로 두 문법을 모두 알아두어야 합니다. - C++11 이상으로 작성하는 새로운 코드에서는 일반적으로
using을 우선 사용할 수 있습니다.