String’s competing constructors →
Help the compiler warn you
Posted on January 24, 2018	by Andrzej Krzemieński
Compiler warnings are a very useful tool for detecting bugs in your program. Because you can enable them selectively, and because you can choose to turn them into hard errors on your compiler, you can in fact build a dialect, which is a safer subset of C++. You can enable even more warning if you assist your compiler a bit and instruct it about your intentions. We will see how to do it in one case.
I have a function in my program for splitting strings. It has the following interface:
template <typename... T>
bool split(string_view s, char sep, T&... tokens);
You can use it like this:
int a, b;
double c;
char d;
string s = "1|99|0.25|X";
if (split(s, '|', a, b, c, d)) {
// use a, b, c, d
That is, first the string to split, second the separator by which to split, and then a variable number of arguments of different types taken by reference. From the number and the types of the out parameters the function determines how many tokens are expected and in what format. While parsing the input string the function can determine if the split succeeded. In a way, it does two things.
It checks if the number and the format of tokens in the string is the one expected.
If so, it populates the variables from the scope with the corresponding values.
If the string is determined not to satisfy the expectations, it is not an “error”. Someone might be calling this function only to determine if the string is in the given format — nothing more. Thus, exceptions would not work here. Since the numeric values are communicated through out parameters, we can use the return value to indicate if the split was possible or not. However, there is a danger here: you cannot use values a, b, c or a unless function split returned true. But it is obvious that in a big program, sooner or later someone will forget about it, and he will just take for granted that the split must work, and will use the variables unguarded:
split(s, '|', a, b);
return b - a;
One might be tempted to just assign the initial values to a and b in order to “avoid” the “problem” later. But that would be very unfortunate. Using values a and b only makes sense when we know we have read them from the string. If not, whether they have some initial values taken out of the blue by the programmer or whether they have unspecified values is the same: in either case we have a logic error. However, if we go with unspecified values, we increase the chances that this bug will be found statically by the compiler or a tool like static analyzer.
Now, it might look like I am questioning the C++ Core Guideline ES.20 (Always initialize an object). But the real essence of the guideline is, “assign initial value upon declaration provided that you know this value”. If you do not have your value yet, and you already need the object (and its address), just initializing any value is not going to do any good.
So, we will likely get into the problem of using values we have not read. Exceptions would have prevented that, but using them is an overkill: there is nothing exceptional about a string (probably read from some config file, or user) not being in the expected format. Also note that sometimes we do have good defaults, and we only want to depart from them if the string matches the format:
int lo = 0, hi = 100;
split(s, '|', lo, hi); // ok to fail
return lo <= val && val <= hi;
Since C++17 you do not need exceptions to prevent unchecked failures from breaking into the program. We now have attribute [[nodiscard]]. We can use it to annotate our function:
[[nodiscard]] bool split(string_view s, char sep, T&... tokens);
Not reading the value returned by this function results in a compiler warning. And if you turn warnings into errors: in an error. The user has to respond to this. For instance, with an if-statement:
throw bad_configuration{};
Now the user is throwing an exception. This makes sense. It is the user who judges whether the situation is exceptional or not — not the implementer of function split. But the warning will now also appear in the cases where we consciously decided not to check the result:
Luckily, they usually go in pairs: a warning, and a construct to disable the warning. In our case, the idiomatic way to disable the warning is the cast to void:
(void)split(s, '|', lo, hi);
C++17 is quite new, but the attribute itself has been around for a while longer. In MSVC it is available since version 2017.3. In Clang it is available since version 3.9 (not restricted to C++17 mode); in GCC, since version 7 (not restricted to C++17 mode). In earlier versions of GCC (and therefore also in Clang) we have a compiler-specific alternative: attribute warn_unused_result:
__attribute__((warn_unused_result))
The only (sometimes annoying) difference is that you cannot silence this one with the cast to void, you need to read it. As per the explanation in bug report, attribute warn_unused_result is treated very strictly. No one should ignore the value, even consciously. You would use it for functions like malloc.