Skip to content

Instantly share code, notes, and snippets.

@MangaD
Created August 25, 2026 15:40
Show Gist options
  • Select an option

  • Save MangaD/88b798ddfd606c46dc1fb5d0b498774e to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/88b798ddfd606c46dc1fb5d0b498774e to your computer and use it in GitHub Desktop.
C++ `using`, `typedef`, and Type Aliases: A Practical Guide

C++ using, typedef, and Type Aliases: A Practical Guide

CC0

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.


1. The basic problem: naming complicated types

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.


2. typedef: the traditional C++ mechanism

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;

typedef does not create a new type

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 signature

The compiler effectively sees two declarations of:

void process(int);

Likewise:

typedef int UserId;
typedef int ProductId;

UserId user = 42;
ProductId product = user;   // perfectly valid

There is no additional type safety.

This applies equally to using aliases.


3. using: the modern type-alias syntax

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;

4. Why modern C++ generally prefers using

For new code, using is normally preferred.

There are several reasons.

4.1 It reads from left to right

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.


5. Function pointers show the readability difference

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.


6. Arrays are another good example

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]

7. The major advantage: alias templates

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;   // invalid

Historically, 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++.


8. Alias templates can transform types

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>::type

For 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."


9. typedef inside classes and templates

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.


10. When should you still use typedef?

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.


11. C-style typedef struct

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 {
    ...
};

12. Anonymous typedef struct patterns

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.


13. Aliases are not strong types

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.


14. When you actually need a new type

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;   // error

The 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.


15. Aliases vs wrapper types

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.


16. using has several completely different meanings

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++20

These features share a keyword but serve different purposes.

Let's examine them individually.


17. Namespace using-declarations

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.


18. Using-directives: using namespace

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.


19. Why using namespace std; is often discouraged

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.


20. Never put broad using-directives in public headers

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_t

or carefully scoped alternatives.


21. Using-directives inside functions are much less problematic

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.


22. A useful guideline for namespaces

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.


23. Namespace aliases

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::Connection

You get brevity without throwing every name into the current scope.


24. Type aliases inside classes

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_type

The standard containers follow this pattern.

For example:

std::vector<int>::value_type
std::vector<int>::iterator
std::vector<int>::size_type

Aliases therefore aren't merely syntactic convenience—they can form part of a type's public interface.


25. Dependent names and typename

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_type

is 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.


26. using and type traits

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.


27. using with decltype

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.


28. Aliasing iterators

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.


29. using vs auto

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 auto when explicitly spelling the variable's type adds little value.


30. using in inheritance

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.


31. Changing accessibility with a using-declaration

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.


32. Inheriting constructors

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.


33. When inherited constructors are useful

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.


34. using enum in C++20

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.


35. Alias templates vs template specialization

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.


36. Aliases can hide implementation details

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.


37. Public aliases can become part of your API

Suppose a library exposes:

class Widget
{
public:
    using Id = std::uint64_t;
};

Users may begin writing:

Widget::Id

throughout 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.


38. Avoid aliases that obscure important information

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.


39. Don't alias simple types merely to shorten them

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.


40. Prefer semantic aliases over mechanical aliases

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.


41. Be cautious with aliases for fundamental types

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.


42. Fixed-width integer aliases

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.


43. Function type aliases

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.


44. Pointer aliases have a famous const trap

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 IntPtr

means:

const (IntPtr)
const (int*)

which is:

int* const

This 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.


45. Reference aliases have special behavior

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.


46. using and concepts

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.


47. using in customization-point patterns

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.


48. typedef vs using: direct comparison

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;

49. using vs macros

Do not use preprocessor macros as substitutes for type aliases.

Bad:

#define UINT unsigned int

Better:

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.


50. Scope your aliases carefully

An alias can exist at many scopes.

Namespace scope

namespace network
{
    using Port = std::uint16_t;
}

Usage:

network::Port port = 443;

Good for aliases that are meaningful throughout a module or library.

Class scope

class Graph
{
public:
    using NodeId = std::size_t;
};

Usage:

Graph::NodeId id;

Good when the type is specifically associated with the class.

Function scope

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.


51. Don't create global aliases casually

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::Id

is both descriptive and scoped.


52. Aliases and ABI considerations

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_t

because 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.


53. Aliases don't give you encapsulation

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.


54. Don't alias away information readers need

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.


55. Don't over-alias

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.


56. Aliasing implementation-heavy template types

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.


57. Template aliases for policies and allocators

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.


58. Alias templates as metafunctions

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:

int

C++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.


59. Alias naming conventions

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_iterator

Metaprogramming 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.


60. Should aliases end in Type?

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
ElementType

may be appropriate.

Avoid redundant suffixes unless they help disambiguate the API.


61. Should pointer aliases end in Ptr?

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.


62. Type aliases and documentation

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.


63. Standard library types are often already good aliases

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.


64. A subtle point: aliases and overloads

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.


65. Aliases and template deduction

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:

int

not 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.


66. Aliases and std::is_same

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.


67. Type alias vs constexpr

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 types

Choosing the correct mechanism is more important than minimizing syntax.


68. enum class can be better than integer aliases

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.


69. using enum and local readability

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.


70. Header-file best practices

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.


71. Source-file best practices

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.


72. Namespace aliases in source files

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.


73. Alias templates and readability trade-offs

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.


74. A useful alias-design test

Before introducing an alias, ask:

  1. Does the alias add semantic meaning?
  2. Does it significantly simplify a complicated type?
  3. Will the name be reused enough to justify another abstraction?
  4. Does hiding the underlying type make the code easier or harder to understand?
  5. Do I actually need a distinct type instead?
  6. 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.


