Disclaimer: ChatGPT generated document.
C++ gives you several mechanisms for introducing names for types and related entities. At first glance, constructs such as typedef and using can look like little more than conveniences for shortening long type names:
typedef unsigned long long ull;
using ull = unsigned long long;These two declarations appear equivalent—and for a simple type alias, they essentially are.
But modern C++'s using keyword does considerably more than replace typedef. It participates in alias templates, namespace declarations, inheritance, overload management, enum handling, and other parts of the language.
Understanding these mechanisms is therefore less about memorizing two syntaxes and more about understanding an important C++ idea:
A type alias introduces another name for a type. It does not normally introduce a new type.
That distinction affects type safety, overload resolution, templates, API design, and how you should use aliases in real projects.
This article covers typedef, using, alias templates, namespace aliases, using-declarations, using-directives, inherited constructors, using enum, strong types, and the best practices surrounding all of them.
Consider this type:
std::unordered_map<
std::string,
std::vector<std::pair<int, double>>
>If it appears repeatedly, spelling it out everywhere makes code noisy:
std::unordered_map<
std::string,
std::vector<std::pair<int, double>>
> measurements;
std::unordered_map<
std::string,
std::vector<std::pair<int, double>>
>::iterator it;An alias gives the type a meaningful name:
using Measurements =
std::unordered_map<
std::string,
std::vector<std::pair<int, double>>
>;
Measurements measurements;
Measurements::iterator it;This is often presented merely as "saving typing," but that undersells aliases.
A good alias can express meaning:
using UserId = std::uint64_t;
using Timestamp = std::chrono::system_clock::time_point;
using Callback = std::function<void(int)>;The underlying representation may be complicated—or extremely simple. Either way, the alias can communicate what the type means in the program.
Before C++11, typedef was the standard mechanism for defining a type alias.
Its syntax is:
typedef ExistingType AliasName;For example:
typedef unsigned int uint;
typedef long long int64;
typedef std::vector<int> IntVector;After these declarations:
uint x = 10;
IntVector values;are equivalent to:
unsigned int x = 10;
std::vector<int> values;This is one of the most important facts about it.
typedef int UserId;does not create a distinct UserId type.
The compiler still considers UserId to be int.
For example:
typedef int UserId;
void process(int);
void process(UserId); // error: same function signatureThe compiler effectively sees two declarations of:
void process(int);Likewise:
typedef int UserId;
typedef int ProductId;
UserId user = 42;
ProductId product = user; // perfectly validThere is no additional type safety.
This applies equally to using aliases.
C++11 introduced alias declarations:
using AliasName = ExistingType;For example:
using uint = unsigned int;
using IntVector = std::vector<int>;
using Callback = std::function<void(int)>;Compare:
typedef std::vector<int> IntVector;with:
using IntVector = std::vector<int>;They mean essentially the same thing.
For ordinary aliases, you can think of:
typedef T Name;as equivalent to:
using Name = T;For new code, using is normally preferred.
There are several reasons.
Consider:
using Counter = unsigned long;The structure is obvious:
Counter = unsigned long
With typedef:
typedef unsigned long Counter;you need to recognize declaration syntax to determine which identifier is the alias.
For simple declarations this barely matters. For complicated declarations it matters considerably more.
Suppose we want a pointer to a function taking two integers and returning an integer.
With typedef:
typedef int (*Operation)(int, int);With using:
using Operation = int (*)(int, int);Then:
int add(int a, int b)
{
return a + b;
}
Operation op = add;The using declaration has a useful conceptual structure:
using Name = Type;The type itself may still contain complicated C++ declarator syntax, but the alias name is clearly separated from it.
With typedef:
typedef int Matrix[4][4];With using:
using Matrix = int[4][4];Then:
Matrix m{};Again, using makes the relationship more explicit:
Matrix = int[4][4]
The most important technical advantage of using over typedef is that using supports alias templates.
Suppose you frequently use:
std::vector<T>with a custom allocator or some other template machinery.
You can write:
template<typename T>
using Vec = std::vector<T>;Then:
Vec<int> numbers;
Vec<std::string> names;
Vec<double> samples;This cannot be expressed directly with a templated typedef.
You cannot write:
template<typename T>
typedef std::vector<T> Vec; // invalidHistorically, programmers worked around this using a class template:
template<typename T>
struct Vec
{
typedef std::vector<T> type;
};Usage becomes:
Vec<int>::type numbers;Modern C++ replaces much of this pattern with:
template<typename T>
using Vec = std::vector<T>;This is one of the strongest reasons to standardize on using in modern C++.
Alias templates become particularly useful in generic programming.
For example:
template<typename T>
using Ptr = T*;
Ptr<int> p;is equivalent to:
int* p;A more realistic example:
template<typename T>
using Vector = std::vector<T>;
template<typename T>
using Iterator = typename Vector<T>::iterator;Then:
Iterator<int> it;means:
std::vector<int>::iterator it;The standard library itself provides many alias templates following this philosophy.
Examples include:
std::remove_reference_t<T>
std::remove_const_t<T>
std::enable_if_t<Condition, T>instead of requiring the older:
typename std::remove_reference<T>::type
typename std::remove_const<T>::type
typename std::enable_if<Condition, T>::typeFor example:
using T = std::remove_reference_t<int&>;instead of:
using T = typename std::remove_reference<int&>::type;The _t convention generally means:
"This template name directly represents the resulting type."
You'll still encounter typedef constantly in older C++ libraries:
template<typename T>
class Container
{
public:
typedef T value_type;
typedef T* pointer;
typedef T& reference;
};Modern C++ usually writes:
template<typename T>
class Container
{
public:
using value_type = T;
using pointer = T*;
using reference = T&;
};Both work.
The second style is generally easier to read.
For new C++11-and-later code, there are relatively few reasons.
Use typedef when:
- maintaining an older codebase that consistently uses it;
- supporting pre-C++11 compilers;
- following an established API or project convention;
- interacting with old C/C++ headers where matching the surrounding style improves clarity.
Otherwise:
using Name = Type;is generally preferable.
This does not mean existing typedef code is bad.
Code such as:
typedef unsigned long size_type;is perfectly valid modern C++.
There is usually little reason to mechanically rewrite an entire stable codebase solely to replace every typedef with using.
C programmers often write:
typedef struct Person {
char name[100];
int age;
} Person;This pattern exists because C distinguishes the struct tag from ordinary type names.
Without the typedef, C code would typically write:
struct Person person;rather than:
Person person;C++ works differently.
In C++, this is enough:
struct Person {
std::string name;
int age;
};and you can immediately write:
Person person;Therefore this C pattern:
typedef struct Person {
...
} Person;is normally unnecessary in C++.
Prefer:
struct Person {
...
};You may also encounter C code like:
typedef struct {
int x;
int y;
} Point;Again, normal C++ would simply use:
struct Point {
int x;
int y;
};The C++ version is simpler and gives the class itself the desired name.
Consider:
using Meters = double;
using Seconds = double;This looks expressive:
Meters distance = 100.0;
Seconds duration = 9.58;But the compiler sees both as double.
Therefore:
distance = duration;is legal.
So is:
void setDistance(Meters);
Seconds time = 5.0;
setDistance(time);This is one of the biggest traps surrounding aliases.
An alias improves readability, not type identity.
If mixing two values would be a bug, consider introducing a distinct class.
Instead of:
using UserId = std::uint64_t;
using ProductId = std::uint64_t;you could write:
struct UserId
{
std::uint64_t value;
};
struct ProductId
{
std::uint64_t value;
};Now:
UserId user{42};
ProductId product{42};
user = product; // errorThe types are genuinely distinct.
This is often called a strong type, strong typedef, or newtype-style wrapper.
Despite the phrase "strong typedef," ordinary C++ typedef is not strong.
The choice is fundamentally about semantics.
Use an alias when:
"These are the same type; I merely want a better name."
Use a wrapper type when:
"These values have different meanings and accidentally mixing them should be rejected."
For example:
using FileSize = std::uint64_t;may be perfectly reasonable if FileSize is simply a descriptive name.
But:
using Celsius = double;
using Fahrenheit = double;can be dangerous because:
Celsius c = 20;
Fahrenheit f = c;compiles despite potentially being conceptually wrong.
Separate classes may be preferable.
One source of confusion is that using is not exclusively the type-alias keyword.
Depending on context, it can mean several things.
Important forms include:
using Name = Type; // type alias
using namespace ns; // using-directive
using ns::name; // using-declaration
using Base::member; // expose inherited member
using Base::Base; // inherit constructors
using enum EnumType; // import enumerators, C++20These features share a keyword but serve different purposes.
Let's examine them individually.
Suppose you have:
#include <iostream>
int main()
{
std::cout << "Hello\n";
}You can introduce one name from a namespace:
using std::cout;
int main()
{
cout << "Hello\n";
}This is a using-declaration.
It introduces a specific name into the current scope.
You can do:
using std::cout;
using std::string;
using std::vector;This is different from a type alias.
Notice the absence of =:
using std::string;versus:
using String = std::string;The first introduces the existing name string.
The second creates an alias named String.
You can also write:
using namespace std;Now names from std can generally be used without qualification:
cout << "Hello\n";
vector<int> values;
string name;instead of:
std::cout << "Hello\n";
std::vector<int> values;
std::string name;This is called a using-directive.
It is convenient—but can cause problems.
The issue is not that it is inherently invalid or universally forbidden.
The issue is namespace pollution.
Namespaces exist partly to prevent unrelated libraries from claiming the same names.
Imagine:
namespace library_a
{
void print();
}
namespace library_b
{
void print();
}Then:
using namespace library_a;
using namespace library_b;
print();is ambiguous.
Qualification avoids the problem:
library_a::print();
library_b::print();Large namespaces such as std contain many names, so importing everything increases the probability of collisions and makes it less obvious where identifiers come from.
This is one of the strongest practical rules in this article.
Avoid:
// my_header.hpp
using namespace std;A header is textually included into other translation units. A using-directive at namespace scope therefore affects code that includes the header.
That means your header silently changes name lookup for somebody else's source file.
For library code, this is particularly undesirable.
Prefer fully qualified names:
std::vector<int>
std::string
std::size_tor carefully scoped alternatives.
This:
void foo()
{
using namespace std;
vector<int> values;
cout << values.size();
}has limited scope.
It does not pollute the entire translation unit.
Whether to use it is partly stylistic, but local scope makes the risks much smaller.
A more conservative alternative is importing individual names:
void foo()
{
using std::cout;
using std::vector;
vector<int> values;
cout << values.size();
}This communicates exactly which names are being introduced.
A good default policy is:
In headers:
std::vector<int> values;Prefer explicit qualification.
In source files:
using std::string;
using std::vector;Selective using-declarations can be reasonable.
Inside small local scopes:
using namespace some_namespace;can sometimes be convenient when collisions are controlled and the context is obvious.
The broader the scope, the more cautious you should be.
There is another related construct:
namespace fs = std::filesystem;This is a namespace alias.
It is particularly useful for long or deeply nested namespaces.
Instead of:
std::filesystem::path path;
std::filesystem::exists(path);
std::filesystem::directory_iterator it;you can write:
namespace fs = std::filesystem;
fs::path path;
fs::exists(path);
fs::directory_iterator it;Another example:
namespace asio = boost::asio;or:
namespace proto = company::network::protocol::v2;This is often preferable to:
using namespace company::network::protocol::v2;because qualification remains visible:
proto::Message
proto::ConnectionYou get brevity without throwing every name into the current scope.
Aliases are extremely useful for exposing important associated types.
For example:
template<typename T>
class Buffer
{
public:
using value_type = T;
using size_type = std::size_t;
using pointer = T*;
using const_pointer = const T*;
};Clients can then write:
Buffer<int>::value_type x = 42;Generic C++ code frequently relies on conventions like:
T::value_type
T::iterator
T::const_iterator
T::size_typeThe standard containers follow this pattern.
For example:
std::vector<int>::value_type
std::vector<int>::iterator
std::vector<int>::size_typeAliases therefore aren't merely syntactic convenience—they can form part of a type's public interface.
Aliases frequently appear in template code where another C++ concept becomes important: typename.
Consider:
template<typename Container>
void process()
{
Container::value_type value{};
}The compiler cannot automatically know whether:
Container::value_typeis a type or something else, because Container depends on a template parameter.
You generally need:
template<typename Container>
void process()
{
typename Container::value_type value{};
}Or you can alias it:
template<typename Container>
void process()
{
using Value = typename Container::value_type;
Value value{};
}This pattern is common in generic code.
Modern template code often combines aliases with type traits.
Suppose:
template<typename T>
void foo(T&& value)
{
using RawT = std::remove_cvref_t<T>;
}RawT gives the transformed type a local semantic name.
Older code might contain something much noisier:
typedef typename std::remove_reference<T>::type RawT;or multiple nested transformations.
Alias templates and standard _t helpers make this considerably cleaner.
Aliases can also make complex deduced types manageable:
auto expression = /* ... */;
using ExpressionType = decltype(expression);Or:
using Result =
decltype(std::declval<F>()(std::declval<T>()));Modern C++ provides utilities such as:
std::invoke_result_t<F, T>for many common cases, but local aliases remain valuable for simplifying template logic.
Older C++ code commonly contains:
typedef std::vector<int>::iterator Iterator;Modern code can write:
using Iterator = std::vector<int>::iterator;But before creating aliases like this, ask whether the alias is actually necessary.
Modern C++ often lets you avoid spelling iterator types entirely:
auto it = values.begin();instead of:
using Iterator = std::vector<int>::iterator;
Iterator it = values.begin();Aliases are useful, but auto can eliminate unnecessary type repetition.
These solve different problems.
using gives a type a name:
using Value = std::pair<int, std::string>;auto asks the compiler to deduce the type of a variable:
auto value = make_value();They can work together:
using Map = std::unordered_map<std::string, int>;
Map scores;
auto it = scores.find("Alice");A useful principle is:
Use aliases when the type itself deserves a meaningful reusable name. Use
autowhen explicitly spelling the variable's type adds little value.
Another important form is:
using Base::member;This is not a type alias.
It introduces a base-class member into the derived class's scope.
Consider:
class Base
{
public:
void print(int);
void print(double);
};
class Derived : public Base
{
public:
void print(const std::string&);
};The declaration in Derived hides the Base::print overload set during ordinary unqualified lookup.
Therefore:
Derived d;
d.print(42);may not behave as a beginner expects.
You can restore the base overloads:
class Derived : public Base
{
public:
using Base::print;
void print(const std::string&);
};Now the overload set contains the inherited candidates as well.
This is an important use of using-declarations in class hierarchies.
A using-declaration can also expose an inherited member under a different access level.
For example:
class Base
{
protected:
void reset();
};
class Derived : public Base
{
public:
using Base::reset;
};Now clients of Derived can call:
Derived d;
d.reset();even though reset was protected in Base.
This should be done intentionally because it changes the public interface of the derived class.
C++11 also allows:
using Base::Base;to inherit constructors.
For example:
class Base
{
public:
Base(int x);
Base(std::string name);
};
class Derived : public Base
{
public:
using Base::Base;
};This allows construction such as:
Derived a(42);
Derived b("example");without manually forwarding each constructor:
Derived(int x)
: Base(x)
{
}This is useful when the derived class does not need special initialization logic for those constructors.
They work well for thin derived classes:
class LoggingStream : public Stream
{
public:
using Stream::Stream;
void log();
};But they can be inappropriate when the derived class has additional invariants or members that need explicit initialization.
For example:
class Derived : public Base
{
Resource resource;
public:
using Base::Base;
};You should carefully consider whether every inherited constructor leaves Derived in the state you intend.
Constructor inheritance saves boilerplate; it should not replace deliberate class design.
C++20 introduced another form:
using enum EnumType;Suppose:
enum class Color
{
red,
green,
blue
};Normally:
Color color = Color::red;Inside an appropriate scope, you can write:
using enum Color;
Color color = red;This is particularly convenient in switch statements:
std::string_view to_string(Color color)
{
using enum Color;
switch (color)
{
case red:
return "red";
case green:
return "green";
case blue:
return "blue";
}
return "unknown";
}Instead of:
case Color::red:
case Color::green:
case Color::blue:Whether this improves readability depends on context.
For a short function clearly operating on one enum, it can be elegant. In a large scope containing several enums with overlapping enumerator names, it can reduce clarity.
Again, scope matters.
There is an important advanced limitation of alias templates.
An alias template cannot be explicitly or partially specialized in the same way a class template can.
For example, you cannot design:
template<typename T>
using Something = ...;and then partially specialize Something directly as though it were a class template.
When specialization logic is required, a common technique is to use a class template internally:
template<typename T>
struct SomethingImpl
{
using type = T;
};
template<typename T>
struct SomethingImpl<T*>
{
using type = T;
};
template<typename T>
using Something = typename SomethingImpl<T>::type;Now users get the convenient alias:
Something<int*>while specialization happens in the helper class template.
This pattern appears frequently in template metaprogramming.
Suppose a class uses:
class Database
{
public:
using RecordId = std::uint64_t;
};Clients write:
Database::RecordId id;Later, the underlying representation might change.
Aliases can therefore provide a small layer of abstraction.
Similarly:
using Storage = std::unordered_map<Key, Value>;allows internal code to talk about Storage rather than its concrete container representation.
If the implementation later changes to:
using Storage = std::map<Key, Value>;much of the dependent code may remain unchanged.
However, don't overestimate this abstraction.
If clients rely on operations unique to std::unordered_map, changing the alias to std::map may still break them.
An alias hides a spelling—not necessarily an interface.
Suppose a library exposes:
class Widget
{
public:
using Id = std::uint64_t;
};Users may begin writing:
Widget::Idthroughout their code.
That alias is now effectively part of the public API.
Removing or changing it can break downstream code.
Therefore, treat public aliases with the same care as other public declarations.
Aliases can improve readability, but they can also hide too much.
Consider:
using Thing = std::unique_ptr<Connection>;Then:
Thing connection;The name Thing tells the reader almost nothing.
Even:
using Connection = std::unique_ptr<NetworkConnection>;may be questionable because the name makes pointer ownership invisible.
Something like:
using ConnectionPtr = std::unique_ptr<NetworkConnection>;communicates more.
However, naming every smart-pointer alias with Ptr is not universally necessary. Sometimes the abstraction intentionally hides storage details.
The right choice depends on whether ownership semantics are important to users of the alias.
This:
using i = int;
using d = double;
using s = std::string;makes code worse.
Aliases should generally provide one or more of:
- semantic meaning;
- abstraction;
- readability;
- reuse of complicated types;
- support for generic programming;
- insulation from implementation details.
Saving three keystrokes is usually not enough.
Compare:
using CustomerDatabase =
std::unordered_map<CustomerId, CustomerRecord>;with:
using M = std::unordered_map<int, CustomerRecord>;The first improves the vocabulary of the program.
The second creates another puzzle for the reader.
Suppose you have:
using StringVector = std::vector<std::string>;This tells us exactly what the implementation already says.
Sometimes that's useful, but often a domain name is better:
using Usernames = std::vector<std::string>;or:
using SearchTerms = std::vector<std::string>;The distinction is important.
A mechanical alias says:
"This type is annoying to spell."
A semantic alias says:
"This type has a specific role in this program."
Semantic aliases are usually more valuable.
Aliases such as:
using UserId = int;
using Age = int;
using Price = double;can improve documentation.
But because they don't create distinct types, they may give a false sense of safety.
Consider:
void update(UserId id, Age age);This looks type-safe:
update(userId, age);but this compiles too:
update(age, userId);if both aliases resolve to int.
For critical domain concepts, wrapper types may be better.
A common case is:
using Id = std::uint64_t;This is often perfectly reasonable.
But don't create aliases that merely duplicate standard integer names:
using u32 = std::uint32_t;
using u64 = std::uint64_t;unless your project has a deliberate convention for them.
std::uint32_t is already widely understood.
An alias like:
using EntityId = std::uint64_t;adds domain information.
u64 mostly just adds another vocabulary the reader must learn.
There are exceptions—game engines and systems libraries often establish standardized short numeric aliases—but consistency across the codebase matters more than the particular convention.
Function-related aliases are one area where using can dramatically improve readability.
For a raw function pointer:
using CompareFn = bool (*)(const Item&, const Item&);Then:
CompareFn comparator = compare_items;You can also alias a function type itself:
using CompareFn = bool(const Item&, const Item&);and then create a pointer:
CompareFn* comparator = compare_items;For higher-level callbacks:
using Callback = std::function<void(Result)>;Then an API becomes:
void execute(Callback callback);instead of repeatedly writing:
void execute(std::function<void(Result)> callback);Whether std::function itself is the right abstraction is a separate performance/API-design question, but the aliasing mechanism is useful.
Consider:
using IntPtr = int*;What does this mean?
const IntPtr p = nullptr;A common mistake is to read it as:
const int* p;But it actually means:
int* const p;Why?
Because the alias IntPtr represents the complete type:
int*Applying const to the alias makes the pointer itself const.
Conceptually:
using IntPtr = int*;
const IntPtrmeans:
const (IntPtr)
const (int*)
which is:
int* constThis is one reason pointer aliases deserve careful naming and use.
If you want a pointer to const:
using ConstIntPtr = const int*;or simply write:
const int*when that is clearer.
Suppose:
using IntRef = int&;Then:
IntRef ref = x;is simply:
int& ref = x;In template contexts, aliases interact with reference collapsing.
For example:
using LRef = int&;
LRef&& x = value;collapses to:
int&because:
T& & -> T&
T& && -> T&
T&& & -> T&
T&& && -> T&&
This becomes especially important in forwarding-reference and template-metaprogramming code.
In C++20 generic programming, aliases often complement concepts.
For example:
template<typename T>
concept Container = requires(T c)
{
typename T::value_type;
typename T::iterator;
c.begin();
c.end();
};Here:
typename T::value_type;checks that the nested type exists.
A class can expose it with:
using value_type = T;Aliases therefore form part of the vocabulary through which generic types describe themselves.
Selective using-declarations are also important in advanced generic C++.
A classic example is:
using std::swap;
swap(a, b);Why not simply write:
std::swap(a, b);?
Because the first pattern allows argument-dependent lookup (ADL) to find a better swap associated with the argument types while still making std::swap available as a fallback.
Conceptually:
using std::swap;
swap(a, b);means:
"Consider
std::swap, but also allow normal unqualified lookup/ADL to discover an appropriate overload."
Similar patterns exist for customization mechanisms elsewhere in C++.
This is a good example of a using-declaration being part of a lookup strategy rather than just a convenience.
For ordinary aliases:
typedef int Integer;and:
using Integer = int;are effectively equivalent.
The differences are primarily in syntax and capability.
| Feature | typedef |
using |
|---|---|---|
| Ordinary type alias | Yes | Yes |
| Available before C++11 | Yes | No |
| Alias templates | No | Yes |
| Readability for complex declarations | Often worse | Usually better |
| Common in legacy code | Very common | Increasingly common |
| Recommended for new modern C++ | Usually no | Yes |
The practical default is therefore:
using Name = Type;Do not use preprocessor macros as substitutes for type aliases.
Bad:
#define UINT unsigned intBetter:
using UInt = unsigned int;The macro performs textual substitution before the C++ compiler properly processes the program.
An alias participates in the C++ type system.
Aliases respect scope, namespaces, templates, debugging information, and language semantics far better than macros.
Macros still have legitimate uses, but simple type naming generally isn't one of them.
An alias can exist at many scopes.
namespace network
{
using Port = std::uint16_t;
}Usage:
network::Port port = 443;Good for aliases that are meaningful throughout a module or library.
class Graph
{
public:
using NodeId = std::size_t;
};Usage:
Graph::NodeId id;Good when the type is specifically associated with the class.
void process()
{
using Iterator = std::vector<Record>::iterator;
// ...
}Good for complicated types only relevant locally.
A useful rule is:
Give an alias the narrowest scope that still reflects where its meaning belongs.
This:
using String = std::string;
using Vector = std::vector<int>;
using Index = std::size_t;at global scope may seem harmless, but a large project can accumulate hundreds of generic names.
Prefer namespaces:
namespace app
{
using UserId = std::uint64_t;
}or associate aliases with the owning abstraction:
class User
{
public:
using Id = std::uint64_t;
};Then:
User::Idis both descriptive and scoped.
A type alias does not create a wrapper and normally introduces no runtime overhead.
For example:
using UserId = std::uint64_t;has the same representation as:
std::uint64_tbecause it literally is the same type.
A wrapper:
struct UserId
{
std::uint64_t value;
};is a separate type.
Although simple wrappers are usually optimized extremely well, they can affect:
- name mangling;
- overload signatures;
- template instantiations;
- ABI;
- serialization assumptions;
- interoperability.
So choosing between alias and wrapper isn't purely stylistic.
Suppose:
using Password = std::string;Nothing prevents:
Password password = "secret";
password += "!";
password.clear();
password.append(...);All std::string operations remain available.
If you want to control valid operations, invariants, construction, or exposure, use a class:
class Password
{
public:
explicit Password(std::string value);
// Carefully chosen interface...
private:
std::string value_;
};An alias changes a name.
A class changes an abstraction.
Suppose:
using Handle = int;What is Handle?
A file descriptor? Database ID? Window handle? Index?
Compare:
using FileDescriptor = int;That is much better.
Likewise:
using Data = std::vector<std::byte>;may be too vague.
Perhaps:
using PacketPayload = std::vector<std::byte>;is more meaningful.
Good aliases compress syntax while preserving—or improving—meaning.
Bad aliases compress meaning itself.
It is possible to turn straightforward code into an alias maze:
using A = int;
using B = A;
using C = std::vector<B>;
using D = C::iterator;
using E = std::pair<A, D>;Now understanding E requires chasing several declarations.
Aliases should reduce cognitive load, not redistribute it.
A useful test is:
Does this alias allow a reader to understand the code without immediately looking up what the alias means?
If yes, it is probably helping.
If the reader must constantly jump to the definition, it may be hurting.
One of the best use cases for aliases is hiding types whose exact spelling is irrelevant.
For example:
using Cache =
std::unordered_map<
CacheKey,
std::unique_ptr<CachedObject>,
CacheKeyHash
>;Then implementation code can say:
Cache cache_;instead of repeating the entire type.
This is especially useful when the container configuration itself is not central to understanding the surrounding algorithm.
Alias templates can encode repeated template configurations.
For example:
template<typename T>
using PoolVector = std::vector<T, PoolAllocator<T>>;Now:
PoolVector<Entity> entities;
PoolVector<Component> components;expresses both the container concept and the allocation policy.
This is cleaner and less error-prone than repeatedly writing:
std::vector<Entity, PoolAllocator<Entity>>throughout the codebase.
Modern C++ frequently treats alias templates as lightweight compile-time functions from types to types.
For example:
template<typename T>
using RemoveCVRef = std::remove_cv_t<std::remove_reference_t<T>>;Then:
RemoveCVRef<const int&>produces:
intC++20 already provides:
std::remove_cvref_t<T>but the pattern is useful when building domain-specific type transformations.
Conceptually:
Type -> Alias Template -> Transformed Type
This is a central technique in template metaprogramming.
C++ has no universal naming convention for aliases.
Common styles include:
using ValueType = int;
using value_type = int;
using value_t = int;
using value_type_t = int;Different conventions communicate different things.
The standard library tends to use lowercase names:
value_type
size_type
difference_type
iterator
const_iteratorMetaprogramming aliases frequently use _t:
remove_reference_t<T>
decay_t<T>
invoke_result_t<F, Args...>Application-level aliases often follow the project's ordinary type naming convention:
using CustomerId = std::uint64_t;
using RequestHandler = std::function<void(Request)>;Consistency matters more than choosing one universal style.
Usually only when it improves clarity.
This:
using CustomerIdType = std::uint64_t;is often less elegant than:
using CustomerId = std::uint64_t;because the fact that CustomerId is a type is already clear from context and naming conventions.
But in template metaprogramming, names like:
ResultType
ElementTypemay be appropriate.
Avoid redundant suffixes unless they help disambiguate the API.
There is no universal answer.
This:
using WidgetPtr = std::unique_ptr<Widget>;makes ownership/storage visible.
That can be valuable.
But an abstraction may intentionally hide that detail:
using WidgetHandle = std::shared_ptr<Widget>;The key question is whether callers should reason about the pointer semantics.
If yes, expose them clearly.
If no, consider whether an alias is enough abstraction—or whether a proper class is needed.
Aliases can serve as lightweight documentation:
using Milliseconds = std::chrono::milliseconds;
using UserMap = std::unordered_map<UserId, User>;
using CompletionHandler = std::function<void(ErrorCode)>;Compare:
void wait(std::chrono::milliseconds duration);and:
void wait(Milliseconds duration);The first may actually be clearer if the standard type already communicates everything.
So aliases aren't automatically improvements.
Ask whether the new name adds information.
Avoid renaming standard types simply because their names are slightly long.
For example:
using String = std::string;usually provides little value.
Likewise:
using Size = std::size_t;may merely introduce another spelling.
But this:
using BufferSize = std::size_t;adds semantic information.
Similarly:
using Timeout = std::chrono::milliseconds;can make sense because it communicates the role of the duration.
Because aliases don't create new types, they cannot distinguish overloads.
This doesn't work:
using Width = int;
using Height = int;
void set(Width);
void set(Height);Both functions are:
void set(int);If the distinction matters to overload resolution, you need distinct types:
struct Width
{
int value;
};
struct Height
{
int value;
};
void set(Width);
void set(Height);Now the overloads are genuinely different.
An alias doesn't fundamentally disguise its underlying type from template deduction.
For example:
using Number = int;
template<typename T>
void inspect(T);
Number n = 42;
inspect(n);T is deduced as:
intnot as some distinct Number type.
Again, Number is merely another name for int.
This matters when designing generic APIs that you expect to behave differently based on semantic aliases—they won't.
The relationship can be demonstrated directly:
using UserId = unsigned long;
static_assert(
std::is_same_v<UserId, unsigned long>
);The assertion succeeds.
Likewise:
typedef unsigned long UserId;
static_assert(
std::is_same_v<UserId, unsigned long>
);also succeeds.
This is the precise type-system meaning of an alias.
Sometimes developers use aliases where a named value is actually what they need.
A type alias:
using Size = std::size_t;names a type.
A constant:
constexpr std::size_t max_size = 1024;names a value.
These occupy different conceptual roles.
Modern C++ provides several mechanisms for creating vocabulary:
using // types
constexpr // compile-time values
enum class // named alternatives
concept // constraints
namespace // grouping
class // abstractions / new typesChoosing the correct mechanism is more important than minimizing syntax.
Suppose:
using State = int;
constexpr State disconnected = 0;
constexpr State connecting = 1;
constexpr State connected = 2;A better design may be:
enum class State
{
disconnected,
connecting,
connected
};Now the valid states are explicit and type-safe:
State state = State::connected;An alias is not always the right abstraction merely because the underlying representation happens to be an integer.
Combining the previous concepts:
enum class State
{
disconnected,
connecting,
connected
};
void handle(State state)
{
using enum State;
switch (state)
{
case disconnected:
break;
case connecting:
break;
case connected:
break;
}
}This is a good example of a scoped using feature.
The enumerators are imported only where the context makes their meaning obvious.
This illustrates a broader C++ principle:
Import names narrowly rather than globally.
For headers, a conservative approach is usually best.
Prefer:
#pragma once
#include <string>
#include <vector>
namespace app
{
using UserId = std::uint64_t;
class UserRepository
{
public:
using Collection = std::vector<User>;
// ...
};
}Avoid:
using namespace std;at global or namespace scope in a public header.
Be cautious with generic global aliases such as:
using String = std::string;
using Vector = std::vector<int>;because every public name becomes another piece of API surface.
Inside .cpp files you have more freedom.
For example:
namespace
{
using UserMap = std::unordered_map<UserId, User>;
}An unnamed namespace restricts the name to the translation unit.
Or inside a function:
void process()
{
using Clock = std::chrono::steady_clock;
const auto start = Clock::now();
// ...
}This is an excellent use of a local alias: the type is long, the alias is meaningful, and its scope is narrow.
A namespace alias can also dramatically improve readability without polluting the scope:
namespace fs = std::filesystem;
namespace chrono = std::chrono;Then:
fs::path config;
chrono::steady_clock::time_point start;This is often a better compromise than:
using namespace std::filesystem;
using namespace std::chrono;because the origin of each name remains visible.
Consider:
template<typename T>
using Shared = std::shared_ptr<T>;Then:
Shared<Widget> widget;Is this better than:
std::shared_ptr<Widget> widget;Often, no.
The original standard-library name clearly communicates shared ownership.
The alias saves only a few characters while hiding important semantics.
Compare that with:
template<typename T>
using PoolAllocatedVector =
std::vector<T, PoolAllocator<T>>;This alias compresses substantial implementation detail and communicates an important policy.
That is much more valuable.
Before introducing an alias, ask:
- Does the alias add semantic meaning?
- Does it significantly simplify a complicated type?
- Will the name be reused enough to justify another abstraction?
- Does hiding the underlying type make the code easier or harder to understand?
- Do I actually need a distinct type instead?
- What is the narrowest sensible scope for this alias?
If you cannot answer any of the first three positively, you may not need the alias.
using UserId = std::uint64_t;
using SessionToken = std::string;Good when the goal is readability and interchangeability is acceptable.
using AdjacencyList =
std::unordered_map<NodeId, std::vector<NodeId>>;Excellent use case.
using CompletionHandler =
std::function<void(Result)>;Useful when the callback concept appears repeatedly.
class Container
{
public:
using value_type = T;
using size_type = std::size_t;
};Standard and useful.
template<typename T>
using Decayed = std::decay_t<T>;Fundamental to modern generic programming.
namespace fs = std::filesystem;Excellent when namespace names are long.
using String = std::string;Usually unnecessary.
using Widget = std::shared_ptr<RealWidget>;Potentially misleading.
using v = std::vector<int>;Poor readability.
using Width = int;
using Height = int;They don't.
using namespace std;Especially problematic in headers.
Type aliases provide several important advantages.
They can turn:
std::unordered_map<
std::string,
std::vector<std::unique_ptr<Node>>
>into:
NodeTableusing RequestId = std::uint64_t;communicates more than:
std::uint64_tA repeated complicated type can be changed in one place.
Alias templates make type transformations concise.
An alias is a compile-time naming mechanism.
Nested aliases such as:
Container::value_typelet types describe associated concepts.
Aliases also have costs.
Handle h;may be less informative than:
std::unique_ptr<Resource> h;using Meters = double;
using Seconds = double;remain interchangeable.
Readers may have to constantly navigate to definitions.
A public alias may accidentally make implementation details part of an API.
For example:
const IntPtrdoes not mean what some readers initially expect.
typedef is:
- supported by old C++ versions;
- universally recognized by experienced C and C++ programmers;
- common in C interoperability;
- perfectly adequate for simple aliases.
It:
- has less intuitive syntax for complex declarators;
- cannot directly define alias templates;
- doesn't follow the convenient
Name = Typereading order; - is less idiomatic in new C++ code.
Therefore, it is mostly a compatibility and legacy-style choice today.
using:
- has clearer alias syntax;
- supports alias templates;
- works well with modern generic programming;
- handles complicated declarations more readably;
- is the conventional choice for modern C++.
The main "disadvantage" is mostly conceptual:
using is heavily overloaded as a keyword.
These:
using T = int;
using std::swap;
using namespace std;
using Base::Base;
using enum Color;do completely different things.
Beginners therefore need to learn that using is a family of related name-introduction mechanisms rather than one single feature.
For modern C++, a strong default policy is:
Write:
using Index = std::size_t;rather than:
typedef std::size_t Index;unless compatibility or project conventions say otherwise.
Prefer:
using CustomerLookup =
std::unordered_map<CustomerId, Customer>;over meaningless abbreviations.
If two values must not be interchangeable, create distinct classes or another type-safe abstraction.
Prefer class, namespace, translation-unit, or local scope over global scope.
Especially:
using namespace std;using std::swap;
using std::string;instead of importing an entire namespace.
namespace fs = std::filesystem;template<typename T>
using Vec = std::vector<T, MyAllocator<T>>;std::unique_ptr<Widget>may be clearer than an alias that conceals ownership.
Consider a networking library:
namespace net
{
using ConnectionId = std::uint64_t;
using ByteBuffer = std::vector<std::byte>;
using ReceiveHandler =
std::function<void(ConnectionId, ByteBuffer)>;
template<typename T>
using Shared = std::shared_ptr<T>;
class Connection
{
public:
using Id = ConnectionId;
explicit Connection(Id id);
Id id() const;
private:
Id id_;
};
}There are several different design choices here.
This alias:
using ConnectionId = std::uint64_t;adds semantic meaning but not type safety.
This alias:
using ByteBuffer = std::vector<std::byte>;both shortens a common type and describes its role.
This:
using ReceiveHandler =
std::function<void(ConnectionId, ByteBuffer)>;dramatically simplifies a callback signature.
But this:
template<typename T>
using Shared = std::shared_ptr<T>;is more questionable because it hides the important fact that ownership is shared.
It might be better to leave:
std::shared_ptr<T>explicit.
Good alias design is therefore not simply "use aliases whenever a type is long."
It is about deciding which details readers should see.
If accidentally mixing identifiers would be dangerous, instead of:
using ConnectionId = std::uint64_t;consider:
class ConnectionId
{
public:
explicit ConnectionId(std::uint64_t value)
: value_(value)
{
}
std::uint64_t value() const
{
return value_;
}
private:
std::uint64_t value_;
};Now:
ConnectionId id{42};is a distinct type.
This makes invalid combinations harder to express.
You can still use aliases elsewhere:
using ByteBuffer = std::vector<std::byte>;
using ReceiveHandler =
std::function<void(ConnectionId, ByteBuffer)>;Aliases and strong types are complementary tools.
It helps to categorize using into three broad families.
using Name = Type;Example:
using UserId = std::uint64_t;using std::swap;
using Base::foo;
using enum Color;These modify name lookup or accessibility.
using namespace std;This is the broadest form and therefore the one requiring the most caution.
Namespace aliases are related but syntactically different:
namespace fs = std::filesystem;This doesn't import names—it gives a namespace another name.
When deciding what to use, ask what you're actually trying to accomplish.
"I want another name for a type."
Use:
using Name = Type;"I need compatibility with pre-C++11 code."
Use:
typedef Type Name;"I want a parameterized family of aliases."
Use an alias template:
template<typename T>
using Name = SomeType<T>;"I want one namespace member available without qualification."
Use:
using ns::name;"I want everything from a namespace available without qualification."
Use:
using namespace ns;but keep the scope narrow and avoid it in public headers.
"I want a shorter name for a namespace."
Use:
namespace short_name = very::long::namespace_name;"I want base-class overloads visible."
Use:
using Base::function;"I want base constructors available in the derived class."
Use:
using Base::Base;"I want enum members locally available without qualification."
In C++20:
using enum EnumType;"I want two semantically different values to be rejected when mixed."
Don't use an alias. Create distinct types.
If you remember only a handful of rules, remember these.
They don't:
using A = int;
using B = int;
static_assert(std::is_same_v<A, B>);It affects code outside the header's implementation and can create collisions.
using v = std::vector<int>;usually reduces readability.
Be cautious about turning:
std::unique_ptr<Resource>into a vague alias.
using Celsius = double;
using Fahrenheit = double;does not prevent accidental mixing.
using P = int*;
const P p = nullptr;means:
int* const p = nullptr;not:
const int* p = nullptr;Keep names close to where their meaning belongs.
For ordinary type aliases in modern C++, prefer:
using Name = Type;rather than:
typedef Type Name;Use typedef primarily when legacy compatibility or established project conventions justify it.
Use alias templates when you need parameterized type expressions:
template<typename T>
using Container = std::vector<T>;Use namespace aliases to shorten long namespaces without polluting name lookup:
namespace fs = std::filesystem;Use selective using-declarations when importing individual names improves local readability:
using std::swap;Be much more cautious with:
using namespace ...;especially in headers.
And most importantly, distinguish renaming a type from creating a type.
using UserId = std::uint64_t;means:
"
UserIdis another name forstd::uint64_t."
Whereas:
struct UserId
{
std::uint64_t value;
};means:
"
UserIdis a new type whose representation happens to contain astd::uint64_t."
That distinction is the foundation for using aliases correctly.
// Modern type alias
using Id = std::uint64_t;
// Old-style type alias
typedef std::uint64_t Id;
// Alias template
template<typename T>
using Vec = std::vector<T>;
// Nested alias
class C
{
public:
using value_type = int;
};
// Selective namespace import
using std::swap;
// Namespace-wide import — use cautiously
using namespace std;
// Namespace alias
namespace fs = std::filesystem;
// Expose base overloads
using Base::foo;
// Inherit constructors
using Base::Base;
// C++20: import enum members
using enum Color;The broad rule is simple:
Use
usingwhen a type or name deserves a clearer name; use a real class, struct, or enum when the program needs a genuinely different type.
And for modern C++, when the choice is specifically between typedef and a using type alias, using should normally be your default.
