Most explanations of these two keywords stop at “they do the same thing, pick one.” And they do, for every alias that is not a template. The difference that matters is this: using can be templated and typedef cannot. That one capability is why using is the default in modern C++, and it is the thing most short explanations leave out. Everything else is a matter of which reads better.
This guide covers both syntaxes, the alias template that only using can express, the one job where the old typedef workaround is still required, and the mistake that survives both keywords: assuming an alias gives you a new type that the compiler will police. If you are new to the language, our complete guide to C++ programming covers the groundwork. Every program on this page was compiled for this article on Ubuntu 24.04 with GCC 13.3, using g++ -std=c++17 -Wall -Wextra -Wpedantic and gcc -std=c17 for the C examples; the examples this refresh replaces did not build, so every code block here has been rewritten and run. Every output block and every compiler error is captured verbatim.
What Is a Type Alias in C++?
A type alias gives an existing type a second name. C++ has two syntaxes for it: typedef old_name new_name;, inherited from C, and using new_name = old_name;, added in C++11. Both produce a synonym, not a new type — the compiler treats the alias and the original as the same type everywhere. The functional difference that matters is that using can be templated and typedef cannot. There is one small difference in the other direction: a single typedef can declare several aliases at once, as in typedef int A, B;, which the using form has no syntax for.
The using form is the one to reach for in new C++ code, for two reasons the rest of this page demonstrates: it reads left to right like every other declaration in the language, and it is the only one that works with templates.
| Capability | typedef | using |
|---|---|---|
| Ordinary type alias | Yes | Yes |
| Alias template | No | Yes |
| Several aliases in one declaration | Yes | No |
| Creates a distinct type | No | No |
| Available in C | Yes | No |
typedef and using produce the same type — using simply puts the new name first. Only using can be templated, which is the capability that makes it the default in modern C++. Neither creates a distinct type, so an alias will not stop you passing feet to a function expecting metres.
The typedef Syntax
typedef is a C keyword that C++ inherited. The declaration reads like a variable declaration with typedef stuck on the front, which is exactly what it is — the new name sits where the variable name would go:
typedef double Metres;
typedef unsigned long ulong;
typedef std::map<std::string, int> MarksList;
That “name goes where the variable would” rule is why typedef gets hard to read the moment the type is not a simple one. For a pointer to a function it puts the name in the middle:
typedef int (*Handler)(int, int);
Handler is buried between the return type and the parameter list. Nothing about the line tells you at a glance that Handler is the thing being declared.
The using Syntax
C++11 added the alias declaration. It puts the new name first, followed by =, followed by the type:
using Metres = double;
using ulong = unsigned long;
using MarksList = std::map<std::string, int>;
using Handler = int (*)(int, int);
Every one of those lines answers “what is being declared here?” in its first word. Compare the two function-pointer forms directly and the argument makes itself:
typedef int (*Handler)(int, int); // name in the middle
using Handler = int (*)(int, int); // name first
Both declare exactly the same type. Verified with std::is_same_v<Handler, HandlerOld>, which prints true.
One misconception worth heading off: using is not a C keyword. It is often described as belonging to “C and C++”, but the type alias declaration is C++11 and later only, and a C compiler rejects it outright:
cusing.c:1:1: error: unknown type name 'using'
1 | using ch = char;
| ^~~~~
In C you have typedef and nothing else. (C23 does add typeof, but that is a different feature.) If you are writing C, the data types in C guide covers what you can alias.
The One Real Difference: Alias Templates
Here is the difference that justifies the keyword’s existence. Try to make a typedef depend on a template parameter and the compiler stops you at the declaration:
template <typename T>
typedef std::map<std::string, T> Dict; // the obvious thing to try
tdtpl.cpp:4:1: error: template declaration of 'typedef'
4 | typedef std::map<std::string, T> Dict;
| ^~~~~~~
That is not a syntax slip you can work around by rearranging. typedef is a declaration specifier, and the grammar for template declarations does not allow one to declare a typedef. With using, the same idea is a single line:
template <typename T>
using Dict = std::map<std::string, T>;
Dict<int> marks; // std::map<std::string, int>
Dict<double> scores; // std::map<std::string, double>
Before C++11, the only way to get this was to wrap the typedef in a class template and pull the type back out through a member:
template <typename T>
struct DictOld { typedef std::map<std::string, T> type; };
DictOld<int>::type marks; // same type, four extra tokens at every use
Both produce the identical type — std::is_same_v<Dict<int>, DictOld<int>::type> prints true. The difference is what happens when you use it inside another template:
template <typename T>
void oldWay() { DictOld<T>::type d; }
typename_cost.cpp:9:17: error: need 'typename' before 'DictOld<T>::type'
because 'DictOld<T>' is a dependent scope
9 | void oldWay() { DictOld<T>::type d; (void)d; }
| ^~~~~~~~~~
The alias template version needs no typename and no ::type:
template <typename T>
void newWay() { Dict<T> d; } // compiles as written
This is the whole reason the standard library is full of using declarations. Worth noting for anyone who has heard that C++20 removed the typename requirement: it removed it in positions where only a type can appear, not here. I checked — GCC 13.3 rejects the line above under both -std=c++17 and -std=c++20.
The One Thing Alias Templates Cannot Do
The comparison is not one-sided, and most articles on this topic stop before reaching the exception. Alias templates cannot be explicitly or partially specialised. Try either and the parser refuses:
template <class T> using Vec = std::vector<T>;
template <> using Vec<bool> = int;
spec.cpp:3:13: error: expected unqualified-id before 'using'
3 | template <> using Vec<bool> = int;
| ^~~~~
Partial specialisation fails the same way:
template <class T, class U> using Pair = std::vector<T>;
template <class T> using Pair<T, int> = std::vector<int>;
partial.cpp:3:30: error: expected '=' before '<' token
3 | template <class T> using Pair<T, int> = std::vector<int>;
| ^
The class-template-with-a-member-type idiom has no such limit:
template <class T> struct VecT { using type = std::vector<T>; };
template <> struct VecT<bool> { using type = int; };
Output:
true true
So when you need the alias itself to vary by type — a trait, a policy, a type map — you still write the struct. Note that the member inside it is now a using declaration too; the wrapper is what you need, not the old keyword.
Neither Keyword Creates a New Type
This is where both keywords disappoint people. An alias is a synonym. The compiler cannot tell it from the original:
#include <type_traits>
typedef double Metres;
using Feet = double;
double gap(Metres m) { return m; }
int main()
{
std::cout << std::boolalpha;
std::cout << "Metres is double? " << std::is_same_v<Metres, double> << '\n';
std::cout << "Metres is Feet? " << std::is_same_v<Metres, Feet> << '\n';
Feet f = 30.0;
std::cout << "gap(Feet) accepted: " << gap(f) << '\n';
}
Output:
Metres is double? true
Metres is Feet? true
gap(Feet) accepted: 30
Metres and Feet are the same type. A function that takes metres accepts feet without a warning, at any warning level, because from the compiler’s point of view nothing unusual happened — you passed a double to a function taking a double.
If you want the compiler to police the distinction, an alias is the wrong tool. A one-member struct is the cheapest thing that works:
// strong_bad.cpp
struct Metres { double value; };
struct Feet { double value; };
double gap(Metres m) { return m.value; }
int main() { return (int)gap(Feet{30.0}); } // line 4
strong_bad.cpp:4:30: error: could not convert 'Feet{3.0e+1}' from 'Feet' to 'Metres'
4 | int main() { return (int)gap(Feet{30.0}); }
| ^~~~~~~~~~
Now the mistake is a build failure. An enum class does the same job for integral values. Use aliases for readability; use distinct types for safety. They are different problems and only one of them typedef and using can solve.
When to Use Each
| Situation | Use | Why |
|---|---|---|
| New C++ code, simple alias | using | Reads left to right; consistent with the rest of the language |
| Alias that depends on a template parameter | using | The only option — typedef cannot be templated |
| Function pointer or array type | using | Puts the name first instead of burying it |
| An alias that must vary by type | struct wrapper with a member using | Alias templates cannot be specialised |
| Writing C, or a header shared with C | typedef | using is not a C keyword |
Existing C++ code full of typedef | Leave it | They are interchangeable here; churn buys nothing |
| You want the compiler to catch a mix-up | Neither | Use a struct wrapper or an enum class |
The short version: using for new C++ aliases, typedef when you are writing C or a header C code must include, and neither when what you actually want is type safety. typedef is not deprecated in C++ and there is no reason to hunt it down in code that already works. Microsoft’s aliases and typedefs reference covers the same ground from the MSVC side if you want a second account.
Four Alias Mistakes That Do Not Compile
These four turn up constantly in tutorial code, and all four are build failures rather than subtle bugs — which is good news, provided you actually build. Here is what GCC 13.3 says about each.
A stray operator in the alias declaration. using ulong &= unsigned long; — there is no &= in an alias declaration, only =:
old1.cpp:3:7: error: expected nested-name-specifier before 'ulong'
3 | using ulong &= unsigned long;
| ^~~~~
Aliasing a reserved name. typedef unsigned int size_t; collides with the standard library, and picks the wrong type as well:
old2.cpp:4:22: error: conflicting declaration 'typedef unsigned int size_t'
4 | typedef unsigned int size_t;
| ^~~~~~
/usr/lib/gcc/x86_64-linux-gnu/13/include/stddef.h:214:23: note: previous declaration
as 'typedef long unsigned int size_t'
On this machine size_t is 8 bytes and unsigned int is 4. Aliasing size_t to unsigned int would halve it even if the redefinition were allowed — and size_t is a reserved name, so you should not be declaring it at all.
Forgetting the namespace inside the aliased type. The alias itself is fine; string is not:
old3.cpp:2:18: error: 'string' was not declared in this scope
2 | typedef std::map<string, int> marks_list;
| ^~~~~~
Two claims that circulate alongside these are worth correcting too, because neither produces an error — they just leave you with the wrong model. Typedef names are sometimes described as “only available inside the function or class where they are declared.” They are not specially restricted: a typedef at namespace scope is visible for the rest of that scope like any other declaration. And the alias declaration is still often called a recent addition. C++11 was standardised fifteen years ago.
Key Takeaways
typedef old new;andusing new = old;produce identical types for any single alias that is not a template. Neither is faster, andstd::is_same_vreports them as the same type.- Only
usingcan be templated.template <typename T> typedef ...is rejected by the grammar: error: template declaration of ‘typedef’. This is the entire reason the alias declaration exists. usingputs the name first, which matters most for function pointers, wheretypedefburies the name between the return type and the parameters.usingis C++ only. A C compiler reports unknown type name ‘using’. In C,typedefis all there is.- Alias templates cannot be specialised. When the alias must vary by type, you still need a class template with a member type — though the member inside it should be a
using. - Neither keyword creates a distinct type.
MetresandFeetaliased todoubleare interchangeable, and passing one where the other is expected compiles silently. Use astructwrapper or anenum classwhen you want the compiler to object. - Do not alias reserved names such as
size_t; the redefinition conflicts with the standard library.
Frequently Asked Questions
Conclusion
Two keywords, one difference, and a widespread belief that there is none. The practical rule fits in a sentence: write using in new C++, write typedef in C, and reach for a struct wrapper when what you actually wanted was a type the compiler would defend.
The deeper point is the one the old version of this page stated and then buried. An alias changes what you type, not what the compiler checks. It makes std::map<std::string, std::vector<int>> readable, and it will not stop you passing feet to a function expecting metres. Knowing which of those problems you have is most of the work. Our other C++ programming guides take the same approach, and the overview of what C++23 added shows where the language has gone since the alias declaration arrived.
What I Could Not Verify
Everything here was compiled on one machine: an Ubuntu 24.04 VM running GCC 13.3, with g++ for the C++ examples and gcc for the single C example. No other compiler was tested, so the exact wording of every error message is GCC’s — Clang and MSVC reject the same code, but they phrase it differently, and the size_t conflict in particular depends on which standard library header your toolchain supplies. The size_t widths quoted are those of a 64-bit Linux build; on a 32-bit target unsigned int and size_t may well be the same width, which changes that example’s second problem but not its first. The claim about C++20 and typename is a single observation from GCC 13.3 in one syntactic position, not a survey of the rule.


