C++ typedef vs using | '타입 별칭' 가이드
이 글의 핵심
C++ typedef vs using - "타입 별칭" 가이드. C++ typedef vs using의 typedef vs using, 기본 사용, 실전 예시를 실전 코드와 함께 설명합니다.
typedef vs using
typedef 와 using (C++11)은 타입에 별칭을 붙이는 기능입니다. using 상세, 템플릿 기초와 함께 보면 템플릿 별칭까지 활용하기 쉽습니다.
// typedef (전통적)
typedef unsigned long ulong;
// using (C++11, 권장)
using ulong = unsigned long;
왜 필요한가?:
- 가독성: 긴 타입 이름을 짧게
- 유지보수: 타입 변경 시 한 곳만 수정
- 명확성: 의도를 명확히 표현
- 템플릿 별칭:
using은 템플릿 별칭 가능
std::map과 std::vector, std::pair가 여러 겹으로 중첩되면 타입 이름만으로 한 줄을 다 채우게 되어, 코드를 읽는 사람이 정작 중요한 로직보다 타입 선언을 해독하는 데 더 많은 시간을 쓰게 됩니다. 아래처럼 별칭 하나로 이런 복잡한 타입에 의미 있는 이름을 붙여두면, 이후 코드에서는 UserData라는 이름만으로 그 타입이 무엇을 표현하는지 즉시 알 수 있습니다.
// ❌ 긴 타입: 읽기 어려움
std::map<std::string, std::vector<std::pair<int, std::string>>> data;
// ✅ 별칭: 읽기 쉬움
using UserData = std::map<std::string, std::vector<std::pair<int, std::string>>>;
UserData data;
typedef vs using 비교:
| 특징 | typedef | using |
|---|---|---|
| 문법 | C 스타일 | 현대적 |
| 가독성 | ❌ 낮음 | ✅ 높음 |
| 템플릿 별칭 | ❌ 불가 | ✅ 가능 |
| 함수 포인터 | ❌ 복잡 | ✅ 명확 |
| 권장 | ❌ 레거시 | ✅ C++11+ |
typedef와 using의 차이가 가장 극명하게 드러나는 지점은 함수 포인터 타입을 선언할 때입니다. typedef는 함수 포인터 문법을 그대로 흉내 내야 하기 때문에 별칭 이름(FuncPtr)이 선언 중간에 파묻혀 버리지만, using은 이름 = 타입 형태의 대입문처럼 읽히므로 어떤 이름에 어떤 타입을 붙이는지가 한눈에 들어옵니다.
// typedef: 복잡
typedef void (*FuncPtr)(int, double);
// using: 명확
using FuncPtr = void (*)(int, double);
기본 사용
typedef와 using은 컨테이너 타입 단축, 함수 포인터, 심지어 배열 타입에 이르기까지 거의 모든 타입 선언 자리에 똑같이 적용할 수 있습니다. 아래 세 쌍의 예시를 나란히 비교해보면, 각 문법이 동일한 결과를 만들어내면서도 using 쪽이 일관되게 “별칭 = 원래 타입” 형태를 유지해 더 예측 가능하다는 것을 알 수 있습니다.
// 긴 타입 단축
typedef std::vector<std::pair<int, std::string>> PairVector;
using PairVector = std::vector<std::pair<int, std::string>>;
// 함수 포인터
typedef void (*FuncPtr)(int);
using FuncPtr = void (*)(int);
// 배열
typedef int IntArray[10];
using IntArray = int[10];
실전 예시
이제 컨테이너, 템플릿, 함수 포인터, 클래스 내부 중첩 타입이라는 네 가지 대표적인 상황에서 typedef와 using이 실제로 어떻게 쓰이는지, 그리고 왜 using이 더 널리 권장되는지를 하나씩 살펴보겠습니다.
예시 1: 컨테이너 별칭
std::map<std::string, std::vector<int>>처럼 중첩된 컨테이너 타입을 여러 곳에서 반복해서 써야 한다면, 별칭 하나로 묶어두는 것이 타이핑을 줄이는 것 이상의 의미를 갖습니다. 나중에 이 타입을 다른 컨테이너로 바꿔야 할 때 별칭 선언 한 줄만 수정하면 되므로, 코드베이스 전체에서 같은 타입을 검색해 일일이 바꾸는 수고를 덜 수 있습니다.
// typedef
typedef std::map<std::string, std::vector<int>> DataMap;
// using (더 읽기 쉬움)
using DataMap = std::map<std::string, std::vector<int>>;
int main() {
DataMap data;
data[numbers] = {1, 2, 3};
}
예시 2: 템플릿 별칭
typedef와 using의 가장 결정적인 차이는 템플릿 별칭 지원 여부입니다. typedef는 이미 완성된 하나의 타입에만 이름을 붙일 수 있을 뿐 타입 매개변수를 받는 별칭을 만들 수 없어서, C++11 이전에는 이런 경우 별도의 클래스 템플릿으로 우회해야 했습니다. using은 이 제약을 없애고 template<typename T> using Vec = std::vector<T>;처럼 제네릭 별칭을 직접 선언할 수 있게 해, 제네릭 프로그래밍에서 반복되는 타입 조합을 훨씬 간결하게 표현할 수 있습니다.
// ❌ typedef는 템플릿 별칭 불가
template<typename T>
typedef std::vector<T> Vec; // 에러
// ✅ using은 템플릿 별칭 가능
template<typename T>
using Vec = std::vector<T>;
Vec<int> numbers = {1, 2, 3};
Vec<std::string> names = {"Alice", "Bob"};
예시 3: 함수 포인터
콜백 함수를 인자로 받거나 함수 테이블을 구현할 때 함수 포인터 타입을 자주 다루게 되는데, C 스타일 문법은 반환 타입·별칭 이름·매개변수 목록이 한데 뒤섞여 있어 처음 보는 사람은 어디까지가 타입이고 어디부터가 이름인지 헷갈리기 쉽습니다. using으로 선언하면 “이름은 이런 시그니처를 가진 함수 포인터 타입이다”라는 문장 구조 그대로 읽히므로, 아래 Operation처럼 함수 포인터를 변수처럼 다루는 코드의 가독성이 크게 좋아집니다.
// typedef (복잡)
typedef int (*Operation)(int, int);
// using (명확)
using Operation = int (*)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
Operation op = add;
std::cout << op(10, 5) << std::endl; // 15
}
예시 4: 중첩 타입
STL 컨테이너들이 value_type, iterator처럼 클래스 내부에 관련 타입을 using으로 노출해두는 관례를 따르면, 제네릭 코드를 작성할 때 그 컨테이너가 어떤 값 타입과 반복자 타입을 쓰는지 템플릿 매개변수만으로 알아낼 수 있습니다. 이런 중첩 타입 별칭은 사용자 정의 클래스를 표준 라이브러리 알고리즘이나 다른 제네릭 코드와 매끄럽게 호환되도록 만드는 관용적인 방법입니다.
template<typename T>
class Container {
public:
using value_type = T;
using iterator = T*;
using const_iterator = const T*;
};
Container<int>::value_type x = 10;
Container<int>::iterator it;
템플릿 별칭 활용
std::unique_ptr<T>나 std::map<std::string, T>처럼 자주 쓰는 템플릿 조합에 짧은 이름을 붙여두면, 코드베이스 전체에서 일관된 네이밍을 유지하면서도 타이핑을 크게 줄일 수 있습니다. 다만 팀 전체가 표준 라이브러리 이름 대신 이런 별칭에 익숙해져야 한다는 학습 비용이 있으므로, 팀 컨벤션으로 명확히 문서화해두는 것이 좋습니다.
// 스마트 포인터 별칭
template<typename T>
using UniquePtr = std::unique_ptr<T>;
template<typename T>
using SharedPtr = std::shared_ptr<T>;
// 컨테이너 별칭
template<typename T>
using StringMap = std::map<std::string, T>;
StringMap<int> ages;
ages[Alice] = 30;
자주 발생하는 문제
typedef에서 using으로 마이그레이션하거나 새 코드를 작성할 때 실무에서 자주 부딪히는 네 가지 상황을 정리했습니다. 대부분은 typedef의 문법적 한계에서 비롯되며, using으로 바꾸는 것만으로 자연스럽게 해결됩니다.
문제 1: typedef 순서
typedef는 마치 변수를 선언하듯 타입과 이름이 뒤섞인 순서로 작성되기 때문에, 함수 포인터처럼 복잡한 타입일수록 별칭 이름이 어디에 있는지 찾기 어려워집니다. using은 항상 “별칭 이름이 왼쪽, 실제 타입이 오른쪽”이라는 일관된 순서를 지키므로, 복잡한 타입을 다룰 때도 시선이 헤매지 않습니다.
// ❌ 읽기 어려움
typedef void (*FuncPtr)(int);
// ✅ using이 더 명확
using FuncPtr = void (*)(int);
문제 2: 템플릿 별칭
앞서 살펴본 것처럼 typedef 앞에 template 선언을 붙이는 것 자체가 문법 오류입니다. 이 제약 때문에 C++11 이전에는 템플릿 형태의 별칭이 필요할 때 구조체 안에 typedef를 감싸는 우회 기법을 써야 했지만, using이 도입되면서 이런 우회가 더 이상 필요 없어졌습니다.
// ❌ typedef 불가
template<typename T>
typedef std::vector<T> Vec;
// ✅ using 사용
template<typename T>
using Vec = std::vector<T>;
문제 3: 네임스페이스
라이브러리를 설계할 때 내부에서 쓰는 표준 타입에 네임스페이스 안에서 별칭을 붙여두면, 사용자에게는 라이브러리 고유의 어휘로 API를 노출하면서도 내부적으로는 표준 타입 그대로 활용할 수 있습니다. 아래처럼 MyLib::String으로 노출해두면 나중에 이 라이브러리가 커스텀 문자열 타입으로 내부 구현을 바꾸더라도, 별칭 선언만 수정하면 되고 사용자 코드는 전혀 손댈 필요가 없습니다.
namespace MyLib {
using String = std::string;
using StringVector = std::vector<String>;
}
MyLib::String s = "Hello";
문제 4: 전방 선언
C에서 넘어온 typedef struct Node Node; 패턴은 struct 키워드를 매번 붙이지 않고도 Node라는 이름만으로 타입을 참조할 수 있게 해주는 관용구로, C++로 넘어와서도 레거시 C API와 상호 운용할 때 여전히 자주 보입니다. using Node = struct Node;로 똑같이 표현할 수 있지만, 사실 C++에서는 struct 정의 자체가 이미 클래스 이름을 도입하기 때문에 이런 별칭 없이도 Node를 바로 쓸 수 있어 실용적으로는 생략되는 경우가 많습니다.
// typedef
typedef struct Node Node;
// using
using Node = struct Node;
struct Node {
int data;
Node* next;
};
권장사항
지금까지 살펴본 차이를 종합하면 결론은 명확합니다. C++11 이상을 대상으로 하는 새 코드에서는 using을 기본으로 쓰고, typedef는 C++11 이전 표준을 지원해야 하는 레거시 코드베이스를 유지보수할 때만 예외적으로 남겨두는 것이 합리적입니다.
// ✅ using 권장 (C++11 이후)
using IntVector = std::vector<int>;
using StringMap = std::map<std::string, int>;
// typedef는 레거시 코드에서만
typedef std::vector<int> IntVector;
실무 패턴
지금까지의 문법 규칙을 실제 프로젝트에 적용할 수 있는 세 가지 대표 패턴 — 플랫폼별 타입 추상화, 자주 쓰는 STL 컨테이너 축약, 콜백 시그니처 통일 — 로 정리해 보겠습니다.
패턴 1: 플랫폼 독립 타입
운영체제마다 파일 핸들이나 소켓 핸들의 실제 타입이 다른 경우, 전처리기 조건문과 using을 함께 쓰면 플랫폼별 차이를 타입 별칭 선언 한 곳에 몰아넣을 수 있습니다. 이렇게 하면 나머지 코드는 FileHandle, SocketHandle이라는 추상화된 이름만 사용하면 되므로, 플랫폼 분기 코드가 애플리케이션 로직 곳곳에 흩어지는 것을 막을 수 있습니다.
// 플랫폼별 타입 정의
#ifdef _WIN32
using FileHandle = void*;
using SocketHandle = unsigned long long;
#else
using FileHandle = int;
using SocketHandle = int;
#endif
// 사용
FileHandle openFile(const std::string& path) {
// 플랫폼 독립적 코드
}
패턴 2: STL 컨테이너 별칭
std::vector, std::map, std::set 같은 이름을 그대로 쓰는 것도 충분히 명확하지만, 일부 팀은 더 짧은 이름의 템플릿 별칭을 프로젝트 전역에 정의해두고 통일된 스타일로 사용하기도 합니다. 이런 접근은 타이핑을 줄여주는 대신 표준 라이브러리에 익숙한 외부 개발자나 신규 합류자에게는 다소 낯설게 느껴질 수 있으므로, 팀 컨벤션 문서에 명시해 두는 것이 좋습니다.
// 자주 사용하는 컨테이너 별칭
template<typename T>
using Vector = std::vector<T>;
template<typename K, typename V>
using Map = std::map<K, V>;
template<typename T>
using Set = std::set<T>;
// 사용
Vector<int> numbers = {1, 2, 3};
Map<std::string, int> ages = {{"Alice", 30}};
Set<std::string> names = {"Bob", "Charlie"};
패턴 3: 함수 타입 별칭
이벤트 기반 시스템이나 콜백을 많이 다루는 코드에서는 std::function<void(int)>처럼 긴 함수 타입 시그니처가 클래스 멤버, 함수 매개변수, 반환 타입 등 여러 곳에 반복해서 등장하기 마련입니다. 아래 EventSystem처럼 Callback, ErrorHandler 같은 의미 있는 이름으로 미리 별칭을 정의해두면, 시그니처 자체보다 그 콜백이 어떤 역할을 하는지가 코드에서 훨씬 명확하게 드러납니다.
// 콜백 타입 정의
using Callback = std::function<void(int)>;
using ErrorHandler = std::function<void(const std::string&)>;
using Validator = std::function<bool(const std::string&)>;
class EventSystem {
std::vector<Callback> callbacks_;
ErrorHandler errorHandler_;
public:
void setErrorHandler(ErrorHandler handler) {
errorHandler_ = std::move(handler);
}
void addCallback(Callback callback) {
callbacks_.push_back(std::move(callback));
}
void trigger(int value) {
for (auto& callback : callbacks_) {
callback(value);
}
}
};
FAQ
Q1: typedef vs using의 차이는?
A:
- using: 더 읽기 쉬움, 템플릿 별칭 가능, 권장
- typedef: C 스타일, 템플릿 별칭 불가, 레거시
// typedef: 복잡
typedef std::map<std::string, std::vector<int>> DataMap;
// using: 명확
using DataMap = std::map<std::string, std::vector<int>>;
Q2: 언제 사용하나요?
A:
- 긴 타입 단축: 가독성 향상
- 유지보수: 타입 변경 시 한 곳만 수정
- 템플릿 별칭: 제네릭 타입 정의
- 플랫폼 독립: 플랫폼별 타입 추상화
using UserId = int64_t; // 나중에 string으로 변경 가능
Q3: 성능 차이는?
A: 없습니다. 컴파일 타임에만 동작하는 별칭입니다.
using IntVector = std::vector<int>;
IntVector v = {1, 2, 3};
// std::vector<int>와 동일한 성능
Q4: 템플릿 별칭은 어떻게 만드나요?
A: using만 가능합니다. typedef는 템플릿 별칭을 지원하지 않습니다.
C/C++ 예제 코드입니다.
// ❌ typedef: 불가
template<typename T>
typedef std::vector<T> Vec; // 에러
// ✅ using: 가능
template<typename T>
using Vec = std::vector<T>;
Vec<int> numbers = {1, 2, 3};
Q5: 권장 방식은?
A: C++11 이후 using 권장합니다. 더 읽기 쉽고 템플릿 별칭을 지원합니다.
C/C++ 예제 코드입니다.
// ✅ using 권장
using IntVector = std::vector<int>;
using FuncPtr = void (*)(int);
// ❌ typedef: 레거시
typedef std::vector<int> IntVector;
typedef void (*FuncPtr)(int);
Q6: 함수 포인터 별칭은?
A: using이 훨씬 읽기 쉽습니다.
C/C++ 예제 코드입니다.
// typedef: 복잡
typedef int (*Operation)(int, int);
// using: 명확
using Operation = int (*)(int, int);
// 또는 std::function
using Operation = std::function<int(int, int)>;
Q7: 중첩 타입은 어떻게 정의하나요?
A: 클래스 내부에서 using 을 사용합니다.
template<typename T>
class Container {
public:
using value_type = T;
using iterator = T*;
using const_iterator = const T*;
using size_type = size_t;
};
// 사용
Container<int>::value_type x = 10;
Container<int>::iterator it;
Q8: 타입 별칭 학습 리소스는?
A:
- “Effective Modern C++” by Scott Meyers (Item 9)
- “C++ Primer” by Stanley Lippman
- cppreference.com - Type alias
관련 글: using 상세, 템플릿 기초, auto 타입 추론, decltype.
한 줄 요약: using은 타입 별칭을 만드는 C++11 키워드로, typedef보다 읽기 쉽고 템플릿 별칭을 지원합니다.
심화: 의존적 이름(dependent name)과 typename
템플릿 안에서 기본 클래스의 중첩 타입을 가리킬 때는 typename이 필요할 수 있습니다. using으로 꺼내 두면 호출부가 읽기 쉬워집니다.
template<typename T>
struct Wrapper {
using value_type = typename T::value_type; // T에 따라 의존적 이름
};
심화: using + 트레일링 리턴 (가독성)
트레일링 리턴 타입 문법(auto func() -> ReturnType)은 매개변수 타입에 의존하는 반환 타입을 표현해야 할 때 유용한데, decltype 표현식이 길어지면 이마저도 가독성이 떨어질 수 있습니다. 이럴 때 using으로 결과 타입에 미리 이름을 붙여두면, 함수 시그니처 자체는 짧게 유지하면서도 반환 타입이 무엇을 의미하는지 명확히 전달할 수 있습니다.
template<typename A, typename B>
auto add(A a, B b) -> decltype(a + b) {
return a + b;
}
// C++14 이후에는 반환 타입 추론과 조합 가능
template<typename F>
using invoke_result_t = std::invoke_result_t<F>; // 결과 타입 별칭
심화: SFINAE·std::enable_if와 별칭
std::enable_if_t<조건, 타입>은 템플릿 함수를 특정 조건을 만족하는 타입에만 활성화하고 싶을 때 쓰는 SFINAE(Substitution Failure Is Not An Error) 기법의 핵심 도구이지만, 조건이 복잡해질수록 함수 시그니처가 읽기 힘들어지는 부작용이 있습니다. 자주 쓰는 조건 조합을 아래처럼 템플릿 별칭으로 미리 정의해두면, 함수 선언부에서는 그 의도(예: “정수 타입에만 활성화”)만 이름으로 드러나고 실제 enable_if 세부 구현은 별칭 뒤에 숨길 수 있습니다.
template<typename T>
using enable_if_integral_t = std::enable_if_t<std::is_integral_v<T>, int>;
template<typename T, enable_if_integral_t<T> = 0>
int bits(T) { return sizeof(T) * 8; }
별칭 덕분에 std::enable_if_t<...> 덩어리를 매번 쓰지 않아도 됩니다.
심화: 성능·ABI
typedef와 using은 컴파일 타임 별칭이라 런타임 비용 차이는 없습니다. 바이너리 크기도 동일한 타입을 가리키면 같습니다. 성능 이슈는 별칭이 가리키는 실제 타입(예: int vs long on LP64)에서 생깁니다.
심화: 디버깅·흔한 실수
타입 별칭 자체는 단순한 기능이지만, 실무에서는 아래와 같은 세 가지 실수가 반복적으로 발생합니다. 특히 매크로와의 이름 충돌은 컴파일 오류 메시지만 봐서는 원인을 파악하기 어려워 디버깅 시간이 오래 걸리는 경우가 많습니다.
| 실수 | 설명 |
|---|---|
템플릿 별칭에 typedef 사용 | C++11 이전 방식의 우회 코드로 복잡해짐 → using으로 통일 |
| 포인터 별칭에서 괄호 위치 | using P = int*; using Arr = P[10]; vs int*[10] 혼동 → 한 줄씩 검증 |
| 매크로와 이름 충돌 | 레거시 #define ulong 같은 매크로가 있으면 using 선언과 충돌 → #undef 또는 이름 변경 |
심화: 실전 예제 — 핸들·콜백 타입 묶기
네트워크 라이브러리나 비동기 API를 설계할 때는 에러 코드와 콜백 시그니처가 세트로 반복되는 경우가 많습니다. 아래처럼 이 둘을 각각 별칭으로 묶어 API 함수 시그니처에 노출하면, 호출자는 매번 std::function<void(int, std::string)> 전체를 타이핑하는 대신 AsyncCallback 하나로 의도를 명확히 전달할 수 있습니다.
#include <functional>
#include <string>
using ErrorCode = int;
using AsyncCallback = std::function<void(ErrorCode, std::string)>;
void download(std::string url, AsyncCallback cb) {
// ...
cb(0, "ok");
}
API 경계에서 긴 std::function<void(...) 를 반복하지 않게 해 팀 합의와 리뷰가 쉬워집니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ using vs typedef | “타입 별칭” 가이드
- C++ 템플릿 | “제네릭 프로그래밍” 초보자 가이드
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
- C++ decltype | “타입 추출” 가이드
관련 글
이 글에서 다루는 키워드 (관련 검색어)
C++, typedef, using, type-alias, C++11 등으로 검색하시면 이 글이 도움이 됩니다.