75. Good use cases

Domain vocabulary

using UserId = std::uint64_t;
using SessionToken = std::string;

Good when the goal is readability and interchangeability is acceptable.

Complex containers

using AdjacencyList =
    std::unordered_map<NodeId, std::vector<NodeId>>;

Excellent use case.

Function signatures

using CompletionHandler =
    std::function<void(Result)>;

Useful when the callback concept appears repeatedly.

Associated types

class Container
{
public:
    using value_type = T;
    using size_type = std::size_t;
};

Standard and useful.

Template transformations

template<typename T>
using Decayed = std::decay_t<T>;

Fundamental to modern generic programming.

Namespace abbreviation

namespace fs = std::filesystem;

Excellent when namespace names are long.


76. Questionable use cases

Renaming common standard types

using String = std::string;

Usually unnecessary.

Hiding ownership

using Widget = std::shared_ptr<RealWidget>;

Potentially misleading.

Tiny abbreviations

using v = std::vector<int>;

Poor readability.

Pretending aliases provide type safety

using Width = int;
using Height = int;

They don't.

Global using-directives

using namespace std;

Especially problematic in headers.


77. Pros of type aliases

Type aliases provide several important advantages.

Readability

They can turn:

std::unordered_map<
    std::string,
    std::vector<std::unique_ptr<Node>>
>

into:

NodeTable

Semantic meaning

using RequestId = std::uint64_t;

communicates more than:

std::uint64_t

Maintainability

A repeated complicated type can be changed in one place.

Generic programming

Alias templates make type transformations concise.

No runtime cost

An alias is a compile-time naming mechanism.

API vocabulary

Nested aliases such as:

Container::value_type

let types describe associated concepts.


78. Cons of type aliases

Aliases also have costs.

They can hide useful implementation information

Handle h;

may be less informative than:

std::unique_ptr<Resource> h;

They do not provide type safety

using Meters = double;
using Seconds = double;

remain interchangeable.

Too many aliases increase cognitive load

Readers may have to constantly navigate to definitions.

They can expose implementation choices

A public alias may accidentally make implementation details part of an API.

Pointer aliases can be confusing

For example:

const IntPtr

does not mean what some readers initially expect.


79. Pros and cons of typedef

Advantages

typedef is:

  • supported by old C++ versions;
  • universally recognized by experienced C and C++ programmers;
  • common in C interoperability;
  • perfectly adequate for simple aliases.

Disadvantages

It:

  • has less intuitive syntax for complex declarators;
  • cannot directly define alias templates;
  • doesn't follow the convenient Name = Type reading order;
  • is less idiomatic in new C++ code.

Therefore, it is mostly a compatibility and legacy-style choice today.


80. Pros and cons of using

Advantages

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++.

Disadvantages

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.


81. Practical style guide

For modern C++, a strong default policy is:

Prefer using over typedef

Write:

using Index = std::size_t;

rather than:

typedef std::size_t Index;

unless compatibility or project conventions say otherwise.

Use aliases to add meaning

Prefer:

using CustomerLookup =
    std::unordered_map<CustomerId, Customer>;

over meaningless abbreviations.

Don't confuse aliases with strong types

If two values must not be interchangeable, create distinct classes or another type-safe abstraction.

Keep aliases narrowly scoped

Prefer class, namespace, translation-unit, or local scope over global scope.

Avoid using namespace ... in headers

Especially:

using namespace std;

Prefer selective imports when practical

using std::swap;
using std::string;

instead of importing an entire namespace.

Use namespace aliases for long namespaces

namespace fs = std::filesystem;

Use alias templates for reusable generic type expressions

template<typename T>
using Vec = std::vector<T, MyAllocator<T>>;

Don't hide important semantics just to shorten code

std::unique_ptr<Widget>

may be clearer than an alias that conceals ownership.


82. A realistic example

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.


83. Improving the networking example with a strong ID

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.


84. A mental model for all the forms of using

It helps to categorize using into three broad families.

Family 1: Give a type another name

using Name = Type;

Example:

using UserId = std::uint64_t;

Family 2: Bring existing names into scope

using std::swap;
using Base::foo;
using enum Color;

These modify name lookup or accessibility.

Family 3: Import names from an entire namespace

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.


85. Quick decision tree

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.


86. The most important mistakes to avoid

If you remember only a handful of rules, remember these.

Mistake 1: Thinking aliases create new types

They don't:

using A = int;
using B = int;

static_assert(std::is_same_v<A, B>);

Mistake 2: Using using namespace std; in headers

It affects code outside the header's implementation and can create collisions.

Mistake 3: Creating aliases purely to save keystrokes

using v = std::vector<int>;

usually reduces readability.

Mistake 4: Hiding important ownership semantics

Be cautious about turning:

std::unique_ptr<Resource>

into a vague alias.

Mistake 5: Assuming semantic aliases give compiler protection

using Celsius = double;
using Fahrenheit = double;

does not prevent accidental mixing.

Mistake 6: Forgetting pointer-alias const behavior

using P = int*;
const P p = nullptr;

means:

int* const p = nullptr;

not:

const int* p = nullptr;

Mistake 7: Creating aliases at unnecessarily broad scope

Keep names close to where their meaning belongs.


87. Modern C++ recommendation

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:

"UserId is another name for std::uint64_t."

Whereas:

struct UserId
{
    std::uint64_t value;
};

means:

"UserId is a new type whose representation happens to contain a std::uint64_t."

That distinction is the foundation for using aliases correctly.


Cheat Sheet

// 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 using when 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment