Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Creating Type Aliases for Clarity
Give complex types readable names using typedef and the modern using keyword.
Typedefs and Type Aliases
Type Aliases
In C++, using is a keyword that creates an alias for an existing data type. To create a type alias, we use the using keyword, followed by a name for the alias, an equals sign, and an existing data type. Example:
using Temperature = double; // define Temperature as an alias for type double
Once defined, a type alias can be used anywhere a type is needed. For instance, we can create a variable with the alias name as the type:
Temperature currentTemp{ 72.5 }; // defines a variable of type double
When the compiler encounters a type alias name, it substitutes the aliased type. Example:
#include <iostream>
int main()
{
using Temperature = double; // define Temperature as an alias for type double
Temperature currentTemp{ 72.5 }; // defines a variable of type double
std::cout << currentTemp << '\n'; // prints a double value
return 0;
}
This prints:
72.5
In the above program, we first define Temperature as an alias for type double.
Next, we define a variable named currentTemp of type alias Temperature. Because the compiler knows Temperature is a type alias, it uses the aliased type, which is double. Thus, variable currentTemp is actually compiled as type double, behaving as a double in all respects.
Finally, we print the value of currentTemp, which prints as a double value.
Advanced note: Type aliases can be templated (covered in a future lesson on class template argument deduction).
Naming Type Aliases
Historically, type alias naming hasn't been very consistent. Three common naming conventions exist (you'll encounter all):
- Type aliases ending in "_t" suffix (short for "type"). The standard library often uses this for globally scoped type names (like
size_tandnullptr_t).
This convention was inherited from C and used to be most popular when defining your own type aliases (and sometimes other types), but has fallen out of favor in modern C++. Note that POSIX reserves the "_t" suffix for globally scoped type names, so using this convention may cause naming conflicts on POSIX systems.
- Type aliases ending in "_type" suffix. Some standard library types (like
std::string) use this for nested type aliases (e.g.,std::string::size_type).
But many nested type aliases use no suffix at all (e.g., std::string::iterator), so this usage is inconsistent at best.
- Type aliases using no suffix.
In modern C++, the convention is to name type aliases (or any type) you define yourself starting with a capital letter and using no suffix. The capital letter differentiates type names from variable and function names (which start with lowercase), preventing naming collisions.
When using this naming convention, you commonly see:
void displayTemperature(Temperature temperature); // Temperature is some defined type
Here, Temperature is the type, and temperature is the parameter name. C++ is case-sensitive, so this works fine.
Name your type aliases starting with a capital letter and without a suffix (unless you have a specific reason otherwise).
Type Aliases Are Not Distinct Types
An alias doesn't actually define a new, distinct type (one considered separate from others) -- it just introduces a new identifier for an existing type. A type alias is completely interchangeable with the aliased type.
This allows syntactically valid but semantically meaningless operations. Example:
int main()
{
using Meters = long; // define Meters as an alias for type long
using Seconds = long; // define Seconds as an alias for type long
Meters distance { 500 }; // distance is actually just a long
Seconds duration { 60 }; // duration is actually just a long
// The following is syntactically valid but semantically meaningless
distance = duration;
return 0;
}
Though conceptually we intend Meters and Seconds to have distinct meanings, both are just aliases for type long. This effectively means Meters, Seconds, and long can all be used interchangeably. When we assign a value of type Seconds to a variable of type Meters, the compiler only sees we're assigning a long value to a variable of type long, and won't complain.
Because the compiler doesn't prevent these semantic errors for type aliases, we say aliases are not type safe. Despite this, they remain useful.
Be careful not to mix values of aliases intended to be semantically distinct.
Advanced note: Some languages support strong typedef (or strong type alias). A strong typedef creates a new type with all original type properties, but the compiler throws an error if you try to mix values of the aliased type and the strong typedef. As of C++20, C++ doesn't directly support strong typedefs (though enum classes are similar), but several 3rd party C++ libraries implement strong typedef-like behavior.
The Scope of a Type Alias
Because scope is a property of identifiers, type alias identifiers follow the same scoping rules as variable identifiers: a type alias defined inside a block has block scope and is usable only within that block, whereas a type alias defined in the global namespace has global scope and is usable to file end. In the above example, Meters and Seconds are only usable in the main() function.
If you need one or more type aliases across multiple files, define them in a header file and #include into any code files needing the definition:
measurement.h:
#pragma once
using Meters = long;
using Seconds = long;
Type aliases #included this way import into the global namespace and thus have global scope.
Typedefs
A typedef (short for "type definition") is an older way of creating a type alias. To create a typedef alias, we use the typedef keyword:
// The following aliases are identical
typedef long Meters;
using Meters = long;
Typedefs remain in C++ for backwards compatibility but have been largely replaced by type aliases in modern C++.
Typedefs have several syntactical issues. First, it's easy to forget whether the typedef name or the aliased type comes first. Which is correct?
typedef Temperature double; // incorrect (typedef name first)
typedef double Temperature; // correct (aliased type name first)
Easy to get backwards. Fortunately, the compiler will complain in such cases.
Second, typedef syntax gets ugly with complex types. Here's a hard-to-read typedef with an equivalent (slightly easier to read) type alias:
typedef int (*FunctionPtr)(double, char); // FunctionPtr hard to find
using FunctionPtr = int(*)(double, char); // FunctionPtr easier to find
In the typedef definition, the new type name (FunctionPtr) is buried mid-definition, whereas in the type alias, the new type name and the rest of the definition are separated by an equals sign.
Third, the name "typedef" suggests defining a new type, but that's not true. A typedef is just an alias.
Prefer type aliases over typedefs.
Nomenclature: The C++ standard uses "typedef names" for the names of both typedefs and type aliases.
In conventional language, "typedef" often means "either a typedef or a type alias" since they effectively do the same thing.
When Should We Use Type Aliases?
Now that we've covered what type aliases are, let's discuss their usefulness.
Using Type Aliases for Platform-Independent Coding
One primary use for type aliases is hiding platform-specific details. On some platforms, an int is 2 bytes; on others, it's 4 bytes. Thus, using int to store more than 2 bytes of information can be potentially dangerous when writing platform-independent code.
Because char, short, int, and long give no size indication, cross-platform programs commonly use type aliases defining aliases including the type's size in bits. For example, int8_t would be an 8-bit signed integer, int16_t a 16-bit signed integer, and int32_t a 32-bit signed integer. Using type aliases this way helps prevent mistakes and clarifies assumptions about variable size.
To ensure each aliased type resolves to a type of the right size, type aliases of this kind typically work with preprocessor directives:
#ifdef SMALL_INT
using int8_t = char;
using int16_t = int;
using int32_t = long;
#else
using int8_t = char;
using int16_t = short;
using int32_t = int;
#endif
On machines where integers are only 2 bytes, SMALL_INT can be #defined (as a compiler/preprocessor setting), and the program compiles with the top alias set. On machines where integers are 4 bytes, leaving SMALL_INT undefined causes the bottom alias set to be used. This way, as long as SMALL_INT is #defined correctly, int8_t resolves to a 1-byte integer, int16_t resolves to a 2-byte integer, and int32_t resolves to a 4-byte integer (using the appropriate combination of char, short, int, and long for the compilation machine).
The fixed-width integer types (such as std::int16_t and std::uint32_t) and the size_t type are actually just type aliases to various fundamental types.
This is why when you print an 8-bit fixed-width integer using std::cout, you likely get a character value. Example:
#include <cstdint>
#include <iostream>
int main()
{
std::int8_t code{ 65 }; // int8_t is usually a typedef for signed char
std::cout << code << '\n';
return 0;
}
This program prints:
A
Because std::int8_t is typically a typedef for signed char, variable code will likely be defined as a signed char. And char types print their values as ASCII characters rather than integer values.
Using Type Aliases to Make Complex Types Easier to Read
Although we've only dealt with simple data types so far, in advanced C++, types can be complicated and lengthy to manually type. For example, you might see a function and variable defined like this:
#include <string>
#include <vector>
#include <utility>
bool checkDuplicates(std::vector<std::pair<std::string, int>> recordList)
{
// some code here
return false;
}
int main()
{
std::vector<std::pair<std::string, int>> recordList;
return 0;
}
Typing std::vector<std::pair<std::string, int>> everywhere you need that type is cumbersome, and typos are easy. Much easier with a type alias:
#include <string>
#include <vector>
#include <utility>
using RecordList = std::vector<std::pair<std::string, int>>; // make RecordList an alias
bool checkDuplicates(RecordList recordList) // use RecordList in function parameter
{
// some code here
return false;
}
int main()
{
RecordList recordList; // instantiate a RecordList variable
return 0;
}
Much better! Now we only type RecordList instead of std::vector<std::pair<std::string, int>>.
Don't worry if you don't know what std::vector, std::pair, or those angle brackets are yet. The key point is type aliases let you take complex types and give them simpler names, making your code easier to read and saving typing.
This is probably the best use for type aliases.
Using Type Aliases to Document Value Meaning
Type aliases can also help with code documentation and comprehension.
With variables, we have the variable's identifier to document the variable's purpose. But consider a function's return value. Data types like char, int, long, double, and bool describe what type of value a function returns, but more often we want to know the meaning of a return value.
For example, given this function:
int processScore();
We see the return value is an integer, but what does the integer mean? A percentage? Points deducted? Student ID? Error code? Who knows! If we're lucky, function documentation exists somewhere to reference. If we're unlucky, we must read the code and infer the purpose.
Now let's do an equivalent version using a type alias:
using Score = int;
Score processScore();
The return type of Score makes it more obvious the function returns a type representing a score.
In our experience, creating a type alias just to document one function's return type isn't worth it (use a comment instead). But if you have multiple functions passing or returning such a type, creating a type alias might be worthwhile.
Using Type Aliases for Easier Code Maintenance
Type aliases also let you change an object's underlying type without updating lots of hardcoded types. For example, if you used a short to hold student ID numbers but later decided you needed a long instead, you'd have to comb through lots of code replacing short with long. Determining which short objects were used for ID numbers versus other purposes would be difficult.
However, if you use type aliases, changing types becomes as simple as updating the type alias (e.g., from using StudentId = short; to using StudentId = long;).
Caution is necessary whenever changing a type, as program behavior may also change. This is especially true when changing a type alias to a type in a different type family (e.g., an integer to a floating-point value, or a signed to unsigned value). The new type may have comparison or integer/floating-point division issues, or other issues the old type didn't. If you change an existing type to some other type, thoroughly retest your code.
Downsides and Conclusion
While type aliases offer benefits, they also introduce another identifier into your code needing understanding. If this isn't offset by some readability or comprehension benefit, the type alias does more harm than good.
A poorly utilized type alias can take a familiar type (such as std::string) and hide it behind a custom name needing lookup. In some cases (such as with smart pointers), obscuring type information can also harm understanding how the type should work.
For this reason, use type aliases primarily where there's a clear benefit to code readability or code maintenance. This is as much art as science. Type aliases are most useful when they can be used in many places throughout your code, rather than in fewer places.
Use type aliases judiciously, when they provide a clear benefit to code readability or code maintenance.
Summary
Type aliases: Alternative names for existing types created with the using keyword. Do not create distinct types, just introduce new identifiers for existing types.
Naming conventions: Modern C++ convention prefers capital letter with no suffix (e.g., Temperature). Older conventions include _t suffix (POSIX-reserved) and _type suffix (inconsistently used).
Type safety limitation: Type aliases are not type-safe - the compiler treats aliases and their aliased types as completely interchangeable, allowing semantically meaningless operations.
Scope: Type alias identifiers follow the same scoping rules as variables. For multi-file use, define them in header files.
Typedefs vs type aliases: Typedefs are older C-style syntax that should be avoided in favor of clearer, more flexible type alias syntax using using.
When to use type aliases:
- Platform-independent coding (e.g., fixed-width integers like
int32_t) - Making complex types easier to read and write
- Documenting value meaning in function signatures
- Easier code maintenance when changing underlying types
When to avoid: Don't use type aliases when they obscure familiar types or provide no readability/maintenance benefit.
Type aliases are powerful tools for creating more readable, maintainable code, especially when working with complex types or platform-specific code.
Quiz
Question #1
Given the following function prototype:
int processData();
Convert the int return value to a type alias named Result. Include both the type alias statement and the updated function prototype.
Show Solution
using Result = int;
Result processData();
Creating Type Aliases for Clarity - Quiz
Test your understanding of the lesson.
Practice Exercises
Typedefs and Type Aliases
Practice creating meaningful type aliases to improve code readability and maintainability. Learn the difference between typedef and using declarations, and when to use each.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!