The problem
Suppose we have a generic Reading type:
#include <chrono>
#include <iostream>
#include <string_view>
template <typename Value, typename Timestamp, typename Source>
struct Reading {
Value value {};
Timestamp timestamp {};
Source source {};
};
Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view>
makeVoltageReading(double voltage)
{
return Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view> {
.value = voltage,
.timestamp = std::chrono::system_clock::now(),
.source = "battery monitor"
};
}
void printReading(
const Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view>& reading){
std::cout << reading.source << ": "
<< reading.value << " V\\n";
}
int main(){
auto reading { makeVoltageReading(12.4) };
printReading(reading);
}
Terminal output:
battery monitor: 12.4 V
The repeated type is the problem:
Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view>
It is technically correct, but it is very long.
Creating an alias template
An alias template looks like this:
template <typename Value>
using SensorReading =
Reading<Value, std::chrono::time_point<std::chrono::system_clock>, std::string_view>;
Value remains flexible.
The timestamp type and source type are fixed:
std::chrono::time_point<std::chrono::system_clock>
std::string_view
Now the code can say:
SensorReading<double>
SensorReading<int>
SensorReading<float>
instead of writing the full Reading<...> type every time.
NOTE: Alias templates do not support CTAD
Refactoring to use an alias template
Here is the refactored program:
#include <chrono>
#include <iostream>
#include <string_view>
template <typename Value, typename Timestamp, typename Source>
struct Reading {
Value value {};
Timestamp timestamp {};
Source source {};
};
template <typename Value>
using SensorReading =
Reading<Value, std::chrono::time_point<std::chrono::system_clock>, std::string_view>;
SensorReading<double> makeVoltageReading(double voltage){
return SensorReading<double> {
.value = voltage,
.timestamp = std::chrono::system_clock::now(),
.source = "battery monitor"
};
}
void printReading(const SensorReading<double>& reading){
std::cout << reading.source << ": "
<< reading.value << " V\\n";
}
int main(){
auto reading { makeVoltageReading(12.4) };
printReading(reading);
}
Terminal output:
battery monitor: 12.4 V
The main improvement is not that the code became shorter. The improvement is that the type now has a domain name:
SensorReading<double>
That tells the reader what the type represents.
The underlying implementation is still:
Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view>
The alias hides the repetitive machinery and exposes the concept.
Alias templates do not create new types
An alias template does not define a new type.
It creates another name for an existing type pattern.
This means:
SensorReading<double>
is the same type as:
Reading<double, std::chrono::time_point<std::chrono::system_clock>, std::string_view>
The compiler treats them as equivalent.
That is useful for readability, but it does not create stronger type safety. If you need a truly distinct type, use a struct or class.
Another small example
Alias templates are common with standard containers.
For example, if a project repeatedly uses a vector with a custom allocator, the full type can be hidden behind an alias template:
template <typename T>
using Buffer = std::vector<T, CustomAllocator<T>>;
Now the code can use:
Buffer<int>
Buffer<double>
Buffer<std::string>
instead of repeating the allocator every time.
The flexible part is still T. The repetitive part is fixed inside the alias.
In conclusion
An alias template gives a template-based name to another type. It is useful when part of a long template type should stay flexible. They improve readability without creating a new type.