Creating Type Aliases for Clarity
Give complex types readable names using typedef and the modern using keyword.
What Is a Type Alias?
A type alias is a second name for a type that already exists. You introduce one with the using keyword: the new name, an equals sign, then the type it stands for.
using Celsius = double; // Celsius now means double
From that point on, Celsius can appear anywhere a type can appear: variable definitions, function parameters, return types. What it cannot do is create anything new. The type system gains no member, no conversion and no check. It gains one extra spelling for double.
The Compiler Substitutes, It Does Not Wrap
The mental model that keeps you out of trouble is textual: when the compiler meets an alias, it replaces it with the aliased type and carries on as if you had written that type yourself. Nothing is layered around the value, and nothing at runtime records where the name came from.
That means an alias and its underlying type mix freely in both directions.
#include <iostream>
using Celsius = double;
double toFahrenheit(double reading)
{
return reading * 1.8 + 32.0;
}
Celsius warmer(Celsius reading)
{
return reading + 5.0;
}
int main()
{
Celsius outside{ 21.5 };
double plain{ 18.0 };
std::cout << toFahrenheit(outside) << '\n';
std::cout << warmer(plain) << '\n';
return 0;
}
Output:
70.7
23
outside is declared Celsius and passed to a parameter declared double. plain is declared double and passed to a parameter declared Celsius. Neither call converts anything, because there is nothing to convert: both functions take a double, and both variables are a double. The alias changed what the source code says, not what the compiler builds.
An alias is a naming device, not a type-building device.
Celsius is not a type related to double; it is the type double, reached by a different name.
Two Aliases of One Type Are One Type
Follow the substitution model to its conclusion and you get the property that surprises people most. If two aliases name the same underlying type, they are indistinguishable to the compiler, however different they look to you.
#include <iostream>
using Meters = long;
using Seconds = long;
int main()
{
Meters depth{ 40 };
Seconds interval{ 90 };
depth = interval;
std::cout << "depth is now " << depth << '\n';
return 0;
}
Output:
depth is now 90
Assigning a duration to a depth is nonsense in every sense except the one the compiler cares about. It sees a long being assigned to a long, which is unremarkable, so it says nothing.
If you are not yet convinced that the two names are the same type, the following program will not compile, and its error message settles the question:
#include <iostream>
using Meters = long;
using Seconds = long;
void report(Meters value)
{
std::cout << "metres: " << value << '\n';
}
void report(Seconds value)
{
std::cout << "seconds: " << value << '\n';
}
int main()
{
report(Meters{ 40 });
return 0;
}
The compiler reports:
s.cpp:11:6: error: redefinition of 'void report(Seconds)'
11 | void report(Seconds value)
| ^~~~~~
s.cpp:6:6: note: 'void report(Meters)' previously defined here
6 | void report(Meters value)
| ^~~~~~
Two overloads that differ only in which alias they name are not two overloads at all. They are one function, defined twice.
A type that rejects such mixing is called a strong typedef, and C++ has no built-in form of it as of C++20. Scoped enumerations come closest among the language's own features, and several third-party libraries offer the full article. Until you reach for one of those, an alias buys readability and nothing else.
Type aliases are not type safe. Naming two quantities
Meters and Seconds documents your intent for human readers, but the compiler will not stop you, or anyone else, from mixing them.
Where an Alias Lives
An alias name is an identifier, so it obeys the ordinary scope rules with no exceptions. Declared inside a function it has block scope and disappears at the closing brace; declared at namespace scope it lasts to the end of the file.
#include <iostream>
int main()
{
using Watts = int; // usable only inside main
Watts draw{ 60 };
std::cout << draw << " W\n";
return 0;
}
Output:
60 W
Which means an alias you want in several translation units goes in a header, exactly like any other declaration you want to share:
#pragma once
using Meters = long;
using Seconds = long;
Including that header brings both names into the global namespace of every file that includes it:
#include "units.h"
#include <iostream>
int main()
{
Meters depth{ 40 };
Seconds interval{ 90 };
std::cout << depth << " m over " << interval << " s\n";
return 0;
}
Output:
40 m over 90 s
Because an alias declaration produces no object and no function, including it in many files causes no duplicate definition problem of the kind you get from defining a variable in a header.
Two Spellings, One Feature
using is the modern spelling. The older one, inherited from C, is the typedef keyword, which writes the aliased type first and the new name second:
typedef long Meters; // the older spelling
using Meters = long; // the modern spelling
Both produce the same alias. The standard even calls the names introduced by both forms typedef-names, and in conversation people say "typedef" for either. Three things make the older spelling the worse choice:
typedef |
using |
|
|---|---|---|
| Order of the two names | aliased type first, new name second, and the two are easy to swap by accident | new name always on the left of =, in the same position as a variable's name |
| Long types | the new name is pushed into the middle of the declaration | the new name stays at the front, separated by = |
| Templates | cannot be templated | can be templated, giving alias templates |
The order problem is real but self-correcting: writing typedef Kelvin double; when you meant typedef double Kelvin; produces error: 'Kelvin' does not name a type, so you find out immediately. The readability problem is the one that lasts, and it shows up as soon as the aliased type is anything but a single word. Here is the same alias for a function type written both ways:
typedef double (*SampleFilter)(double, int); // where is the name?
using SampleFilter = double(*)(double, int); // the name is first
You have not met that syntax yet, and that is precisely the point. In the typedef form the new name sits buried between the return type and the parameter list, and you have to parse the declaration to find out what is even being defined. In the using form everything left of the = is the name and everything right of it is the type.
Prefer
using for new aliases. Reach for typedef only when working in code that already uses it consistently.
Naming an Alias
Three conventions are in circulation, and you will meet all of them in real code:
| Style | Example | Where you see it |
|---|---|---|
_t suffix |
size_t, nullptr_t |
inherited from C; POSIX reserves this suffix for globally scoped names, so your own use of it risks a collision |
_type suffix |
std::string::size_type |
some standard library nested aliases, though many others such as std::string::iterator use no suffix at all |
| No suffix, initial capital | Celsius, Meters |
the modern convention for aliases you write yourself |
The initial capital is doing useful work. Type names starting with a capital and variable names starting with lowercase means a type and a variable can share a word without colliding:
void logReading(Celsius celsius);
Name your own aliases with an initial capital and no suffix, matching how you name any other type.
Where Aliases Earn Their Keep
An alias pays for itself when the name it replaces is long, repeated, or platform-dependent. Four situations account for most good uses:
| Situation | What the alias replaces | What you gain |
|---|---|---|
| Platform-dependent sizes | a fundamental type whose width varies by platform | one place decides which type is used, and the name states the width |
| Long compound types | a nested template type repeated in many signatures | signatures you can read at a glance, and no chance of mistyping one of them |
| Meaningless return types | a bare int or double returned by a function |
the return type says what the number means, not just how it is stored |
| Types that may change later | a type used in dozens of declarations | changing it is a one-line edit rather than a search |
Platform-dependent sizes. char, short, int and long have minimum widths, not fixed ones, so code that depends on an exact width cannot spell it with those keywords. The fixed-width types in <cstdint> solve this, and they are nothing more than aliases: on each platform, std::int32_t is defined as whichever fundamental type is 32 bits wide there.
Because they are aliases and not new types, they behave exactly like whatever they name, which produces one famous surprise:
#include <cstdint>
#include <iostream>
int main()
{
std::int8_t code{ 65 };
std::int32_t total{ 65 };
std::cout << "as int32_t: " << total << '\n';
std::cout << "as int8_t: " << code << '\n';
std::cout << "sizes: " << sizeof(std::int8_t) << ' ' << sizeof(std::int32_t) << '\n';
return 0;
}
Output:
as int32_t: 65
as int8_t: A
sizes: 1 4
Both variables hold 65 and the two objects differ only in width, yet one prints as a number and the other as a letter. On this platform std::int8_t is an alias for signed char, and std::cout prints character types as characters. The alias hid the underlying type from the reader but not from the language.
Long compound types. This is where aliases repay the most effort. Compare a pair of declarations written out in full against the same pair behind an alias:
bool hasEmptyChannel(std::vector<std::vector<double>> grid);
std::vector<std::vector<double>> transposeChannels(std::vector<std::vector<double>> grid);
using ChannelGrid = std::vector<std::vector<double>>;
bool hasEmptyChannel(ChannelGrid grid);
ChannelGrid transposeChannels(ChannelGrid grid);
std::vector and the angle-bracket syntax are covered in a later chapter; what matters here is the shape. The second version is shorter, but more importantly it is uniform: every place that deals in a grid of channels now says so in the same words, and a typo in one of them becomes a compile error rather than a subtly different type.
Meaningless return types. A declaration such as int calculateScore(int base, int bonus); tells you the result is an integer and leaves you guessing what integer. Points? A percentage? An error code? An alias moves that answer into the signature:
using Score = int;
Score calculateScore(int base, int bonus);
For a single function a comment does the same job more cheaply. The alias starts paying once a handful of functions accept and return the same quantity.
Types that may change later. If batch identifiers are declared short in fifty places and you outgrow short, you have fifty edits and a hunt to work out which short objects are identifiers and which are something else. Behind using BatchId = short; it is one edit. Do treat that edit as a real change rather than a rename: moving between signed and unsigned, or between integral and floating point, changes how comparisons, division and overflow behave, so retest anything that touches the type.
Where They Cost More Than They Save
Every alias adds a name a reader has to learn. When the name replaces something long, repeated or genuinely platform-dependent, that trade is worth making. When it replaces something the reader already knew, it is a loss: an alias for std::string turns a type everybody recognises into one they have to look up, and hides the interface they would otherwise expect. The same applies to aliases that obscure how a type is meant to be used, which is a common complaint about aliasing smart pointer types down to a bare name.
The rough test is how many places the alias appears. One use is usually noise. Twenty uses across a header is usually a saving.
Introduce an alias when it makes code shorter or clearer in many places at once. Leave familiar types spelled out.
Looking Forward
Aliases put a name on a type you already know. The next lesson goes the other way with auto, which lets you leave the type unnamed and have the compiler deduce it from the initializer. The two are complementary: auto is for a type you would only have to write once, an alias is for one you would otherwise have to write everywhere. Later chapters return to aliases with alias templates, and you will meet them constantly inside the standard library, where names such as std::string::size_type are exactly this feature applied to somebody else's code.
Key Terminology
| Term | Meaning |
|---|---|
| Type alias | A second name for an existing type, introduced with using |
typedef |
The older keyword for the same feature, writing the aliased type before the new name |
| Typedef-name | The standard's term for a name introduced by either form |
| Strong typedef | A distinct type carrying the original's properties, which C++ does not provide directly |
| Fixed-width integer | A type such as std::int32_t from <cstdint>, itself an alias for whichever fundamental type has that width on the platform |
Summary
An alias renames, it does not create. using Celsius = double; makes Celsius another way to write double, with no new type, no conversion and no runtime cost.
Aliases of the same type are the same type. Values mix silently in both directions, and two functions differing only in which alias they name are a redefinition error, not an overload.
Scope is ordinary. An alias declared in a block ends with the block; one declared at namespace scope reaches the end of the file. Share aliases by declaring them in a header.
using beats typedef. The new name always sits to the left of the = instead of somewhere inside the declaration, and only using can be templated.
Name aliases like types, with an initial capital and no suffix. The _t suffix is reserved by POSIX for globally scoped names.
Fixed-width integers are aliases. std::int32_t and friends name whichever fundamental type has the right width on the platform, which is why a std::int8_t holding 65 prints as A.
The payoff scales with repetition. Aliases are worth introducing for long compound types, for values whose meaning is not obvious from a fundamental type, and for types you expect to change. An alias used once, or one that hides a type the reader already knows, costs more than it saves.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
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!