User Defined Literals

A practical guide to user-defined literals in C++, which allow us to write more descriptive and expressive values
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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, or ip

Do we need the underscore?

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:

  • A literal without a _ looks like a standard library literal. This is confusing to other developers reading or working on our code
  • If a future version of the spec adds a built-in literal with the same name as ours, our code gets significantly more confusing
  • Some compilers do enforce the rule, and those that currently don’t may do so in the future. We generally want our code to be as portable as possible, and deviating from the standard reduces our portability

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 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

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.

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 notably unsigned long long, long double, char, and const 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.

Was this lesson useful?

Next Lesson

The Spaceship Operator and Expression Rewriting

A guide to simplifying our comparison operators using C++20 features
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

The Spaceship Operator and Expression Rewriting

A guide to simplifying our comparison operators using C++20 features
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved