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.

4e.jpg
Ryan McCombe
Ryan McCombe
Posted

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 could be:

  • 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 really need the underscore?

We may have noticed that built-in literals do not have intermediate underscores. This option is reserved for standard library literals, including ones that may come in the future. The C++ standard has decided 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 succinct - 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 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>

int operator""_meters(unsigned long long x){
  std::cout << "Used _meters with arg: " << x;
  return 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 literals like 3_meters, the name of the function will be operator""_meters

For literals like 3.14_radians, the name of the function will be operator""_radians

For literals like "192.128.0.1"_ip, the name of the function will be operator""_ip

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
  • Characters - char
  • Strings - const char*

With const char* literals, we can include a second function parameter, which will receive the length of the string:

#include <iostream>

void operator""_ip(const char* x,
                   std::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 that later in this lesson.

There are additional options for so-called 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 if we need to deal with properties such as weights, currencies, or distances:

#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 from User-Defined Literals

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{static_cast<float>(D)};
}

Distance operator""_kilometers(long double D){
  return Distance{static_cast<float>(D * 1000)};
}

Distance operator""_miles(long double D){
  return Distance{static_cast<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{static_cast<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. A Temperature class would need a little more thought to ensure conversions between Fahrenheit and Celsius or Kelvin are respectful of this unusual order of operations.

Advice: Don’t Overuse User-Defined Literals

When people first learn about user-defined literals, many are tempted to overuse them. The ability to almost create our own syntax to match our exact needs is tempting, but it can be overused.

For example, we could construct a Character with a user-defined literal:

"Legolas"_character;

We could even allow multiple arguments in a string, and then parse them out within our function or class:

"Legolas,Elf,100"_character

But, just because we can do something, doesn’t mean we should. Techniques like this really don’t save many keystrokes and are less clear than calling a constructor the regular way:

Character{"Legolas"};
Character{"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.

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

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

7a.jpg
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:

  • 106 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 automatic expression rewriting, and the new spaceship operator added in C++20
ds7.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved