using
statements, and typedef
to simplify or rename complex C++ types.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.
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.
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
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.
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
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 us a way to implement the behaviour of the previous example using C++ rather than the preprocessor.
std::conditional
accepts three template parameters:
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 result in an 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
(available by including <utility>
) allows us to work with a hypothetical object of a specific type.
#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.
In this lesson, we explored type aliases, learning how to simplify and give semantic meaning to complex types, The key takeaways include:
using
keyword allows for the creation of type aliases.std::conditional
.decltype
specifier is useful for deducing the type of an expression.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.Learn how to use type aliases, using
statements, and typedef
to simplify or rename complex C++ types.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.