C# has support for type aliases in the form of using ScoreMap = System.Collections.Generic.Dictionary<string, int>; directives. This allows us to use ScoreMap instead of the verbose System.Collections.Generic.Dictionary<string, int> or even Dictionary<string, int>. C++ also has type aliases, but they go way beyond what C# supports. Today we’ll dig into everything C++ offers us to make our code more concise and readable.

Table of Contents

Typedef

There are two main ways of creating type aliases in C++. The first, typedef, is inherited from C. It’s still common to see in C++ codebases, but later in the article we’ll learn another approach that’s essentially a complete replacement for typedef.

To create an alias this way, we write typedef SourceType AliasName;:

// Create an alias of "unsigned int" called "uint32"
typedef unsigned int uint32;
 
// Use the "uint32" alias in place of "unsigned int"
constexpr uint32 ZERO = 0;

C# using X = Y; aliases can only appear in two places. If they’re placed at the start of a .cs file, they’re in scope in that file. If they’re placed in a namespace block, they’re in scope in that block. This means they’re never usable in other namespace blocks or other files.

C++ type aliases work differently. They can be added to other kinds of scopes and used across files:

////////////
// Math.h //
////////////
 
namespace Integers
{
    // Add a "uint32" alias for "unsigned int" in the Integers namespace
    typedef unsigned int uint32;
}
 
// Use "uint32" like any other member of the Integers namespace
constexpr Integers::uint32 ZERO = 0;
 
////////////
// Game.h //
////////////
 
// Include header file to get access to the Integers namespace and ZERO
#include "Math.h"
 
constexpr Integers::uint32 MAX_HEALTH = 100;
 
//////////////
// Game.cpp //
//////////////
 
// Include header file to get access to Integers, ZERO, and MAX_HEALTH
#include "Game.h"
 
DebugLog(ZERO); // 0
DebugLog(MAX_HEALTH); // 100
 
// The type alias is usable here, too
for (Integers::uint32 i = 0; i < 3; ++i)
{
    DebugLog(i); // 0, 1, 2
}

This example added a type alias to a namespace, but we can add them to almost any kind of scope. For example, we might add an alias to just a single function:

void Foo()
{
    // Type alias scoped to just one function
    typedef unsigned int uint32;
 
    for (uint32 i = 0; i < 3; ++i)
    {
        DebugLog(i); // 0, 1, 2
    }
}

Or even a block within a function:

void Foo()
{
    {
        // Type alias scoped to just one function
        typedef unsigned int uint32;
 
        for (uint32 i = 0; i < 3; ++i)
        {
            DebugLog(i); // 0, 1, 2
        }
    }
 
    // Compiler error: type alias is only visible in the above block
    uint32 x = 0;
}

It’s also common to see type aliases added as members of classes:

struct Player
{
    // Player::HealthType is now an alias for "unsigned int"
    typedef unsigned int HealthType;
 
    // We can use it here without the namespace qualifier
    HealthType Health = 0;
};
 
// We can use it outside of the class by adding the namespace qualifier
void ApplyDamage(Player& player, Player::HealthType amount)
{
    player.Health -= amount;
}

This approach is particularly useful when we think we might change the type of Health later on. We can simply update the typedef line to typedef unsigned long long int HealthType; and the types of Health and amount will both be changed. In a larger project, this might save us from having to update hundreds or thousands of types.

It’s important to remember that, like C# type aliases, these typedef statements don’t create new types. When we use uint32, it’s exactly the same as if we used unsigned int. The alias we create is exactly that: another way to refer to the same type.

In addition to the simple typedef statements we’ve used so far, we can also write a couple kinds of more complex statements. First, we can make more than one alias in a single statement. This works similarly to declaring multiple variables at once:

// Create four type aliases:
// 1) "Int" for "int"
// 2) "IntPointer" for "int*" a.k.a. "a pointer to an int"
// 3) "FunctionPointer" for "int (&)(int, int)"
//    a.k.a. "reference to function that takes two ints and returns an int"
// 4) "IntArray" for "int[2]" a.k.a "an array of two ints"
typedef int Int, *IntPointer, (&FunctionPointer)(int, int), IntArray[2];
 
Int one = 1;
DebugLog(one); // 1
 
IntPointer p = &one;
DebugLog(*p); // 1
 
int Add(int a, int b)
{
    return a + b;
}
 
FunctionPointer add = Add;
DebugLog(add(2, 3)); // 5
 
IntArray array = { 123, 456 };
DebugLog(array[0], array[1]); // 123, 456

Second, typedef is sometimes used to create struct types. This is a hold-over from C that’s not necessary in C++, but some legacy code may still do this and it’s supported for backwards-compatibility reasons. This is valid C and C++:

// C code
 
// Create two type aliases:
// 1) "Player" for "struct { int Health; int Speed; }"
// 2) "PlayerPointer" for "Player*" a.k.a. "pointer to Player"
typedef struct
{
    int Health;
    int Speed;
} Player, *PlayerPointer;
 
Player p;
p.Health = 100;
p.Speed = 10;
DebugLog(p.Health, p.Speed); // 100, 10
 
PlayerPointer pPlayer = &p;
DebugLog(pPlayer->Health, pPlayer->Speed); // 100, 10

Without the typedef, C code would be forced to prefix Player with struct like this:

// C code
 
struct Player
{
    int Health;
    int Speed;
};
 
struct Player p; // C requires "struct" prefix
p.Health = 100;
p.Speed = 10;
DebugLog(p.Health, p.Speed); // 100, 10

Again, neither the struct prefix nor the typedef workaround are necessary in C++. It’s just important to know why typedef is used like this since it’s still commonly seen in C++ codebases.

