When either using existing C++ libraries or creating your own, understanding the principle of least surprise (also called the principle of least astonishment) is critical to developing source code efficiently and effectively. This principle simply states that any feature that a C++ library provides should be intuitive and should operate as the developer expects. Another way of saying this is that a library's APIs should be self-documenting. Although this principle is critically important when designing libraries, it can and should be applied to all forms of software development. In this recipe, we will explore this principle in depth.
Understanding the principle of least surprise
Getting ready
As with all of the recipes in this chapter, ensure that all of the technical requirements have been met, including installing Ubuntu 18.04 or higher and running the following in a Terminal window:
> sudo apt-get install build-essential git cmake
This will ensure your operating system has the proper tools to compile and execute the examples in this recipe. Once you've done this, open a new Terminal. We will use this Terminal to download, compile, and run our examples.
How to do it...
Perform the following steps to complete this recipe:
- From a new Terminal, run the following code to download the source code:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter01
- To compile the source code, run the following code:
> mkdir build && cd build
> cmake ..
> make recipe01_examples
- Once the source code has been compiled, you can execute each example in this recipe by running the following commands:
> ./recipe01_example01
The answer is: 42
> ./recipe01_example02
The answer is: 42
> ./recipe01_example03
The answer is: 42
> ./recipe01_example04
The answer is: 42
The answer is: 42
> ./recipe01_example05
The answer is: 42
The answer is: 42
> ./recipe01_example06
The answer is: 42
The answer is: 42
> ./recipe01_example07
The answer is: 42
> ./recipe01_example08
The answer is: 42
> ./recipe01_example09
The answer is: 42
In the next section, we will step through each of these examples and explain what each example program does and how it relates to the lessons being taught in this recipe.
How it works...
As stated in the previous section, the principle of least surprise states that a library's APIs should be intuitive and self-documenting and this principle generally applies to all forms of software development and not just library design. To understand this, we'll look at some examples.
Example 1
Example 1 demonstrates the principle of least surprise as follows:
#include <iostream>
int sub(int a, int b)
{ return a + b; }
int main(void)
{
std::cout << "The answer is: " << sub(41, 1) << '\n';
return 0;
}
As shown in the preceding example, we have implemented a library API that adds two integers and returns the results. The problem is that we named the function sub, which most developers would associate with subtraction and not addition; although the API functions as designed, it breaks the principle of least surprise because the API's name is not intuitive.
Example 2
Example 2 demonstrates the principle of least surprise as follows:
#include <iostream>
void add(int a, int &b)
{ b += a; }
int main(void)
{
int a = 41, b = 1;
add(a, b);
std::cout << "The answer is: " << b << '\n';
return 0;
}
As shown in the preceding example, we have implemented the same library API that we implemented in the previous exercise; it is designed to add two numbers and return the result. The issue with this example is that the API is implementing the following:
b += a;
In this example, the principle of least surprise is being violated in two different ways:
- The add function's arguments are a and then b, even though we would write this equation as b += a, meaning that the order of the arguments is intuitively backward.
- It is not immediately obvious to the user of this API that the result would be returned in b without reading the source code.
A function's signature should document how the function will execute using semantics the user is already accustomed to, thus reducing the probability of causing the user to execute the API incorrectly.
Example 3
Example 3 demonstrates the principle of least surprise as follows:
#include <iostream>
int add(int a, int b)
{ return a + b; }
int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}
As shown in the preceding example, we're adhering to the principle of least surprise here. The API is designed to add two integers and return the result, and the API intuitively performs this action as expected.
Example 4
Example 4 demonstrates the principle of least surprise as follows:
#include <stdio.h>
#include <iostream>
int main(void)
{
printf("The answer is: %d\n", 42);
std::cout << "The answer is: " << 42 << '\n';
return 0;
}
As shown in the preceding example, another great example of the principle of least surprise is the difference between printf() and std::cout. The printf() function requires the addition of format specifiers to output integers to stdout. There are many reasons why printf() is not intuitive:
- To a beginner, the printf() function's name, which stands for print formatted, is not intuitive (or in other words, the function's name is not self-documenting). Other languages avoid this issue by picking more intuitive names for a print function, such as print() or console(), which do a better job of adhering to the principle of least surprise.
- The format specifier symbol for an integer is d. Once again, to a beginner this is unintuitive. In this specific case, d stands for decimal, which is another way of saying signed integer. A better format specifier might have been i to match the language's use of int.
Contrast this with std::cout, which stands for character output. Although this is less intuitive compared to print() or console(), it is more intuitive than printf(). Furthermore, to output an integer to stdout, the user doesn't have to memorize a table of format specifiers to complete their task. Instead, they can simply use the << operator. Then, the APIs handle formatting for you, which is not only more intuitive but also safer (especially when working with std::cin as opposed to scanf()).
Example 5
Example 5 demonstrates the principle of least surprise as follows:
#include <iostream>
int main(void)
{
auto answer = 41;
std::cout << "The answer is: " << ++answer << '\n';
std::cout << "The answer is: " << answer++ << '\n';
return 0;
}
As shown in the preceding example, the ++ operators uphold the principle of least surprise. Although a beginner would have to learn that ++ represents the increment operator, which means the variable is incremented by 1, the position of ++ with respect to the variable is quite helpful.
To understand the difference between ++variable and variable++, all the user has to do is read the code left to right as normal. When ++ is on the left, the variable is incremented and then the contents of the variable are returned. When ++ is on the right, the contents of the variable are returned and then the variable is incremented. The only issue with respect to the position of ++ is the fact that ++ on the left is generally more efficient (as the implementation doesn't require extra logic to store the value of the variable prior to the increment operation).
Example 6
Example 6 demonstrates the principle of least surprise as follows:
#include <iostream>
int add(int a, int b)
{ return a + b; }
int Sub(int a, int b)
{ return a - b; }
int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
std::cout << "The answer is: " << Sub(43, 1) << '\n';
return 0;
}
As shown in the preceding code, we have implemented two different APIs. The first adds two integers and returns the results while the second subtracts two integers and returns the results. The issue with the subtract function is two-fold:
- The addition function is in lowercase while the subtraction function is in uppercase. This is not intuitive and users of the APIs would have to learn which APIs are in lowercase and which are in uppercase.
- The C++ standard APIs are all in snake case, meaning they leverage lowercase words with the use of _ to denote a space. In general, it is better to design C++ library APIs with snake case as a beginner is more likely to find this intuitive. It should be noted that, although this is generally the case, the use of snake case is highly subjective and there are several languages that do not adhere to this guidance. The most important thing is to pick a convention and stick to it.
Once again, ensuring your APIs mimic existing semantics ensures the user can quickly and easily learn to use your APIs, while reducing the probability of the user writing your APIs incorrectly, leading to compile errors.
Example 7
Example 7 demonstrates the principle of least surprise as follows:
#include <queue>
#include <iostream>
int main(void)
{
std::queue<int> my_queue;
my_queue.emplace(42);
std::cout << "The answer is: " << my_queue.front() << '\n';
my_queue.pop();
return 0;
}
As shown in the preceding example, we are showing you how a std::queue can be used to add integers to a queue, output the queue to stdout, and remove elements from the queue. The point of this example is to highlight the fact that C++ already has a standard set of naming conventions that should be leveraged during C++ library development.
If you are designing a new library, it is helpful to the user of your library to use the same naming conventions that C++ has already defined. Doing so will lower the barrier to entry and provide a more intuitive API.
Example 8
Example 8 demonstrates the principle of least surprise as follows:
#include <iostream>
auto add(int a, int b)
{ return a + b; }
int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}
As shown in the preceding example, we are demonstrating how the use of auto, which tells the compiler to figure out what the return type of the function is automatically, does not uphold the principle of least surprise. Although auto is extremely helpful for writing generic code, its use should be avoided as much as possible when designing a library API. Specifically, for the user of the API to understand what the inputs and outputs of the API are, the user must read the API's implementation as auto does not specify the output type.
Example 9
Example 9 demonstrates the principle of least surprise as follows:
#include <iostream>
template <typename T>
T add(T a, T b)
{ return a + b; }
int main(void)
{
std::cout << "The answer is: " << add(41, 1) << '\n';
return 0;
}
As shown in the preceding example, we are demonstrating a more appropriate way to uphold the principle of least surprise while simultaneously supporting generic programming. Generic programming (also called template meta-programming or programming with C++ templates) provides the programmer with a way to create an algorithm without stating the types that are being used in the algorithm. In this case, the add function doesn't dictate the input type, allowing the user to add two values of any type (in this case, the type is called T, which can take on any type that supports the add operator). Instead of returning an auto, which would not state the output type, we return a type T. Although T is not defined here as it represents any type, it does tell the user of the API that any type we input into this function will also be returned by the function. This same logic is used heavily in the C++ standard library.