Type Aliases
Learn how to use type aliases and utilities to simplify working with complex types.
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:
- A boolean value that is known at compile time
- A type to use if the boolean is true
- 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
.
Type Deduction Using decltype
and declval
Learn to use decltype
and std::declval
to determine the type of an expression at compile time.