API design

An API is the part of a library that a caller sees: the declarations, the types and the guarantees. The compiler enforces the ones written in the type system, and a review is left for the rest.

Three attributes carry most of the weight. [[nodiscard]] makes an ignored result a warning, const makes a mutation a compile error, and noexcept tells the caller that a move cannot fail. Lesson 080 measured what noexcept does for a vector; this lesson is about the contract the header states.

The header is also where a breaking change is visible: a new virtual function, a changed parameter type, a removed default. A library that cares about its users adds and deprecates, and the compiler warnings become the migration guide.

A result that must be used

[[nodiscard]] marks a function whose return value the caller must not drop: an error code, a new object, a size. The compiler warns at the call site, and the warning names the function and the attribute.

The program calls compute and throws the result away. The build prints the warning and the program still runs, because a warning is not an error unless the project adds -Werror.

The attribute is right for a function without side effects and wrong for one that mainly does something, such as push_back. Marking too much makes the warning noise, and noise is ignored.

#include <iostream> [[nodiscard]] int compute(int value) { return value * 2; } int main() { compute(21); std::cout << "done\n"; }

g++ -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard_demo
./nodiscard_demo

nodiscard.cpp: In function 'int main()':
nodiscard.cpp:10:12: warning: ignoring return value of 'int compute(int)', declared with attribute 'nodiscard' [-Wunused-result]
10 | compute(21);
| ~~~~~~~^~~~
nodiscard.cpp:3:19: note: declared here
3 | [[nodiscard]] int compute(int value)
| ^~~~~~~
done

const on everything that reads

A member function that does not change the object is marked const, and a parameter that is only read is a reference to const. The two together let a const object be passed to any function that only looks at it.

The program renames a user through a mutator and prints the name through a function that takes const user&. The const member returns a reference to const, so the caller cannot write through it.

This is the const correctness of lesson 012 applied to an interface: every function that can be const is const, and the compiler then rejects a call through a const object that would have been a surprise.

#include <iostream> #include <string> #include <utility> class user { public: explicit user(std::string name) : name_(std::move(name)) {} const std::string& name() const { return name_; } void rename(std::string value) { name_ = std::move(value); } private: std::string name_; }; void print(const user& person) { std::cout << person.name() << '\n'; } int main() { user person{"ada"}; print(person); person.rename("grace"); print(person); }

g++ -std=c++23 -Wall -Wextra const.cpp -o const_api
./const_api

ada
grace

noexcept as a promise

noexcept says that a function does not throw. The compiler does not prove it; the promise is the author's, and a throw from inside calls std::terminate. In exchange, the type traits can see it and containers can move instead of copy.

The program asserts at compile time that square is noexcept and prints the trait for a string's move constructor. The answer is 49 and 1: the move is nothrow, which is why a vector of strings moves its elements when it grows.

Promise noexcept only for operations that cannot fail: an arithmetic function, a move of a unique_ptr, a size query. A function that allocates or parses does not qualify, and marking it anyway turns an exception into a crash.

#include <iostream> #include <string> #include <type_traits> int square(int value) noexcept { return value * value; } int main() { static_assert(noexcept(square(7))); std::cout << square(7) << ' '; std::cout << std::is_nothrow_move_constructible<std::string>::value << '\n'; }

g++ -std=c++23 -Wall -Wextra noexcept.cpp -o noexcept_api
./noexcept_api

49 1

The const object that calls a mutator

A const object can only call const member functions. Calling a mutator is a compile error about the implicit this argument, and the message says the qualifiers are discarded.

The program declares a const counter and calls increment. The compiler reports that this would lose its const qualifier, which is the rule that keeps a const reference from changing the object.

The fix is either a non-const object or a const member function, and the choice is the design question: if the function really changes the object, the object must not be const. A mutable member is the narrow exception, for a cache that does not change the logical value.

class counter { public: void increment() { ++value_; } int value() const { return value_; } private: int value_ = 0; }; int main() { const counter c; c.increment(); }

g++ -std=c++23 -Wall -Wextra mutate.cpp -o mutate

mutate.cpp: In function 'int main()':
mutate.cpp:16:16: error: passing 'const counter' as 'this' argument discards qualifiers [-fpermissive]
16 | c.increment();
| ~~~~~~~~~~~^~
mutate.cpp:4:10: note: in call to 'void counter::increment()'
4 | void increment() { ++value_; }
| ^~~~~~~~~

Exercises

  1. Add [[nodiscard]] to a function that returns a bool and check the warning at a call that ignores it.
  2. Make the user class hold a std::vector of nicknames and mark every reader const.
  3. Write a move constructor for a small class, mark it noexcept and check the trait with a static_assert.

Questions

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

  1. What does [[nodiscard]] do?

    Answer

    It warns when the caller ignores the return value

    The attribute is a compiler hint at the call site, and -Werror turns the warning into a failure.

  2. Which function should be marked const?

    Answer

    One that does not modify the object

    const is a promise about the object's state, and every function that keeps it can carry it.

  3. What does noexcept promise?

    Answer

    That the function will not let an exception escape

    A throw that leaves a noexcept function calls std::terminate, so the promise is strong.

  4. The program is built with g++ -std=c++23 -Wall -Wextra q.cpp and run. What does it print? #include <iostream> [[nodiscard]] int compute(int value) { return value * 2; } int main() { compute(21); std::cout << "done"; }

    Answer

    done

    The ignored result produces a compiler warning, and the program itself prints done.

  5. What does this program print? #include <iostream> #include <string> class user { public: explicit user(std::string name) : name_(name) {} const std::string& name() const { return name_; } void rename(std::string value) { name_ = value; } private: std::string name_; }; void print(const user& person) { std::cout << person.name() << '\n'; } int main() { user person{"ada"}; print(person); person.rename("grace"); print(person); }

    Answer

    ada / grace

    The first print reads the original name and the rename changes it before the second.

  6. What does this program print? #include <iostream> #include <string> #include <type_traits> int square(int value) noexcept { return value * value; } int main() { static_assert(noexcept(square(7))); std::cout << square(7) << ' ' << std::is_nothrow_move_constructible<std::string>::value; }

    Answer

    49 1

    The square is 49 and the string's move constructor is nothrow.

  7. Why does a vector move its elements instead of copying when it grows?

    Answer

    Because the move constructor is noexcept and the copy is not

    The strong guarantee needs a move that cannot throw, which the trait reports.

  8. What does the compiler report when a const object calls a mutator?

    Answer

    That the this argument would discard the const qualifier

    The implicit object argument of a non-const member is a non-const reference, which cannot bind to a const object.

  9. When should a function not be marked noexcept?

    Answer

    When it allocates or parses and can fail

    A promise that cannot be kept turns an exception into a call to std::terminate.

  10. What does a const member function return if the caller must not write through the result?

    Answer

    A reference to const

    The reference to const keeps the read-only guarantee after the function has returned.

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. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev