User-Defined Literals
A literal refers to a fixed constant written directly in source code. In C++98, literals could only be of built-in types, such as:
"hello"
— a string literal, typeconst char[6]
1
— an integer literal, typeint
0.0
— a floating-point literal, typedouble
3.14f
— a floating-point literal, typefloat
123456789ul
— an unsigned long integer literal, typeunsigned long
C++11 introduced user-defined literals, allowing you to use the operator""
suffix to convert user-defined literals into actual types.
C++14 expanded this by adding many standard literals to the standard library. The following program demonstrates how they are used:
#include <chrono>
#include <complex>
#include <iostream>
#include <string>
#include <thread>
using namespace std;
int main()
{
cout << "i * i = " << 1i * 1i << endl;
cout << "Waiting for 500ms" << endl;
this_thread::sleep_for(500ms);
cout << "Hello world"s.substr(0, 5) << endl;
}
Output:
i * i = (-1,0)
Waiting for 500ms
Hello
This example shows standard C++ literal suffixes that help create imaginary numbers, time durations, and basic_string
literals. One thing to note: I used using namespace std
, which brings in not only std
but also its inline namespaces, including the namespaces for the literal operators:
std::literals::complex_literals
std::literals::chrono_literals
std::literals::string_literals
In production code, it’s generally discouraged (and should be avoided) to use using namespace std
globally. But for brevity, examples like this one use it. If you’re avoiding global using
, you should import only the necessary namespaces within the appropriate scope. For example:
using namespace std::literals::chrono_literals;
Adding support for literals in your own classes is fairly easy. The only restriction is that non-standard literal suffixes must begin with an underscore (_
). For instance, suppose we have a length class:
struct length {
double value;
enum unit {
metre, kilometre, millimetre, centimetre,
inch, foot, yard, mile,
};
static constexpr double factors[] = {
1.0, 1000.0, 1e-3,
1e-2, 0.0254, 0.3048,
0.9144, 1609.344
};
explicit length(double v, unit u = metre) {
value = v * factors[u];
}
};
length operator+(length lhs, length rhs) {
return length(lhs.value + rhs.value);
}
You could write something like length(1.0, length::metre)
—but most developers wouldn’t want to. Instead, a more pleasant syntax would be:
1.0_m + 10.0_cm
To support that syntax, we just define these operators:
length operator"" _m(long double v) {
return length(v, length::metre);
}
length operator"" _cm(long double v) {
return length(v, length::centimetre);
}
Binary Literals
You probably know that C++ supports the 0x
prefix to write hexadecimal literals like 0xFF
. There’s also an older, lesser-used prefix: a leading 0
followed by digits 0–7
for octal values. This still shows up, especially in filesystem-related code—seasoned Unix programmers may find chmod(path, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
no more readable than chmod(path, 0644)
.
Since C++14, binary literals are now supported using the 0b
prefix:
unsigned mask = 0b111000000;
This is useful in bit-level operations and low-level programming.
Unfortunately, the I/O stream manipulators (dec
, hex
, and oct
) still don’t include bin
, so there’s no direct way to print a number in binary like there is for decimal, hexadecimal, or octal. A workaround is to use std::bitset
, though the user has to manually specify the number of bits:
#include <bitset>
cout << bitset<9>(mask) << endl;
Output:
111000000
Digit Separators
When numbers get long, it becomes harder to read them clearly. With the introduction of binary literals, this issue becomes even more noticeable. Starting with C++14, you can insert single quotes ('
) anywhere in a numeric literal to make it more readable. How you group the digits is entirely up to you, but here are some common conventions:
- Decimal numbers grouped by 3 digits, matching English units like thousand, million, etc.
- Decimal numbers grouped by 4 digits, matching Chinese units like 万 (ten-thousand), 亿 (hundred-million).
- Hexadecimal numbers grouped by 2 or 4 digits, corresponding to bytes or double-bytes.
- Binary numbers grouped by 3 digits, useful for Unix file permission modes.
Examples:
unsigned mask = 0b111'000'000;
long r_earth_equatorial = 6'378'137;
double pi = 3.14159'26535'89793;
const unsigned magic = 0x44'42'47'4E;
default and delete Special Member Functions
In C++, certain special member functions can be automatically generated by the compiler, including:
- Default constructor
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
Each of these can be:
- Implicitly declared or user-declared
- Defaulted (i.e., automatically provided) or user-defined
- Active or deleted
These states can combine in various ways, though not all combinations are valid. For instance:
- If it’s implicitly declared, it must be defaulted.
- Only defaulted functions can be deleted.
- If it’s user-provided, it must be user-declared.
By default, if no special constraints exist (like const
or reference members), and the user doesn’t declare anything explicitly, the compiler automatically provides these member functions.
However, if you have user-declared constructors or complex member types, the situation changes:
- Uninitialized
const
or reference members can cause the compiler to delete the default constructor. - Same applies to copy/move constructors and assignment operators.
- If the user doesn’t declare a copy constructor, the compiler will implicitly declare one (not as a template).
- Same for copy assignment.
- If the user declares a move constructor or move assignment, the compiler will delete the copy operations.
- If none of the copy/move/assignment/destructor functions are declared, the compiler will generate move operations.
You don’t need to memorize all of this. Instead, focus on understanding the rationale through hands-on experience. In general, the rules are conservative to avoid unsafe defaults, particularly around move semantics.
You can override the default behavior using = default
to explicitly request a default implementation or = delete
to disable it. For example:
Without default constructor:
template <typename T>
class my_array {
public:
my_array(size_t size);
private:
T* data_{nullptr};
size_t size_{0};
};
If you want a default constructor but also have custom ones, you’d need to write it manually:
my_array() : data_(nullptr), size_(0) {}
But with default member initialization, you can now simplify:
my_array() = default;
To disable copying:
class shape_wrapper {
shape_wrapper(const shape_wrapper&) = delete;
shape_wrapper& operator=(const shape_wrapper&) = delete;
};
In pre-C++11, you’d typically declare these functions as private
to restrict copying, but = delete
provides clearer intent and better error messages.
Note: Declaring a function as = delete
is still considered a declaration—so if you delete copy functions, the compiler won’t generate move functions.
override and final Specifiers
C++11 introduced two specifiers: override
and final
. They are not keywords—just markers that apply only when placed at the end of a member function declaration.
override
Declares that this function overrides a virtual function in the base class. If it doesn’t (due to a typo or signature mismatch), the compiler will issue an error.
Benefits:
- Clearly signals your intent to override
- Helps the compiler catch mismatches early
final
Declares that:
- A virtual function cannot be overridden by derived classes
- Or a class cannot be inherited from
Usage example:
class A {
public:
virtual void foo();
virtual void bar();
void foobar(); // Not virtual
};
class B : public A {
public:
void foo() override; // OK
void bar() override final; // OK
// void foobar() override; // Error: Not a virtual function
};
class C final : public B {
public:
void foo() override; // OK
// void bar() override; // Error: `bar` is final
};
class D : public C {
// Error: Class `C` is final and cannot be inherited from
};