Using Aliases

Since C++11, typedef is no longer the preferred way of creating type aliases. The new way looks a lot more like C#’s using X = Y;. Note that the order of the alias and the type has reversed compared to typedef:

// Create an alias of "unsigned int" called "uint32"
using uint32 = unsigned int;
 
// Use the "uint32" alias in place of "unsigned int"
constexpr uint32 ZERO = 0;

We’re simply listing the type name on the right side. This is particularly more readable than typedef for some of the more complex types we’ve seen since the alias name isn’t mixed in with the type being aliased:

// Alias for a pointer to an int
using IntPointer = int*;
 
// Alias for a function that takes two ints and returns an int
using FunctionPointer = int (*)(int, int);
 
// Alias for an array of two int elements
using IntArray = int[2];

This syntax is exactly equivalent to a typedef. Both create an alias to the original type, not a new type. Both can appear in global, namespace, function, or function block scope. Most programmers find this form more readable since it mimics the form of variable assignment and separates the alias from the original type with an =.

Multiple aliases can’t be created in one statement with using. This is probably for the best as that typedef syntax is relatively difficult to read and seldom used:

// Compiler error: can only create one alias at a time
using uint32 = unsigned int, f32 = float;

Besides these syntactic advantages, using has an functional improvement as well: we can create alias templates. Consider this code that doesn’t make use of alias templates:

// Namespace with a class template
namespace Math
{
    template <typename TComponent>
    struct Vector2
    {
        TComponent X;
        TComponent Y;
    };
}
 
// Another namespace with a class template
namespace Collections
{
    template <typename TKey, typename TValue>
    struct HashMap
    {
        // ... implementation
    };
}
 
// Type names start getting long
Collections::HashMap<int32_t, Math::Vector2<float>> playerLocations;
Collections::HashMap<int32_t, Math::Vector2<int32_t>> playerScores;
 
// Shortening requires an alias for each template instantiation
using vec2f = Math::Vector2<float>;
using vec2i32 = Math::Vector2<int32_t>;
Collections::HashMap<int32_t, vec2f> playerLocations;
Collections::HashMap<int32_t, vec2i32> playerScores;

Now consider if we do have access to alias templates:

// Template of a type alias
// Takes two type parameters: TKey and TValue
template <typename TKey, typename TValue>
using map = Collections::HashMap<TKey, TValue>; // Can use parameters in alias
 
template <typename TComponent>
using vec2 = Math::Vector2<TComponent>;
 
// Pass arguments to the aliases just like any other template
map<int32_t, vec2<float>> playerLocations;
map<int32_t, vec2<int32_t>> playerLocations;
 
// We can still create non-template type aliases to get more specific
using vec2f = vec2<float>;
using vec2i32 = vec2<int32_t>;
map<int32_t, vec2f> playerLocations;
map<int32_t, vec2i32> playerScores;
 
// And even more specific...
using LocationMap = map<int32_t, vec2f>;
using ScoreMap = map<int32_t, vec2i32>;
LocationMap playerLocations;
ScoreMap playerScores;

Alias templates give us a tool to keep some of the type parameters without being forced to alias a concrete type. These templates can be reused, as we did with both map and vec, rather than duplicating aliases. This becomes more and more useful as types become more complicated and generic.

These alias templates inherit all the functionality of other kinds of templates, such as for functions and classes. For example, we can use non-type parameters:

// Class template for a fixed-length array
template <typename TElement, int N>
struct FixedList
{
    int Length = N;
    TElement Elements[N];
 
    TElement& operator[](int index)
    {
        return Elements[index];
    }
};
 
// Alias template taking a non-type parameter: int N
template <int N>
using ByteArray = FixedList<unsigned char, N>;
 
// Pass a non-type argument to the alias template: <3>
ByteArray<3> bytes;
bytes[0] = 10;
bytes[1] = 20;
bytes[2] = 30;
DebugLog(bytes.Length, bytes[0], bytes[1], bytes[2]); // 3, 10, 20, 30
Permissions

Lastly, and quickly, there’s one final use for type aliases. As we’ve seen before, class permissions like private can be used to prevent code outside of the class from using certain members. This applies to types that the class creates:

class Outer
{
    // Member type that is private, the default for "class"
    struct Inner
    {
        int Val = 123;
    };
};
 
// Compiler error: Inner is private
Outer::Inner inner;

Type aliases can be used to avoid this restriction. This works because the compiler only checks the permissions of the type being used. If that type is an alias for another type, the aliased type’s permission is irrelevant and ignored:

class Outer
{
    // Member type is still private
    struct Inner
    {
        int Val = 123;
    };
 
public:
 
    // Type alias is public
    using InnerAlias = Inner;
};
 
// OK: uses permission level of InnerAlias, not Inner
Outer::InnerAlias inner;

Usually we’ll just specify the desired permission level to begin with. In cases such as using third-party libraries, we don’t have the ability to change that original permission level. This workaround can be used to get the access we need.

Conclusion

Type aliases in C++ go way beyond their C# counterparts. They’re not limited to a single source code file or namespace block. Instead, we can and commonly do declare them in header files as globals, in namespaces, and as class members. We declare terse names in functions or even blocks in functions to avoid a lot of type verbosity, especially when using generic code such as a HashMap<TKey, TValue>. These aliases can be created once and shared across the whole project, not just within one file.

Alias templates go even further by allowing us to create aliases that don’t resolve to a concrete type. These can prevent a lot of code duplication and give names to in-between steps such as map that lives between the very generic HashMap<TKey, TValue> and the very concrete LocationMap. They inherit the powers of other C++ templates with capabilities including non-type parameters and variable numbers of parameters.