Linus: bool is dangerous in C if you don't understand it
lkml.org
lkml.org
typedef char bool;
and then maybe they use it like so: bool found_it = strstr(haystack, needle);
This has a serious bug that will only manifest 0.4% of the time. The problem is that strstr returns a pointer, and converting a pointer to a smaller integer type throws away the high bytes. If the resulting pointer is not NULL but happens to have a zero low byte, this bool will be false, even though the string was found.Even if that doesn't happen, you'll often see code like this:
#define true 1
...
if (found_it == true) ...
which is wrong as well.bool addresses these. Conversions to bool always result in values of 0 or 1, so both of the above problems are avoided.
That said, I agree with Linus. It's not well understood, and using bool in a library header may conflict with another definition of bool in the project. Also, compilers typically warn about at least the first error above. As a C99 feature, bool is too little, too late; had it been part of C89, things might be different.
I mean sure, there are warnings and there are warnings. Absurdist pedantry about warning states isn't something I tend to worry about.
But if you're dealing with a code base that is building to completion without attention to this warning (which pops out with the default flags on gcc 4.7.2 -- no -Wall needed!), then you have more serious problems than can be fixed by a builtin pointer-to-bool conversion.
I mean, this example is a little contrived as it is. The "normal" way to treat a pointer as a boolean and branch off of it is just "if(ptr)", and that's worked without trouble on all compilers for 40 years now.
Is C sort of a mess? Yeah. But this isn't a particularly illustrative example IMHO. All languages have this nonsense (c.f. the Ruby/JS "Wat" video).
A lot of people forget this, and think that NULL pointers must actually have a bit pattern that is all zeros.
You might as well be worrying about code compatibility with systems that have a 7 bit char, or some other irrelevance.
#include <iostream>
struct Foo
{
int bar;
};
int main()
{
int (Foo::*null_pointer) = nullptr;
int (Foo::*first_member_pointer) = &Foo::bar;
std::cout << *(std::ptrdiff_t*)&null_pointer << "\n";
std::cout << *(std::ptrdiff_t*)&first_member_pointer << "\n";
}
Running this code after compiling it with VS2012 results in, on my machine: -1
0 An integer constant expression with the value 0, or
such an expression cast to type void *, is called a
null pointer constant. If a null pointer constant
is converted to a pointer type, the resulting pointer,
called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
Conversion of a null pointer to another pointer type
yields a null pointer of that type. Any two null pointers
shall compare equal.
If you do this: int pv = 0;
char * p = (char *)pv;
you are not guaranteed to have p set to a null pointer, because the zero you are casting and assigning is not an integer constant zero. The behavior is implementation-defined, as is going the other way. Section 6.3.2.3 paragraphs 5 and 6: An integer may be converted to any pointer type. Except
as previously specified, the result is implementation-defined,
might not be correctly aligned, might not point to an entity
of the referenced type, and might be a trap representation.
Any pointer type may be converted to an integer type. Except
as previously specified, the result is implementation-defined.
If the result cannot be represented in the integer type, the
behavior is undefined. The result need not be in the range of
values of any integer type. char *p1 = 0;
int pv = (intptr_t)p1;
char *p2 = (char *)pv;
I.e. NULL must round-trip through intptr_t.Is that the kind of knowledge/insight you gain from understanding C when studying it or when you are bitten by a bug ?
But that's not universal, there are people that like to know the ins and outs of the language before using it... and yes, they are the ones doing the right thing.
I'm failing to understand this line. Can you be more explicit why it's bad now, and would have been good earlier?
bool as a type in C strikes me as dubious. In C, you need to care about the underlying representation of things. I'm not even sure it was a good idea to call a char a char, because it's actually a small integer. Calling it a char is just misleading; it's signed, and a literal character is not even a char. It's a source of bugs. "bool" sounds potentially worse.
bool found_it = !!strstr(haystack, needle);bool is a bad thing in C and C++, because of the implicit conversion rules. In C++, pointers implicitly convert to bool in every context-- there is never any warning message. This effectively reduces the compiler's ability to typecheck your program, since so many variables are pointers already, and C++ will happily stuff them into any bool argument to a function.
It seems like the C99 _Bool type implements the same implicit type conversion brain damage. That makes it a step backwards in terms of type safety, not forwards. That's right, using the old-fashioned, fuddy-duddy, plain old int and 0 and 1 gives you better type checking than the shiny new C++ feature.
As Linux points out, the worse typechecking comes with a side order of compatibility problems. And it is not any more efficient or readable.
It does: "When any scalar value is converted to _Bool, the result is 0 if the value compares equal to 0; otherwise, the result is 1." (from §6.3.1.2 of the N1256 draft)
Compilers will happily compare/set pointers to 0 since that's the null pointer constant (§6.5.9, §6.3.2.3), so there's no warning:
% cat foo.c
#include <stdio.h>
int main() { _Bool b = "blah"; printf("%d\n", b); return 0; }
% gcc -std=c99 -W -Wall -Wextra -o foo foo.c
% ./foo
1(And of course if the value passed happens to be an int rather than a pointer, typedefing bool to int won't save you!)
edit: And as mentioned in the rest of the thread, while it's unjustifiably confusing to, say, pass 'p' as a boolean parameter rather than 'p != NULL', it's not unreasonable to do something like
typedef int bool;
bool foo_enabled;
void set_foo_enabled(bool enabled) {
if(enabled == foo_enabled) return;
/* new value is different, do some work */
}
set_foo_enabled(1);
...
set_foo_enabled(flags & ENABLE_FOO);
Though I haven't come across this kind of bug in practice either.What in C is NOT dangerous if you don't understand it?
Now, this is not the same thing defining what exactly the truth test is. It's perfectly reasonable and arguably a good design to have both a narrow boolean type and a wide definition of truthiness.
If you treat it this way you remove the vast majority of the problems with most C or C++ BOOL implementations.
The low bytes of a pointer being 0 (It can happen on Windows with VirtualAlloc and company) can screw up the works, so avoid assigning a pointer to a BOOL and use BOOL b = !!pointer if you have to break that rule.
But some semantic maps are less great. Having your compiler help you to ensure you're not implicitly mixing up bad maps can be helpful.
std::bitset<N> is quite nice though, as long as you have a fixed size.
bool is also subject to integer promotion so when you pass a bool to a function or do some integer arithmetic it can become an int.
From the text K&R, C Programming Language, 2nd Ed. p. 174
A.6.1 Integral Promotion
A character, a short integer, or an integer bit-field, all either signed or not, or an object of enumeration type, may be used in an expression wherever an integer may be used. If an int can represent all the values of the original type, then the value is converted to int; otherwise the value is converted to unsigned int. This process is called integral promotion.
On the other hand, I'm happy if my C code compiles, so I shouldn't throw any stones here.
Ever worked on a big code base where some programmers were in Vietnam, some in Russia, some in China and most here in the US? Conventions are different and pride and communications issues make it hard to correct behavior.
Ever worked as the commit point for an offshore group? Code review?
Didn't think so...
Basically the true idiot is not one that makes mistakes, but one that assumes only idiots make mistakes.
the mistakes come from pre-C99 code that calls things boolean, but has unexpected dependencies (that _Bool removes) on bit-level structure.
the more i learn from this thread, the more i want to use _Bool. which worries me, because Linus isn't dumb. but i think(?) the point he is making is related more to legacy code that is "broken but works". or maybe to programmers that rely on/use/exploit bit-structure/implementation details in "bool" types because they don't have experience with more strictly typed languages. [edit: or as mbell said in a reply to me elsewhere, because at os level you often do need to care about bits]
(One issue with "native" bools, that others have pointed out, is that it's possible to introduce bugs due to implicit conversions. But I've personally found this behaviour usually to be what you want, and I don't remember having to fix any bugs caused by it, suggesting that they can't have been all that difficult to sort out.)
His point about casting to fake bool types is well made, and I wonder how many instances of this I will now spot? (gcc appears to issue perfectly fine warnings for this case, though.) Another argument for using the built-in types, in my view. Or for switching to "typedef uintptr_t bool".
The bool type prevents a lot of kinds of mistakes, but opens your code to a lot of new types. The problem is that if you have a lot of experience in not using it, you already learned how to avoid those first mistakes, but all that experience is useless for dealing with the newly introduced ones.
My opinion is that the bool is a good construct. It's a good idea to use it on new code, but first you must understand it, otherwise you won't get a fighting chance.
We need to develop one universal standard... :)
Maybe there is some subtly there I am missing but I do not see it.
http://pubs.opengroup.org/onlinepubs/007904875/basedefs/stdb...
It's interesting though... does anyone know of specific cases of the problems he's refering to?
Prior to C99, assuming you use the mentioned typedef for bool:
bool a = someInt & 0x02
'a' will be 0x02 if the bit is set.In C99, bool is aliased to _Bool and if the flag is set, the above code will result in 'a' being 0x01 because of C99's requirements for type conversion.
To accurately get the same behavior prior to C99, you can add !!, e.g.:
bool a = !!(someInt & 0x02) //'a' is now 0x01 when the bit is set.If you had written
bool a = (someInt & 0x02) == 0x02
or something similarly clear and unambiguous, nothing odd would happen, even in C.(Edit: OK, that's not strictly true, because of the operator precedence order. It's never made sense to me that integer arithmetic operators have higher precedence than comparisons but bitwise logical operators have lower precedence, so if you remove the parentheses above then the resulting code doesn't do what you'd expect. I suppose this is because I'm looking at the problem as if comparison operators return a proper boolean value rather than an integer, and the ordering we've wound up with in C dates from a historical oddity about 40 years ago.)
The underlying problem with booleans in C99, as Linus and others have been saying, is that the language doesn't actually enforce basic type safety, so cases like your first example
bool a = someInt & 0x02
that should result in a type error are allowed through, and with odd results: how does it make any sense for a boolean variable to have an integer value like 0x01 or 0x02?Then programmers who relied on such odd results wind up writing horrific code like your second example
bool a = !!(someInt & 0x02)
where fudge factors build on top of distortions to make the old hacks work.And then we wonder why in 2013 we still have widely used, essential software that is riddled with security flaws and crash bugs. :-(
if (x == 1 & y == 2) {
..and have it do what you meant. This became a bit of a wart when the && and || operators were introduced (still well before ANSI standardisation), but it was considered that changing it would have broken too much existing code. bool a = someInt & 0x02;
is perfectly fine in C99. 0 converts to false, non-zero converts to 1 when assigning to a _Bool.What's not fine is people creating their own compatibility booleans where they define true as 1, as that would indeed break(rather odd..) code such as
bool a = someInt & 0x02;
if (a == true)
If the bool above is not the C99 _Bool, but just a typedef to another integer type, you end up with if(0x02 == 1) evaluating to false.What's horrific about that? Would it be better if we had:
#define to_bool(_X) !!(_X)
...
bool a = to_bool(someInt & 0x02);if you rely on something called "bool" being 0x02 you're going to have a bad time. that's hardly C99's fault.
your last line of code is what i would write, effectively, if i needed to compare booleans. it seems to me that _Bool is an improvement because, pre-C99, if i forgot the !! dance somewhere, i likely had a bug. with _Bool things just work.
(disclaimer, as with other reply here - still trying to get a grasp on this, so may be saying something stupid myself).
It's also worth considering in context that a lot of the code which will run into problems with these small differences is low level OS/driver code that often deals with a lot of bit flags and bit manipulation in general. When your trying to fit a web server into 3800 _bytes_ of ram on an 8 bit microcontroller, 'doing dumb things' becomes 'being inventive'.
bool a = value & (1 << 5)
a will be 1 or 0, not 1 << 5. You don't get this behavior with a normal int.
MSVC also has a warning about some of this behavior [1], with a nonsense performance subtext. I don't think theres a GCC equivalent.In any sane language you can't redefine bool that way, nobody would ever expect bool to take more than two values, and there wouldn't be a problem.
For what he does there is NO better language than C.
So what would be a "better language"? Haskell? In what way would it be better -- since it wouldn't be better for the tasks he wants?
Abstract better?
Sorry, I don't believe in that.
Linus used C for all of those, but was it because C was technically the best choice in each case, or because it was good enough to get the job done and because it's the language he's most comfortable with?
Well, I mean...
SLOC Directory SLOC-by-Language (Sorted)
100691 top_dir ansic=80140,perl=10458,sh=7523,python=2570
98482 t sh=97926,perl=546,ansic=10
38293 builtin ansic=38293
22256 contrib sh=9888,perl=5838,python=3130,lisp=1786,ansic=1449,
php=120,csh=45
18056 compat ansic=18004,perl=52
13754 git-gui tcl=10299,sh=3455
10859 gitk-git tcl=10745,sh=114
6225 gitweb perl=6225
5400 perl perl=5400
2350 xdiff ansic=2350
1288 vcs-svn ansic=1288
689 git_remote_helpers python=689
292 Documentation perl=155,sh=137
266 templates sh=266
203 block-sha1 ansic=203
173 ppc asm=98,ansic=75
0 mergetools (none)
0 po (none)
Totals grouped by language (dominant language first):
ansic: 141812 (44.42%)
sh: 119309 (37.37%)
perl: 28674 (8.98%)
tcl: 21044 (6.59%)
python: 6389 (2.00%)
lisp: 1786 (0.56%)
php: 120 (0.04%)
asm: 98 (0.03%)
csh: 45 (0.01%)
Git's design is such that different parts of it can be written in different languages with no hassle. The majority is in C, but typically new features are prototyped out in other languages first until it becomes clear that speed will be important (and it generally will, since a major usage pattern of git is scripts and other commands calling your command many times in a row. Does git-add need to be fast if you're just doing 'git add ...'? Perhaps not. Does it need to be fast if my fancy-smancy script is wailing on it several thousand times? Yeah; particularly it needs to have a fast startup time. Does git-difftool need to be in C? Probably not, which is probably why it is still Perl instead. Test-cases? No reason in the world for them to be in C, so they're in sh.)Subsurface though probably could be written in a different language and not feel any slower.
Sure -- since he also wrote those himself, so a good requisite would be "language Linus can quickly write shit in".
And for git there are other reasons too: portable, fast, lots of people can hack in C, etc.
Linux implements some C++ features like the `virtual`.
The TLDR is very alike his TLDR for bool. C++ is too complex, it's hard to know about everything it's doing behind the scenes, and he wants total control of the code on the kernel.
I didn't make the original comment.
Linus says: If "bool" had real advantages (like having a dense array representation, for example).
But doesn't bool have the advantage of reducing perceived complexity of the code and making the code more understandable? If the function returns int, one might assume it is some number, and he would have to use documentation or look at code samples to find out the int is only being compared to 1 or 0. Type bool instantly tells there's some kind of check or flag that is returned, and makes it undoubtedly easier to tell what's going on.
As for casting rules other mentioned, doesn't the C compiler warn about anything converting to anything that can store less information than what it converts from?
That's why C99 bool typecasting problems are so subtle. If you were previously using typedef char BOOL and you switch to the "real" bool, you don't get compiler warnings about suspicious casting anymore.
If bool had been always been part of the standard, people would be aware of this issue. But it wasn't, and so the C world is full of BOOL typedefs that can be chars, ints or whatever, and it's easy for programmers to implicitly assume that the "real" bool behaves like the typedefs they're used to.
You cannot reduce "perceived complexity" by adding subtle error cases, i.e actual complexity.
If you're starting a new codebase and don't have to care for legacy compilers, use it. Otherwise, it's a case by case decision.
I agree that you should never typedef a type different than _Bool to bool if you want to avoid a world of hurt, and I'd argue that using true and false in a C codebase is harmful as well.
And that stuff was standardized by a committee? Wow.
bool helps statically analyzers better as well.
And that powers 99% of the software that matters.
Name me one language that you cannot say similar BS about some aspect of it.