C++ 17 is a few years old now, and most compilers have caught up with the standard and have it implemented. Today I want to share three new features that I found very useful when modernizing a legacy C++ codebase.
These examples can run with an updated compiler. For GCC, use the flag -std=c++17 when compiling to be able to use these new features.
When a function returns some value, and this value should not be ignored, use this attribute to generate an API violation. I found this very helpful mainly in two scenarios:
It’s very useful for catching common mistakes. If a return value marked with this attribute is discarded, the compiler will let you know. For example:
// The function returns an error code [[nodiscard]] int do_something() { return -1; } int main() { // Calling and not checking the return value will cause a // compiler warning or error (depends on the compiler’s settings) do_something(); // This will work fine int result = do_something(); if (result == -1) { // Handle error } }
std::optional is a container of an optional value, i.e the value may or may not be present.
Let’s say you want a function that takes a string, parses it, and returns a number. If the string is not valid, return nothing. Now you can write it like this:
#include#include std::optional parse(const std::string& string) { // Returns an empty value return {}; } int main() { std::string input; std::cin >> input; std::optional parsed_number = parse(input); if (parsed_number.has_value()) // Or just if (parsed_number) { // Access the value using .value() std::cout << parsed_number.value() << std::endl; } else { // Calling parsed_number.value() will throw, because its empty } }
std::variant is a way to make type-safe union types. An instance of the variable will only hold one of the types at a time. The memory occupied by the variable will be the size of its biggest type, plus a tag to store which type is currently instantiated (not accounting for any padding required).
I found this very useful for defining the type of messages in a system, required for an implementation of the command pattern. For example, the commands for a robot could be something like this:
#include#include // Defines the possible commands struct CommandMove { int distance; }; struct CommandTurn { float degrees; }; struct CommandPickUp{ }; struct CommandDrop{ }; // robot_command can be any of the previous defined commands typedef std::variant robot_command; // This function will be the command executor void do_command(robot_command command) { // We need to define a struct with overloads for each command type struct command_visitor { void operator()(CommandMove& command_move) { std::cout << "command_move\n"; } void operator()(CommandTurn& command_turn) { std::cout << "command_turn\n"; } void operator()(CommandPickUp& command_turn) { std::cout << "command_pickup\n"; } void operator()(CommandDrop& command_turn) { std::cout << "command_drop\n"; } }; // And then call it using the std::visit std::visit(command_visitor{}, command); } int main() { // Call the executor like so do_command(CommandPickUp()); }
C++ has been adding a lot of new features over the last few years, and it's not easy to follow everything that changes. I hope this post helped you learn something new that can make your programs easier to write and read. Until next time!
Featured Image by Furbee on Unsplash.