C++23 features

C++23 is a smaller release than C++20, and its additions are the kind that remove a small annoyance from every file: a return type that carries an error without exceptions, a print function with a format string, and a member function that deduces the type of its own object.

The library side is available in this compiler and standard library. The features on this page are the ones that change ordinary code; the rest, from std::flat_map to multidimensional subscript, are one line each in the reference.

The additions of C++23 are smaller than the ones of C++20, which is the normal rhythm of the standard: a large release is followed by a small one, and a library that targets both guards each feature with its macro.

The expected is also a good return type for the storage layer of the task tracker, where a bad line is a value and not a reason to stop the program. The type says so in the signature.

std::expected

std::expected<int, std::string> holds either an int or a string that explains why there is none. The caller checks has_value() or calls value() and error(), and no exception is involved.

The program parses two strings and prints 5 for the good one and the message for the empty one. The type is the contract: a function that returns expected forces the caller to deal with both cases.

The alternative is a pair of a value and a bool, or an out parameter and a return code. The expected carries the same two things with a type that says which is which, and it works with and_then and transform in the monadic style.

#include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(const std::string& text) { if (text.empty()) { return std::unexpected{"empty input"}; } return static_cast<int>(text.size()); } int main() { auto good = parse("hello"); auto bad = parse(""); std::cout << good.value() << ' '; std::cout << bad.error() << '\n'; }

g++ -std=c++23 -Wall -Wextra expected.cpp -o expected_demo
./expected_demo

5 empty input

std::print

std::print writes a format string with {} placeholders, and std::println adds the newline. The format syntax is the one from <format>, so {:04} pads a number to four digits with zeros.

The program prints 42 and 0007 on one line and a marker on the next. There is no << chain and no manipulator, and the type of each argument decides how it is formatted.

The function writes to stdout, and std::print(stderr, ...) writes to another stream. A project that already uses iostream can adopt it one call at a time, because both write to the same file descriptor.

#include <print> int main() { int value = 42; std::print("value {} {:04}\n", value, 7); std::print("done\n"); }

g++ -std=c++23 -Wall -Wextra print.cpp -o print_demo
./print_demo

value 42 0007
done

Deducing this

A member function can declare its object as an explicit parameter, this const widget& self or this widget&& self. The compiler deduces the value category, and one template can cover const, non-const, lvalue and rvalue objects.

The program calls describe on a named object and on a temporary and gets two different lines. Without the feature the same code needs two overloads with ref-qualifiers, or a const and a non-const version.

The explicit object parameter is also the way to write a recursive lambda and to deduplicate the const and non-const overloads of a container's accessor. It is a C++23 feature, and a compiler without it reports the parameter as a syntax error.

#include <iostream> #include <string> struct widget { std::string name; void describe(this const widget& self) { std::cout << "lvalue " << self.name << '\n'; } void describe(this widget&& self) { std::cout << "rvalue " << self.name << '\n'; } }; int main() { widget first{"first"}; first.describe(); widget{"second"}.describe(); }

g++ -std=c++23 -Wall -Wextra this.cpp -o deducing_this
./deducing_this

lvalue first
rvalue second

value() on an error

value() on an expected that holds an error throws std::bad_expected_access<E>, where E is the error type. The exception carries a copy of the error, which the catch can read with error().

The program asks for the value of a failed parse inside a try block. The catch prints the message, and the output is caught: negative.

The fix is to check has_value() before the call, or to use value_or for a default. Calling value() without the check is the same decision as dereferencing a pointer without a null test, with an exception instead of undefined behaviour.

#include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(int value) { if (value < 0) { return std::unexpected{"negative"}; } return value; } int main() { auto result = parse(-1); try { std::cout << result.value() << '\n'; } catch (const std::bad_expected_access<std::string>& error) { std::cout << "caught: " << error.error() << '\n'; } }

g++ -std=c++23 -Wall -Wextra access.cpp -o expected_access
./expected_access

caught: negative

Exercises

  1. Change parse to return std::expected and print a division result.
  2. Print a table with std::print using two widths and check the alignment.
  3. Add a const overload of a member with deducing this and call it on a const object.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does std::expected<int, std::string> hold?

    Answer

    Either an int or a string that explains the error

    The two template arguments are the value type and the error type, and exactly one of them is stored.

  2. What does std::print use for its placeholders?

    Answer

    The format syntax of <format>

    The braces and the format specifiers are the same language as std::format.

  3. What does this const widget& self declare?

    Answer

    An explicit object parameter whose type is deduced from the call

    Deducing this makes the object an ordinary parameter, so the value category is part of the deduction.

  4. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(const std::string& text) { if (text.empty()) return std::unexpected{"empty input"}; return static_cast<int>(text.size()); } int main() { std::cout << parse("hello").value() << ' ' << parse("").error(); }

    Answer

    5 empty input

    The good parse returns the length and the empty one returns the message.

  5. What does this program print? #include <print> int main() { int value = 42; std::print("value {} {:04}\n", value, 7); std::println("done"); }

    Answer

    value 42 0007 / done

    The first placeholder takes 42 and the second pads 7 to four digits; println adds the newline.

  6. What does this program print? #include <iostream> #include <string> struct widget { std::string name; void describe(this const widget& self) { std::cout << "lvalue " << self.name << '\n'; } void describe(this widget&& self) { std::cout << "rvalue " << self.name << '\n'; } }; int main() { widget first{"first"}; first.describe(); widget{"second"}.describe(); }

    Answer

    lvalue first / rvalue second

    The named object is an lvalue and the temporary is an rvalue, and each picks its overload.

  7. What does value() throw on an expected that holds an error?

    Answer

    std::bad_expected_access<E>

    The exception type is templated on the error type and carries a copy of it.

  8. What does this program print? #include <expected> #include <iostream> #include <string> std::expected<int, std::string> parse(int value) { if (value < 0) return std::unexpected{"negative"}; return value; } int main() { auto result = parse(-1); try { std::cout << result.value(); } catch (const std::bad_expected_access<std::string>& error) { std::cout << "caught: " << error.error(); } }

    Answer

    caught: negative

    value() on the error throws, and the catch reads the stored message.

  9. Which is the safer way to read a possibly-failed expected?

    Answer

    Check has_value() before value(), or use value_or

    The check is the same discipline as a null test; the exception is the fallback, not the plan.

  10. What does std::println add to std::print?

    Answer

    A newline after the formatted text

    The two functions share the format syntax, and println appends the line break.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image