Type Aliases

Learn how to use type aliases, using statements, and typedef to simplify or rename complex C++ types.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

At this point, 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.

Why do we need aliases?

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 being 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 the code that uses it is easier to 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.

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

std::conditional

Within <type_traits>, the std::conditional helper lets us choose between one of two types at compile time. This gives is 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

decltype

Occasionally, it will be easier (or necessary) to ask the compiler to figure out what type an expression returns. We can do this using the decltype specifier.

In the following examples, we use decltype simply to set up type aliases, but its applications are much wider ranging as we’ll see through the rest of this chapter.

Below, 42 is an int, so decltype(42) will return int:

int main() {
  // SomeType will be an alias for int
  using SomeType = decltype(42);

  SomeType SomeValue;
}

The expression we use with decltype does not need to be a simple literal - below, we use it to determine what is returned by a function:

int SomeFunction() { return 42; }

int main() {
  // SomeType will be an alias for int
  using SomeType = decltype(SomeFunction());

  // This will be an int
  SomeType SomeValue;
}

We don’t need to use it with a type alias - we can use it almost anywhere a type is expected:

int SomeFunction() { return 42; }

int main() {
  // This will be an int
  decltype(SomeFunction()) SomeValue;
}

std::declval

In some scenarios, the expression we need to write to determine a type can be quite complex. For example, we might want to get the type returned by some member function: We could construct the object

struct MyType {
  int Get(){};
};

int main() {
  // Will be int
  using SomeType = decltype(MyType{}.Get());
}

But things can get more awkward if the constructor requires arguments:

struct MyType {
  MyType(int, float, bool) {}
  int Get(){};
};

int main() {
  // Will be int
  using SomeType = decltype(
    MyType{42, 9.8, true}.Get()
  );
}

Worse, in more complex scenarios involving templates that we’ll cover later, we won’t even know what type we’re constructing, or what arguments its constructor requires.

To help with this, std::declval within <utility> allows us to create a hypothetical object of a specific type. We can use it like this:

#include <utility>

struct SomeType {};

int main() {
  // A hypthetical int
  std::declval<int>();

  // A hypthetical float
  std::declval<float>();

  // A hypthetical SomeType
  std::declval<SomeType>();
}

This hypothetical object is only useful to assess the characteristics of its type at compile time, such as the return types of its methods.

But, its key advantage is that it doesn’t need to actually construct an object, so we don’t need to provide constructor arguments. We can solve our original problem like this:

#include <utility>

struct MyType {
  MyType(int, float, bool) {}
  int Get(){};
};

int main() {
  // Will be int
  using SomeType = decltype(
    std::declval<MyType>().Get()
  );
}

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.
  • The decltype specifier is useful for deducing the type of an expression.
  • Although typedef is supported for backward compatibility with C and legacy C++ code, using is generally preferred for its readability and compatibility with modern C++ features.

Was this lesson useful?

Next Lesson

Class Templates

Learn how templates can be used to create multiple classes from a single blueprint
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Type Aliases

Learn how to use type aliases, using statements, and typedef to simplify or rename complex C++ types.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Templates
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Class Templates

Learn how templates can be used to create multiple classes from a single blueprint
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved