That’s pretty much all of us IMO. We need to get stuff done, and if there’s a feature available that lets us do it quicklier, we use it. We take the path of least resistance—a warning saying “don’t” provides no actual resistance.
That’s why I like languages like Haskell that by default prevent me from doing anything I’m probably going to regret later; if I want to break referential transparency, type safety, memory safety, &c. then I can always opt in explicitly and grep for it later when it hits the fan.
That said, I do prefer C over C++—really, the only things I miss are destructors (local automatic memory management is nice) and templates (generic/type-level programming is nice), but they’re not critical for the kinds of stuff I use C for anyway.
head []
(Not that I disagree with what you want, just that even Haskell has made some mistakes :-P) ∀a. [a] → a
Viewed as a logical formula, it’s not a tautology, so I know it must be partial. Proof: Given: L(a) = μr. 1 + (a × r)
∀a. L(a) → a
= [definition of L]
∀a. (μr. 1 + (a × r)) → a
= [unroll μ]
∀a. (1 + (a × μr. 1 + (a × r))) → a
= [∀xyz. (x + y) → z ⇔ (x → z) × (y → z)]
∀a. (1 → a) × ((a × μr. 1 + (a × r)) → a)
= [(∀a. 1 → a) = 0]
∀a. 0 × ((a × μr. 1 + (a × r)) → a)
= [∀x. 0 × x = 0]
∀a. 0
=
0
Unfortunately, this is also false if a is false (void/bottom), and all those “→ a”s are actually “→ ¬¬a” because Haskell isn’t a total language—with that pesky intuitionistic double-negative, any function can be partial. But it’s a good rule of thumb for quickly checking whether a function must not return for some (or all) inputs.What needs to be happening, to avoid crashes and get correct functioning:
1) virtual method tables need to be allocated (otherwise you might be getting a function pointer from unallocated memory and calling it. Good luck surviving that. Bugs like this have happened in both kernel and GNOME).
1b) Pointers to the virtual method tables need to be correctly set upon every allocation of the struct that contains them. So you lose the ability to create an instance without constructing it. (again plenty of bugs)
2) every level of the inheritance hierarchy need to fill in the function pointers in the correct order (You can't count the number of bugs of this type in GNOME).
3) the pointers for same functions need to be at the same memory location for every object (and the corollary. Function pointers for different functions cannot, at any point of the inheritance tree, have the same location). This leads directly to the kernel datastructures, where everybody is utterly terrified of changing anything or even reordering fields. And sadly, that fear is there for good reason.
4) 1, 2 and 3 need to be redone (best from scratch) any time the inheritance hierarchy changes. And of course, need to be done correctly, so you really ought to erase the whole thing in the whole inheritance (and somehow tell out-of-tree developers to do the changes on their end), but in C nobody does this because of the amount of work involved. Then ... bugs happen.
5) May God help you if you modprobe a module across one of these changes without recompiling it. In other words, any change to the inheritance hierarchy risks making out-of-tree modules deathtraps (such changes require modifying the source to the out-of-tree module and recompiling)
In C++
a) put "virtual" in front of the function you want to work across the inheritance hierarchy
b) (optional) don't remove or reorder virtual functions in versions where you want to maintain binary compatibility (caveat: not in the functions themselves, and of course, not in any data structures they might use) (in short: anything out-of-tree needs to be recompiled)
Given that KDE maintains binary compatibility across major versions b) is not optional within that project (with a bit of a cleanup at avery major version). Except for DCOP. But if you recompile from scratch for every deployment like every large C++ shop, you can just outright ignore it.
The result of this is that the lookup tables can be compiled into programs. This works ridiculously fast. A virtual function call in C++ is one indirection. Not even one extra instruction (just a much more expensive one than the one you'd ordinarily use).
In Java, if I remember the last time I checked it was ~20.000 instructions (but can be compiled out by the JIT compiler ... eventually. Then it's still ~1000). And I assure you Java is a lot more efficient at this than any scripting language.
This is the usual difference between C and C++. In C++ you get all the advantages that lots of manual work gets you in C, at the same runtime cost.
The criticism is that if you give inexperienced programmers lots of high level tools, they will quickly use it to blindly generate programs that are 20G+ of machine code. And ... well that's true. Fixing it can be pretty hard.
The arguments of people like Linus are essentially that it's a good thing that people go through and redo the low-level stuff regularly. It's a lot of work but at times you find problems and inefficiencies. I do agree with that, but sadly, I find it pretty hard to assemble a 2K+ member team that I don't have to pay. So work that programmers don't have to do is a win for me.
The hard part is to select which features of C++ to use and more importantly which NOT to use and to ENFORCE these rules.
Maybe I've just not kept up properly, but I find it surprising that there isn't a tool which can enforce this already. It seems so obviously desirable.
I guess clang-tidy + custom "checks" + CI server enforcement could achieve something similar, but I'm just surprised that it doesn't exist.
(Of course, many of the useful metaprogramming libraries for C++, e.g. Boost::Hana, use a lot of weird features of C++ to achieve their goal of being simpler to use for clients of the library. I suppose it might be possible to limit the warnings to "self-implemented" functionality, but it might be tricky...)