User Defined Literals
A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values
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
Examples of User-Defined Literals
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:
- A value, e.g.
3
,3.14
, or"192.128.0.1"
- An underscore,
_
- A name, e.g.
meters
,radians
, orip
Creating User-Defined Literals
User-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.
Function Name
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 calledoperator""_meters
3.14_radians
will invoke a function calledoperator""_radians
"192.168.0.1"_ip
will invoke a function calledoperator""_ip
'C'_grade
will invoke a function calledoperator""_grade
Function Parameter Type
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:
- Integers -
unsigned long long int
- Floats -
long double
- Strings -
const char*
- Characters -
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.
Function Return Type and Body
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
Use Case: Conversions
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
.
Dates, Times and Durations
Learn the basics of the chrono library and discover how to effectively manage durations, clocks, and time points
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
User-Defined Literals in a Namespace
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};
}
Returning Custom Types
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
Negative Numbers and Precedence
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.
Don't Overuse User-Defined Literals
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.
Summary
User-defined literals enhance code expressiveness and readability, enabling us to create more intuitive APIs.
Key Learnings
- The syntax and structure of user-defined literals, including the use of the underscore and the naming conventions enforced by the C++ standard.
- How to create user-defined literals using the
operator""
syntax, and the specific argument types supported, most notablyunsigned long long
,long double
,char
, andconst char*
. - The importance of using the underscore in user-defined literals to avoid conflicts, differentiating them from standard library literals.
- Like any function, user-defined literals can return values by specifying a return type in the signature and using the
return
statement within the body. - The common use case of user-defined literals for unit conversions, such as distances and currencies.
- The practice of defining user-defined literals within namespaces to prevent naming conflicts and enhance code organization.
- The consideration of operator precedence, especially in the context of negative numbers, and how it impacts the behavior and implementation of user-defined literals.
- The caution against overuse of user-defined literals to maintain code readability and leverage the benefits of tooling support and compiler checks.
The Spaceship Operator and Expression Rewriting
A guide to simplifying our comparison operators using C++20 features