Type Aliases

Learn how to use type aliases and utilities to simplify working with complex types.

Ryan McCombe
Updated

In this lesson, we'll introduce some useful utilities that makes working with complex types a little easier. These utilities will become increasingly important through the rest of the chapter, when we fully explore templates.

So far, we may have noticed our types are getting more and more verbose. This trend is going to continue, particularly once we start using templates later in this chapter.

We'll cover template classes in more detail later in this chapter. For now, let's introduce a basic example of one from the standard library - the std::pair.

The std::pair data type is available by including <utility>. It lets us store two objects in a single container. We specify the two types we will be storing within the < and > syntax: For example, to create a std::pair that stores an int and a float, we would use this syntax:

#include <utility>

std::pair<int, float> MyPair;

We can access each value through the first and second members, respectively:

#include <utility>
#include <iostream>

int main() {
  std::pair<int, float> MyPair{42, 9.8f};

  std::cout << "First " << MyPair.first
    << "\nSecond " << MyPair.second;
}
First: 42
Second: 9.8

We cover std::pair in full detail later in the course, but for this lesson, we'll just use it as the basis for creating type aliases.

Using std::pair

Master the use of std::pair with this comprehensive guide, encompassing everything from simple pair manipulation to template-based applications

Why Type Aliases are Useful

Let's see a more complicated example of a std::pair type, which will show why type aliases can be useful.

We might want to store a Player object, alongside a Guild object that the player is part of. We could store that as the following type:

std::pair<const Player&, const Guild&>

Below, we show this type in action:

#include <utility>
#include <iostream>

struct Player {
  std::string Name;
};

struct Guild {
  std::string Name;
};

void LogDetails(std::pair<
  const Player&, const Guild&>& Member) {
  std::cout << "Player: " << Member.first.Name
            << ", Guild: " << Member.second.Name;
}

int main() {
  Player Anna{"Anna"};
  Guild Fellowship{"The Fellowship"};

  std::pair<const Player&, const Guild&> Member{
	  Anna, Fellowship
  };

  LogDetails(Member);
}
Player: Anna, Guild: The Fellowship

Using a type as complex as this can make our code difficult to follow and understand.

It's quite difficult to quickly figure out what our code is doing when so much of the signal is drowned out by the noise of such a complex and verbose type.

Additionally, the type doesn't have as much semantic meaning as it could. If a type is supposed to represent a member of a guild, we'd prefer the type to have a name like GuildMember. Fortunately, we have a way to give our types more friendly names.

Creating an Alias with using

We can create an alias for a type using a using statement, as shown below:

using GuildMember =
  std::pair<const Player&, const Guild&>;

This has at least two benefits

  • Our type is less cumbersome, meaning code that uses it is easier to write and follow
  • The alias describes what the type is supposed to represent

We can use the type alias in place of the type, in any location where a type would be expected. Compared to the previous example, the following program has simplified our function signature and our variable creation:

#include <utility>
#include <iostream>

struct Player {
  std::string Name;
};

struct Guild {
  std::string Name;
};

using GuildMember =
  std::pair<const Player&, const Guild&>;

void LogDetails(GuildMember& Member) {
  std::cout << "Player: " << Member.first.Name
            << ", Guild: " << Member.second.Name;
}

int main() {
  Player Anna{"Anna"};
  Guild Fellowship{"The Fellowship"};

  GuildMember Member{Anna, Fellowship};

  LogDetails(Member);
}
Player: Anna, Guild: The Fellowship

Through the alias, we can freely add qualifiers to the underlying type. This can include things like * or & to make it a pointer or a reference type, and const to make it a constant:

using Integer = int;

int main() {
  // Value
  Integer Value;

  // Reference
  Integer& Ref{Value};

  // Const Reference
  const Integer& ConstRef{Value};

  // Pointer
  Integer* Ptr;

  // Const pointer to const
  const Integer* const CPtr{&Value};
}

The alias also does not prevent us from using the original type name. In the previous example, we aliased int to Integer, but we could still use int where preferred, such as in the main function's return type.

Alias Scope

Aliases are scoped in the same way as any of our other declarations.

This means aliases can have global scope, by being defined outside of any block:

using Integer = int;

int main() {
  Integer SomeValue;
}

Alternatively, aliases can be defined within a block, such as one created by a function, namespace (including an anonymous namespace) or an if statement.

