Blog 2024 11 13 Implicit string conversions to booleans
Post
Cancel

Implicit string conversions to booleans

From C++ Brain Teasers by Anders Schau Knatten, I learned about a compiler warning offered by Clang, called -Wstring-conversion. It emits a warning when a string literal is implicitly converted into a boolean.

You might even be surprised that it is even possible. And why would anyone do that?

Let’s start with the first part by explaining why such an implicit conversion is possible. A string literal is an array of const chars. Arrays can be converted into pointers, something we talked about last week when we discussed why spans are so useful. This is also called decay. Furthermore, pointers can be converted into booleans. That is how a string literal can be converted into a bool.

1
2
static_assert(!!"" == true);
static_assert(static_cast<bool>("") == true);

What might be surprising though is that even an empty string literal is converted into true. The reason is that only a nullptr would be converted into false, but an empty string literal is an array of a size of one so it’s not a nullptr. As a result, "" converted to true. The possible confusion is that the one character in that array of one is the \0 terminator. But this shouldn’t really matter. You shouldn’t use such shady implicit conversions.

We could end this article right here. But life is not ideal and I tried to turn on -Wstring-conversion in a production codebase where I found a few different cases of string literals conversions.

Fail with a message

The most frequent usage of these conversions was tied to constantly failing assertions.

1
assert(!"Invalid argument")

Considering that a string literal can be converted into true, the above assertion will always fail and the literal "Invalid argument" will appear in the logs.

The problem with the above assertion is that if you don’t know about such implicit conversions, it’s hard to understand this line of code. We can replace it in a very simple way.

1
assert(false && "Invalid argument")

With the expanded version, the false && communicates even to non-C++ developers that the assertion will always fail and it’s easier the deduce that the second part is just an error message.

Fail conditionally with a message

The second case is very similar to the previous one, just a bit more complicated. Before the negated literal, there is another condition combined with an OR.

1
assert((arg != 42) || !"Invalid argument");

This combination means that if the condition fails, then the assertion will fire. Otherwise, if the condition is true, then the whole expression is true and we can move on.

To make it more readable, we should make the same breakdown in code that we just did in words. We should extract the condition and invert it, then we can make the same transformation on the negated literal that we did previously.

1
2
3
  if (arg == 42) {
    assert(false && "Invalid argument");
  }

We end up with slightly more code, but it raises way less questions. Our code becomes more readable.

Remember, when we write production code, we don’t write it to win in a code golf where every character counts. We simply want to make the future maintainers’ lives easier while doing the right thing.

Now, let’s have a look at a third case.

Allow additional implicit conversions

The last case is plain horror. Let’s have a look at it first:

1
2
3
4
5
6
7
8
9
10
void foo(long l) {
}

void bar(const char* arg) {
    foo(arg); // this fails to compile
}

void baz(const char* arg) {
    foo(!!arg);
}

We have a function called foo that takes a long, but it can take any other integer-like parameter that we want to call with an argument of const char*. A const char* can be implicitly converted to a bool which can be implicitly converted to a long. But the compiler cannot invoke both on its own.

But if we force an implicit conversion to a bool with a negation, the compiler will take care of the other. Of course, due to the negation, we wouldn’t get the value we wanted, so we have to negate the negated value.

It will always be the same result, unless arg is nullptr. But let’s say, we don’t want to eliminate this argument, we want to keep it. How should we do it without relying on the implicit conversion to bool?

Instead of a double negation, let’s use a static_cast which is longer, but it’s very clear on its intent

1
2
3
void bar(const char* arg) {
    foo(static_cast<bool>(arg));
}

We still rely on an implicit conversion from bool to long, but that’s obviously not reported by -Wstring-conversion. If it really bothered us, we could use another static_cast. Though I guess nobody really wants that.

Conclusion

In this article, we learned about -Wstring-conversion, something I learned from C++ Brain Teasers by Anders Schau Knatten](https://www.sandordargo.com/blog/2024/10/16/cpp-brain-teasers). Clang offers this compiler warning which fires on implicit conversions from C-strings to bools.

I presented you with three different scenarios where I saw that developers relied on this kind of implicit conversion in production codebases. Two of them are related to assertions and one is about a chain of conversions, ultimately to integral numbers. None of them are difficult to get rid of.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.