7 ms·
I'll expand my point to be clearer. In C there is no operator overloading, so an expression like `a += 1` is easy to understand as incrementing a numeric value
by jmillikin 2y ago
I'll expand my point to be clearer.
In C there is no operator overloading, so an expression like `a += 1` is easy to understand as incrementing a numeric value by 1, where that value's type is one of a small set of built-in types.
You'd need to look further up in the function (and maybe chase down some typedefs) to see what that type is, but the set of possible types generally boils down to "signed int, unsigned int, float, pointer". Each of those types has well-defined rules for what `+= 1` means.
That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function. Or going the other direction, when you're in a GDB prompt and you disassemble the current EIP and you see `ADD EAX,1` then you can pretty much just look at the C code and figure out where you are.
---
Neither of those is true in C++. The combination of completely ad-hoc operator overloading, function overloading, and implicit type conversion via constructors means that it can be really difficult to map between the original source and the machine code.
You'll have a core dump where EIP is somewhere in the middle of a function like this:
std::string some_fn() {
some_ns::unsigned<int> a = 1;
helper_fn(a, "hello");
a += 1;
return true;
}
and the disassembly is just dozens of function calls for no reason you can discern, and you're staring at the return type of `std::string` and the returned value of `true`, and in that moment you'll long for the happy days when undefined behavior on signed integer overflow was the worst you had to worry about.
- eru 2y agoI heartily agree that C++ is a lot more annoying here than C, yes. I'm just saying that C is already plenty annoying enough by itself, thanks eg to undefined behaviour. > That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function. Or going the other direction, when you're in a GDB prompt and you disassemble the current EIP and you see `ADD EAX,1` then you can pretty much just look at the C code and figure out where you are. No, there's no guarantee of that. C compilers are allowed to do all kinds of interesting things. However you are often right enough in practice, especially if you run with -O0, ie turn off the optimiser. See eg https://godbolt.org/z/YY69Ezxnv https://godbolt.org/z/YY69Ezxnv and tell me where the ADD instruction shows up in the compiler output.
- MaulingMonkey 2y agoerror: could not convert 'true' from 'bool' to 'std::string' {aka 'std::__cxx11::basic_string<char>'} I don't think anyone's claiming C nor C++'s dumpster fires have signed integer overflow at the top of the pile of problems, but when the optimizer starts deleting security or bounds checks and other fine things - because of signed integer overflow, or one of the million other causes of undefined behavior - I will pray for something as straightforward as a core dump, no matter where EIP has gone. Signed integer overflow UB is the kind of UB that has a nasty habit of causing subtle heisenbugfuckery when triggered. The kind you might, hopefully, make shallow with ubsan and good test suite coverage. In other words, the kind you won't make shallow.
- jmillikin 2y agoFor context, I did not pick that type signature at random. It was in actual code that was shipping to customers. If I remember correctly there was some sort of bool -> int -> char -> std::string path via `operator()` conversions and constructors that allowed it to compile, though I can't remember what the value was (probably "\x01"). --- My experience with the C/C++ optimizer is that it's fairly timid, and only misbehaves when the input code is really bad. Pretty much all of the (many, many) bugs I've encountered and/or written in C would have also existed if I'd written directly in assembly. I know there are libraries out there with build instructions like "compile with -O0 or the results will be wrong", but aside from the Linux kernel I've never encountered developers who put the blame on the compiler.
- MaulingMonkey 2y ago> but aside from the Linux kernel I've never encountered developers who put the blame on the compiler. I encounter them frequently. 99.99% of the time it's undefined behavior and they're "wrong". Frequently novices who have been failed by their teachers and documentation (see previous rant using atoi as an example of the poor quality of documentation about UB: https://news.ycombinator.com/item?id=14861917 https://news.ycombinator.com/item?id=14861917 .) Less frequently, it's experienced devs half joking out of a need for catharsis. Rarely, experienced devs finally getting to the end of their rope, and are finally beginning to seriously consider if they've got a codegen bug. They don't, but they're considering it. They know they were wrong the last 10 times they considered it, but they're considering it again damnit! The linux kernel devs aren't quite unique in "just because you can, doesn't mean you should"ing their way into blaming the compiler for what could be argued to be defects in the standard or fundamental design of the language (the defect being making UB so common), but that's probably among the rarest slice of the pie of people blaming the compiler for UB. Few have the will to tilt at that windmill and voice their opinions when the compiler devs can easily just blame the standard - better to keep such unproductive rants close to heart instead, or switch to another language. Something actually productive. 0.01% of the time, it's a legitimate codegen bug on well-defined behavior code. Last one I tracked down to a bug tracker, was MSVC miscompiling 4x4 matrix multiplications by failing to spill a 17th value to stack when it only had 16 SSE register to work with. Caught by unit tests, but not by CI, since people updated compiler versions at their own random pace, and who runs `math_tests` on their personal machines when they're not touching `math`?
- gpderetta 2y agoCounterexample: https://godbolt.org/z/z3jqjPT6o https://godbolt.org/z/z3jqjPT6o
- jmillikin 2y agoI'm not sure that's a counter-example -- what assembly do you think should be emitted for floating-point math on an AVR microcontroller?
- gpderetta 2y agoIt means that "a += 1` is easy to understand as incrementing a numeric value by 1" is not true and instead "it can be really difficult to map between the original source and the machine code". More examples of non-trivial mapping from C code to generated code: https://godbolt.org/z/jab6vh6dM https://godbolt.org/z/jab6vh6dM
- jmillikin 2y agoAll of those look pretty straightforward to me -- again, what assembly would you expect to be emitted in those cases? For contrast, here's the assembly generated for Haskell for integer addition: https://godbolt.org/z/vdeMKMETT https://godbolt.org/z/vdeMKMETT And here's assembly for C++: https://godbolt.org/z/dedcof9x5 https://godbolt.org/z/dedcof9x5
- gpderetta 2y ago> All of those look pretty straightforward to me -- again, what assembly would you expect to be emitted in those cases? It is very straightforward indeed, but it is still not mapping primitive operations to direct machine code, but it is forwarding to out-of-line code. Same as operator overloading in other languages. > And here's assembly for C++: https://godbolt.org/z/dedcof9x5 https://godbolt.org/z/dedcof9x5 That's just a symptom of allowing the compiler to inline the add code, otherwise the generated code is as straightforward: addOne(Int): push rax mov esi,0x1 call 4010c0 <add_safe(int, int)> Ref: https://godbolt.org/z/xo1es9TcW https://godbolt.org/z/xo1es9TcW
- acdha 2y ago> That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function. I completely agree that C++ is orders of magnitude worse but I’ve seen at least a couple counter-examples with code almost that simple. A researcher I used to support compared each release against a set of reference results, and got a surprise when they didn’t match but his program was working. This turned out to be a new compiler release being smart enough to inline and reorder his code to use a fused multiply-add instruction, which had greater internal precision and so the result was very slightly different from his saved referenced set. GCC has -fexcess-precision=standard for this but you have to understand the problem first.