As a general goal, we want our code to be descriptive. In the very first lesson, we discussed the importance of having descriptive identifiers - which include variables and function names. A variable called Health
is more descriptive than one called x
.
The idea of custom types takes that idea further. Types like Distance
and Temperature
are inherently more descriptive than types like int
and float
.
In this lesson, we’ll see how we can even make values more descriptive. In the following code, it’s clear we’re adding a value of 3
to a variable called Distance
:
Distance += 3;
But three what? Three centimeters? Three meters? Three kilometers?
With user-defined literals, we can be more expressive:
Distance += 3_meters;
Distance += 4_kilometers;
Distance += 5_miles;
Behind the scenes, these literals are calling functions that we can define to meet our specific requirements. Let's see how we can set this up
Some examples of user-defined literals include:
3_meters
3.14_radians
"192.128.0.1"_ip
They all follow the same pattern - they have 3 components, in order:
3
, 3.14
, or "192.128.0.1"
_
meters
, radians
, or ip
We may have noticed that built-in literals do not have intermediate underscores. For example, a std::string
literal looks like "Hello"s
, rather than "Hello"_s
.
The option to define literals without an underscore is reserved for standard library literals, including ones that may come in the future. The C++ standard requires that the suffix of user-defined literals must start with an _
.
However, not all compilers are forcing this standard, and some books and learning resources are also not including the _
in user-defined literals.
Removing the _
does make our code more concise - 3km
looks nicer than 3_km
. However, there are some issues:
_
looks like a standard library literal. This is confusing to other developers reading or working on our codeUser-defined literals are, in effect, another way to call a function that we define.
The function that will be invoked by the 3_meters
literal will have this syntax:
#include <iostream>
void operator""_meters(unsigned long long x){
std::cout << "Used _meters with arg: " << x;
}
int main(){
3_meters;
}
Used _meters with arg: 3
Let's break down the various components of this function.
The name begins with operator""
, followed by an underscore, and then the name we want to use for the literal. For example:
3_meters
will invoke a function called operator""_meters
3.14_radians
will invoke a function called operator""_radians
"192.168.0.1"_ip
will invoke a function called operator""_ip
'C'_grade
will invoke a function called operator""_grade
The value we have before the _
of the literal will be passed to the function as an argument. The only values we can support are specific types of integers, floating point numbers, characters, or strings. The types are:
unsigned long long int
long double
const char*
char
With const char*
literals, we can include a second function parameter, which receives the length of the string:
#include <iostream>
void operator""_ip(const char* x, size_t size) {
std::cout << "Called _ip with a string"
" of size: " << size;
}
int main() {
"192.168.0.1"_ip;
}
Called _ip with a string of size: 11
Even though the integer must be unsigned, we can still use the negation operator -
. We’ll discuss this later in this lesson.
There are additional options for wide characters and wide strings. We’ll discuss wide characters and strings in the next chapter.
We are free to return any type from our user-defined literal functions, including custom types
Similarly, we are free to implement the function body in whatever way we want
The most common use case for user-defined literals is to handle conversions. We already saw examples of this in the chrono literals, which gave us time-based literals like 3d
, 5h
, and 20min
.
We can implement similar literals in our code - for example, we could implement literals to give us a descriptive syntax for weights, currencies, or distances.
The following example demonstrates literals for converting distances to meters:
#include <iostream>
float operator""_mm(long double D){
return D / 1000;
}
float operator""_cm(long double D){
return D / 100;
}
float operator""_in(long double D){
return D / 39.37;
}
float operator""_ft(long double D){
return D / 3.28;
}
float operator""_m(long double D){
return D;
}
float operator""_km(long double D){
return D * 1000;
}
int main(){
float Distance{3.0_m};
std::cout << "Distance: " << Distance <<
" meters";
Distance += 2.0_ft;
std::cout << "\nDistance: " << Distance <<
" meters";
}
Distance: 3 meters
Distance: 3.60976 meters
Literals are typically defined in an external file, which is globally available across our project. As part of this, it’s often sensible to wrap them in a namespace, to prevent naming conflicts:
namespace distance_literals{
float operator""_mm(long double D){
return D / 1000;
}
// ...
}
We can then implement a using namespace
statement anywhere we need to use our literals:
int main(){
using namespace distance_literals;
float Distance{3.0_mm};
}
We are not restricted to returning built-in types from our literals. We can return any type we want. The following examples return a custom Distance
type, which has overloaded the <<
 operator:
#include <iostream>
class Distance {
public:
Distance(float Value) : Value{Value}{}
float Value;
};
std::ostream& operator<<(
std::ostream& Stream,
Distance D
){
Stream << D.Value << " meters\n";
return Stream;
}
Distance operator""_meters(long double D){
return Distance{float(D)};
}
Distance operator""_kilometers(long double D){
return Distance{float(D * 1000)};
}
Distance operator""_miles(long double D){
return Distance{float(D * 1609)};
}
int main(){
std::cout << 4.2_meters;
std::cout << 0.4_kilometers;
std::cout << 0.1_miles;
}
4.2 meters
400 meters
160.9 meters
Even though the values passed to our literal functions must be positive, we can still use the -
 operator:
-0.1_miles
However, it’s important to understand what is going on here. The -
operator has lower precedence than the user-defined literal.
That means that our literal function is called with the positive value. Then, the negation operator is applied to the value that is returned from that function:
#include <iostream>
class Distance {
public:
Distance(float Value) : Value{Value}{}
Distance operator-(){
std::cout << "Negating\n";
return Distance{-Value};
}
float Value;
};
Distance operator""_miles(long double D){
return Distance{float(D * 1609)};
}
int main(){
std::cout << (-0.1_miles).Value << " meters";
}
Negating
-160.9 meters
This has a few implications. Most notably, it means the type returned must support the unary -
operator. But also, we need to be mindful of the order of operations, particularly when dealing with conversions.
This order of operations still returns the correct values for distances, for example, but it would not work for temperatures. 10 degrees Celsius is 50 degrees Fahrenheit, but -10 degrees Celsius is not -50 degrees Fahrenheit.
Therefore, our hypothetical temperature implementation would need a little more thought to ensure conversions are respectful of this order of operations.
When we first learn about user-defined literals, many are tempted to overuse them. The ability to define our syntax to match our exact needs is tempting, but it can be overused.
For example, we could construct a custom Player
object with a user-defined literal:
"Legolas"_player;
We could even allow multiple arguments in a string, and then parse them out within our function or class:
"Legolas,Elf,100"_player;
But, just because we can do something, doesn’t mean we should. Techniques like this don’t save many keystrokes and are less clear than calling a constructor the regular way:
Player{"Legolas"};
Player{"Legolas", Race::Elf, 100};
This way also provides more help from our tooling. As soon as our IDE recognizes what class we’re constructing, it can jump in and assist us by telling us what arguments we need. And, if we get it wrong, the compiler will throw an error at the exact location where we’re passing an invalid argument.
User-defined literals are a powerful way to make our code more expressive, but in almost all programs, we should be highly selective in where we deploy them.
User-defined literals enhance code expressiveness and readability, enabling us to create more intuitive APIs.
operator""
syntax, and the specific argument types supported, most notably unsigned long long
, long double
, char
, and const char*
.return
statement within the body.A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.