C++ / FUNDAMENTAL TYPES AND VARIABLES
Type aliases with using declarations
After this you can introduce readable type names with using, including alias templates, and predict where an alias behaves exactly like the type it names.
What you will learn
- Write using Name = Type; to give one existing type a second, clearer name
- Prove transparency with static_assert(std::is_same_v<Alias, Target>)
- Parameterise a type with template <typename T> using Alias = ...;
- Choose a struct or enum class when you need the compiler to reject mixups
Understanding Type aliases with using declarations
The declaration using Celsius = double; binds the name Celsius, in the scope where it appears, to the type double. The compiler resolves that name at every use and then has nothing left over: there is one type with two spellings, so declarations, sizeof, overload resolution and template matching all see plain double. The syntax deliberately mirrors a variable initialisation, name on the left and the thing it names on the right, which is why it reads cleanly even for awkward types.
Because an alias is a name and not a wrapper, std::is_same_v<Celsius, double> is true, a double passes freely where a Celsius is expected, and you cannot write two overloads distinguished only by two aliases of the same type. You pay nothing at runtime and gain nothing in checking: the alias documents intent for human readers only. Being a declaration, it also obeys ordinary scoping and access rules, so it can live in a block, a namespace, or a class, where using value_type = T; becomes part of the class interface the way the standard containers publish their element types.
For simple cases typedef X Y; and using Y = X; mean the same thing, but typedef pushes the new name into the middle of a declarator: typedef int (*BinaryOp)(int, int); hides BinaryOp inside parentheses, while using BinaryOp = int (*)(int, int); keeps the name first. The decisive difference is that only an alias declaration can take template parameters, so template <typename T> using Table = std::vector<std::vector<T>>; is expressible and its typedef equivalent is not. That single capability is why modern C++ code and the standard library headers use alias declarations almost everywhere.
Since Table<int> is not a new type but a rewriting rule, it deduces, overloads and prints identically to std::vector<std::vector<int>>.
<iostream>
<map>
<string>
<type_traits>
using Celsius = double;
using ScoreTable = std::map<std::string, int>;
static_assert(std::is_same_v<ScoreTable, std::map<std::string, int>>);
int main() {
Celsius boiling = 100.0;
ScoreTable scores{{"ada", 42}, {"linus", 17}};
std::cout << "boiling: " << boiling << '\n';
std::cout << "ada: " << scores["ada"] << '\n';
std::cout << std::boolalpha;
std::cout << "Celsius is double: " << std::is_same_v<Celsius, double> << '\n';
std::cout << "same size: " << (sizeof(Celsius) == sizeof(double)) << '\n';
}
A using declaration adds another name for a type that already exists, so alias and target are the same type as far as the compiler is concerned.
Worked examples
An alias template, which typedef cannot express
Leaving a template parameter open in the alias so one name covers a whole family of types.
<iostream>
<vector>
template <typename T>
using Table = std::vector<std::vector<T>>;
using Row = std::vector<int>;
int main() {
Table<int> grid{{1, 2}, {3, 4}};
grid[1].push_back(5);
for (const Row& row : grid) {
std::cout << "row of " << row.size() << ": ";
bool first = true;
for (int v : row) {
if (!first) std::cout << ',';
std::cout << v;
first = false;
}
std::cout << '\n';
}
}
Example explained
Line 1template <typename T> using Table = ...; declares an alias template; a typedef has no place to put the open parameter T.
Line 2Table<int> names exactly std::vector<std::vector<int>>, so grid[1].push_back(5) is an ordinary vector call on the second row.
Line 3const Row& row binds directly to the stored std::vector<int> with no conversion, because Row is that type under another name.
Line 4row.size() reports 2 then 3, confirming the push_back went to the real underlying vector.
Aliases give no type safety
Two aliases of double are interchangeable, while a struct is a genuinely separate type.
<iostream>
using Meters = double;
using Seconds = double;
struct Km { double value; };
void report(Meters m) { std::cout << "distance " << m << " m\n"; }
// void report(Seconds s) { } // error: redefinition of report(double)
int main() {
Meters d = 400.0;
Seconds t = 45.0;
std::cout << "sum " << d + t << '\n';
report(t);
Km k{0.4};
std::cout << "km " << k.value << '\n';
}
Example explained
Line 1d + t compiles and yields 445 because both operands are double; the alias names are gone by the time the addition is checked.
Line 2report(t) passes a Seconds value to a Meters parameter for the same reason, so the unit error is invisible to the compiler.
Line 3The commented overload would redefine report(double) rather than add a candidate, which is why uncommenting it breaks the build.
Line 4Km is a class type, so it is genuinely distinct: a bare double would not bind to a Km parameter without an explicit construction.
Function pointer and member aliases
Using an alias to keep a function pointer type readable and to publish a member type from a class.
<iostream>
using BinaryOp = int (*)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
struct Accumulator {
using value_type = int;
value_type total = 0;
void apply(BinaryOp op, value_type x) { total = op(total, x); }
};
int main() {
Accumulator acc;
acc.total = 3;
acc.apply(add, 4);
std::cout << "after add: " << acc.total << '\n';
acc.apply(mul, 5);
std::cout << "after mul: " << acc.total << '\n';
Accumulator::value_type copy = acc.total;
std::cout << "copy: " << copy << '\n';
}
Example explained
Line 1using BinaryOp = int (*)(int, int); puts the alias name leftmost, whereas typedef int (*BinaryOp)(int, int); buries it inside the declarator.
Line 2acc.apply(add, 4) works because the function name add decays to a pointer of exactly the aliased type.
Line 3using value_type = int; inside the struct is a member alias, reachable from outside as Accumulator::value_type.
Line 4Editing that one member alias would retype total, the apply parameter and copy together, since all three spell the type through it.
Important notes
typedef X Y; and using Y = X; produce identical types, so mixing both styles in one codebase is legal; alias templates, however, cannot be partially or explicitly specialised, so wrap the specialised part in a class template if you need that.
std::is_same_v and the message-less static_assert require C++17; on older compilers write std::is_same<A, B>::value and supply a message string.
Common mistakes
Expecting using Meters = double; and using Seconds = double; to prevent unit mixups: meters + seconds compiles silently and produces a number, because there is only one type involved.
Writing using CharPtr = char*; and then const CharPtr p = buf;. The const applies to the alias as a whole, giving char* const, so p = other; is rejected while *p = 'x'; still compiles, the opposite of what const char* would do.
Reaching for typedef when a template parameter must stay open: typedef std::vector<T> Vec; at namespace scope will not compile, since only using can be parameterised.
Try it yourself
Change, predict, then run
Define using Grid = std::vector<std::vector<int>>;, build a 3x3 grid where element (r, c) holds r * 3 + c, and print the diagonal. Then add static_assert(std::is_same_v<Grid::value_type, std::vector<int>>); and confirm it still compiles.
Open the C++ workspaceCheck your understanding
A file contains using Id = int; using Count = int; followed by two definitions, void log(Id x) { ... } and void log(Count x) { ... }. What happens?
- It compiles, and every call to log is ambiguous.
- It compiles, and the compiler picks the overload whose alias name matches the argument's declared alias.
- It fails to compile, because both lines define the same function, void log(int).
- It compiles only when Id and Count are declared in different namespaces, since aliases are scoped.
Show answer
Id and Count are two spellings of int, so the second definition supplies a second body for void log(int), which is a redefinition error. Ambiguity is the tempting answer, but ambiguity needs two distinct viable functions to choose between, and here only one function ever exists.