When this is done, the alias will follow normal scoping rules. Typically, this means it will only be available within that block, including any nested child blocks.

Scope

Learn more about how and when we can access variables, by understanding the importance of the scope in which they exist.

In the following example, we will get a compilation error, as the Integer alias is only available within the scope of MyFunction

void SomeFunction() {
  using Integer = int;
  // ...
}

int main() {
  Integer SomeValue; 
}
error: 'Integer': undeclared identifier

Project Wide Aliases

Another common use case for type aliases is to specify types that we believe may need to change across our whole project. One way to implement this is to define all our aliases in a header file that gets included in every other file in our project.

In this example, we want our project to use the int32_t type for integers, which use 32 bits (4 bytes) of memory:

// types.h
#pragma once
#include <cstdint>

using Integer = int32_t;
#include <iostream>
#include "types.h"

int main() {
  // Will be int32_t
  Integer SomeInt;

  std::cout << "Integer size: "
    << sizeof(Integer) << " bytes";
}
Integer size: 4 bytes

The benefit of this approach is that if we want to change a type across our entire project, we now only need to change the alias, which is defined in a single place. This also allows us to change the type at compile time, with help from preprocessor definitions:

//types.h
#pragma once
#include <cstdint>

#ifdef USE_64_BIT_INTS
  using Integer = int64_t;
#else
  using Integer = int32_t;
#endif

Preprocessor Definitions

Explore the essential concepts of C++ preprocessing, from understanding directives to implementing macros

Using std::conditional

Within <type_traits>, the std::conditional helper lets us choose between one of two types at compile time. This gives us a way to implement the behaviour of the previous example using C++ rather than the preprocessor.

std::conditional accepts three template parameters:

  1. A boolean value that is known at compile time
  2. A type to use if the boolean is true
  3. A type to use if the boolean is false

The resulting type is available from the type static member:

#include <cstdint>
#include <type_traits>
#include <iostream>

constexpr bool Use64BitInts{true};

using Integer = std::conditional<
  Use64BitInts, int64_t, int32_t
>::type;

int main() {
  std::cout << "Integer size: "
    << sizeof(Integer) << " bytes";
}
Integer size: 8 bytes

Rather than accessing ::type, we can alternatively use std::conditional_t, which returns the resolved type directly:

#include <cstdint>
#include <type_traits>
#include <iostream>

constexpr bool Use64BitInts{true};

using Integer = std::conditional_t<
  Use64BitInts, int64_t, int32_t>;

int main() {
  std::cout << "Integer size: "
    << sizeof(Integer) << " bytes";
}
Integer size: 8 bytes

Using typedef

The C language implemented type aliases using the typedef keyword, and this is still supported in C++. Instead of:

using Integer = int;

We could write:

typedef int Integer;

The support of typedef is mostly for historical reasons, but it still crops up a lot in existing code. In general, we should prefer the using approach. Most people find using more readable, and it is also compatible with templates, which we'll cover later in this chapter.

Summary

In this lesson, we explored type aliases, learning how to simplify and give semantic meaning to complex types, The key takeaways include:

  • Type aliases help simplify verbose or complex type declarations, making code easier to understand and maintain.
  • The using keyword allows for the creation of type aliases.
  • Type aliases can be scoped globally or locally, following normal C++ scoping rules to control visibility.
  • Project-wide type aliases enable consistent type usage across a project and can be easily changed from a single location.
  • The type an alias uses can be conditionally set at compile time, using the preprocessor or std::conditional.
Next Lesson
Lesson 20 of 128

Type Deduction Using decltype and declval

Learn to use decltype and std::declval to determine the type of an expression at compile time.

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Performance Impact of Type Aliases
Are there any performance implications of using type aliases?
Type Aliases with Function Pointers
Can I use type aliases with function pointers? How?
Type Aliases with Const and Volatile
How do type aliases interact with const and volatile qualifiers?
Naming Conventions for Type Aliases
What are the best practices for naming type aliases?
Type Aliases with Auto-Deduced Types
Are there any limitations to using type aliases with auto-deduced types?
typedef vs using in Templates
What's the difference between type aliases and typedefs in terms of template support?
Platform-Specific Type Aliases
Can I use type aliases to create platform-specific type definitions?
Type-Safe Enum Pattern with Aliases
How can I use type aliases to implement a type-safe enum pattern in C++?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant