C++ For C# Developers: Part 30 – Type Aliases
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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.