You use unique_ptr and shared_ptr because float
is unsafe. If you don't care about the safety that smart pointers provide, you can use float* in C++ too.> ticking with the example of matrix math, it is baffling how C code is often more compact and readable than C++ code. A matrix declaration would be float matrix[4][4]; [...] Of course, you could take the same approach in C++ but then you are not really using C++. The example from above in C could be simply: float* top = matrix;
But that's not that same thing, is it? I fail to see how you're actually getting the top 3 rows there; you're just assigning a pointer. Again, you're making your code less safe and expressive by using no abstraction at all.
And please, how is `matrix_add(matrix_mul(A, B), c)` more compact and readable than `A*B + c`? And this is being kind to the C version.
If the problem is with C++'s compilation times, then it's perfectly reasonable; this is one of the major problems of the language. But using unsafe constructs and then saying that they're better than C++'s safer alternatives is just comparing apples to oranges.
Look at all the very smart people that tried and completely failed to write libraries that do such basic things as copying strings.
In practice this pretty much cannot be done unless you use a formal verification framework, which almost no one does. Even the most well-resourced projects written in C tend to have trouble with low-level bugs. Same goes for C++. Both the Linux kernel and Chromium have plenty of these issues.
You can also live forever, if you have right combination of genetics and environment.
It is possible to write safe C, only if one uses static analysis tools and undefined behaviour sanitizers. In the case presented by the article, as it is related to game, safety does not matter much, unlike device drivers, operating systems or embedded systems where C bugs can introduce security vulnerabilities.
Regarding the compile-time complaint, it is possible to reduce the compile-time by using forward declarations; forward template declarations; template forced instantiation and isolating large parts of a project in a static library using CMake.
Ironically C++ by adding a ton of features to make the language “safer” basically achieves the opposite. Complexity obscures how your code works and thus aids in hiding critical bugs.
I have had so many cases with obscure bugs the 15 years I used C++ that simply would be impossible to reproduce in C.
Does it mean I advocate C for safer coding? Not at all, but I think the advantage of C++ is grossly overstated.
Languages such as Go are better examples for writing safe code. Why? Because they quite strong type safety and good memory semantics while not adding too high complexity to the language. Everything is far more explicit than in C++ which has too much “magic” and implicit behavior.
I would say both C and Go falls into the “most lines are readable, but the whole is not” camp, due to not enough abstraction. For smaller programs one can hold in memory it may be good, but it doesn’t scale.
Also I think that like everything in C++ it's a matter of not abusing a given feature. It's still useful to have the ability to overload operators because it makes the intent behind certain operations much clearer. There's nothing worse than not being able to address an ArrayList in Java with the subscript operator.
Nowadays I don't use operator overloading in C++ a lot, it's mostly `=`, `==`, `<=>`, `bool`, `*`, `->`.
But then why bother with C++ at all, right?
> more compact and readable
This: overloading of operators (and function) is one of the biggest draws of C++ (along with RAII and templates).
example:
float* f = GetF();
// In a C world, you rely on the documentation to tell you how long f is valid for.
SomeFunc(*f);
// We _know_ this is safe.
std::unique_ptr<float> f = GetF();
SomeFunc(*f.get());float a = 0.1f * 0.1f; assert((a - 0.01f) < 0.0001); <-- Works assert(a == 0.01f); <-- Will fail
You can use the type system to erase certain classes of error entirely, without ever having to worry about something pointing to bad memory or whatever. If you follow certain rules, C++ can be _almost_ as safe as Rust.
I love C personally, and I've been coding in it for more than a decade now. It is simple, easy to learn, but it gives you literally zero ways to create abstractions. That could be a good thing, until you realize that almost all complex C codebases reimplement (badly) half of the STL due to libc providing no containers of sort. Linked lists are terrible for performance, yet C programs are pestered with them due to how simple they are to implement. And I won't get started with C strings, which are just a historical BadIdea™.
Most C codebases tend naturally to evolve into a mishmash of reinventing the wheel and ugly macro magic. After all it's either that or to risk committing preventable mistakes all over.
That's not without saying that C++ is that better - every three years a new release comes out and fixes certain issues so vastly that it completely changes how you are supposed to write code (see string_view, or concepts). That's not without saying that the language is also full of old cruft no one should ever think about using.
What's the better alternative to null-prefixed strings you are referring to, length prefixed?
struct string_type {
char *ptr; // or char[]
size_t size;
}
because the whole idea of scrolling a whole string just to get its length it's an immense waste of CPU time. It has been optimized to death given that C does that for historical reasons¹, but still it's unwieldy and less safe.For instance, C strings make 0-cost string slicing impossibile: for instance, if you wanted to slice a string to get str[2;4] (i.e. a 2 char string from position 2), it would be simply
string slice { .data = str.data + 2, .size = 2 }
with the C++/Java/Rust/whatever approach. With C strings, given that you have to put a '\0' terminator at string end, you're forced to allocate memory and memcpy() the string, or to corrupt the original string.¹: size_t is much bigger than char (2x on 16 bit, 8x on 64 bit machines), so it made sense on a memory constrained machine like the PDP-11 to waste CPU time in order to save memory.
The wall of text generated by GCC/Clang/MSVC when template substitutions fail is a much worse issue, IMHO. Thankfully concepts help a lot in cutting off SFINAE earlier, so you don't get flooded with useless errors about the compiler trying to substitute random constructors in places or something.
For instance, in C, every function has a unique, global name. That means the implementation in the compiler can be a hash-table from string to implementation. C++ allows function overloading; now, our 'hash table' is a much more complicated thing: first, we must consider every function in the overload set no matter what; second, we must pick which function is "best" — usually through some complicated unification scheme. In all, this means that C function lookup is O(1) (probabilistic), whereas C++ function lookup is O(n) at best, but with unification it might be O(n^2) or O(n^3). I've seen slow compiling code with hundreds of identically named functions.
Templates are slow for two reasons: (1) they get shared via header files, so they get reparsed over-and-over-and-over; and, (2) they're implemented with a stringy interpreter whose job is to instantiate copies of the code. This is distinctly slower than macro expansion; generally, macro instantiation is of code and so occurs in source files — only requiring a single parse; and, also, there's just string-replacement, not an interpreter.
But ... here's the thing. You can make your C++ code compile fast by simply not using slow paths through the compiler.
I sometimes think I've seen everything programmers do, and then something like this pops up.
> You can make your C++ code compile fast by simply not using slow paths through the compiler.
D is fast to compile because it sidesteps or redesigns features that make for slow compilation.
A single templated function & a few n*100kloc macros later, and I got the compile time down to ~1s on a crappy laptop.
Macros are 'fast paths' through compilers — they copy structures in memory, rather than going through the entire disk->lex->parse chain.
https://archive.org/details/byte-magazine-1988-09/page/n148/...
If only we were so lucky ;). Symbols are not unique across the translation unit boundary, which is why we have C’s mess of fake namespace hacks. Even `static` doesn’t save the linker from doing extra work —- check out the locally bound symbols in one of your ELFs sometime! You’ll probably find some duplicates.
Rust is also working on making this easier, BTW. There's still quite a bit of excess monomorphization going on when using generic code, but better support for const: in generics should ultimately obviate this.
* Compiles to C, so you stand on the shoulders of giants wrt compilers.
* Very low overhead GC that doesn't "stop the world".
I was working on a project recently where I had to just write a bunch of stuff to disk, and I tried Node, and then Java, and they were about the same, and I figured - yep, makes sense, it's IO bound.
Nim got twice the throughput w/ an order of magnitude less memory usage! Because the serialization I suppose was the bottleneck and msgpack4nim is pretty fast.
You sure about that?
https://www.npmjs.com/package/mmap-io
https://docs.oracle.com/en/java/javase/11/docs/api/java.base...
mmap is a system call, not some C-exclusive fancy feature. If your language can print something on the screen, there is no reason it can't mmap a file.
And if the serialization was the bottleneck, mmap would not have changed anything anyway.
Vulkan can't possibly be the most widely supported graphics API can it? I have to imagine that targeting older hardware is better with OpenGL because it's been around forever, but I don't actually know and a quick google search doesn't turn up much.
I did some graphics work in my last job. OpenGL2 is supported by just about every device made in the last 15 years, on every major operating system, including software OpenGL in VMs.
Vulkan is not officially supported on OSX (although there is an unofficial port that is quite good, from what I understand) and only runs on modern hardware.
Yeah, that's how crap Apple support for OpenGL is.
I used to develop a Qt/QML app for Windows it was impractical to leave hardware acceleration enabled. In then end, using Mesa/llvmpipe everywhere was just more reliable.
Even with 10 or later, if not using Google or Samsung flagship devices, you will bump into driver issues.
Here on my laptop (Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz), on linux, compiling an eigen example takes 0.7 seconds
clang++ -fuse-ld=lld eigen.cpp -I/usr/include/eigen3/ 0,66s user 0,07s system 100% cpu 0,728 total
if I put the eigen header in a PCH this drops to 0.1 seconds clang++ -fuse-ld=lld eigen.cpp -I/usr/include/eigen3/ -include-pch 0,08s user 0,02s system 104% cpu 0,099 total
I have a hard time seeing how 300 lines of code (vs the 10 lines of the example) would multiply compile times by 300 - here's the example I used #include <iostream>
#include <Eigen/Dense>
using Eigen::MatrixXd;
int main()
{
MatrixXd m(2,2);
m(0,0) = 3;
m(1,0) = 2.5;
m(0,1) = -1;
m(1,1) = m(1,0) + m(0,1);
std::cout << m << std::endl;
}
I develop a GUI software with boost, Qt, and a fair amount of TMP and my average turn-around time from a change in the average file to the program running is between in and 2 seconds. Maybe it could get down to 0.1~ seconds if using C, but there would be so many less checks and guards !Try adding a bunch of matrix operations (particularly chained ones resulting in massive expression templates), fixed size matrices of various sizes, and compile with optimisation and SIMD enabled, and you'll see different results. I really like eigen and use it a lot, but it can definitely result in long compile times.
[1] https://blog.mozilla.org/nnethercote/author/nnethercotemozil...
Rust has really impressive incremental compilation built in already.
The first compile is annoyingly slow, but additional ones will be surprisingly fast, often finishing in a second or two , even in larger code bases. Especially if you split up your own code into multiple crates.
This could further be significantly improved by a demonized compiler that keeps everything in memory.
Additionally there could be a way to share compilation artifacts via the package registry, also making first time compiles fast.
There is also an effort to use Cranelift as a codegen backend, which can be faster than llvm for debug builds, and has the potential for JIT compiling functions on first use, which would make debug builds a lot faster again as well.
Zig also has some really cool plans in this domain.
There is lots of headroom to make compiling these static languages faster, but compilers would often need to be (re)written from scratch with these compilation strategies in mind.
If the language is not doing a great deal of your work for you, then you are not using it right. Doing it right, things flow fast and work right the first time. So, you don't care that it takes 30 seconds to compile, because you coded all afternoon and then compiled and it worked right away, with no debugging needed. After that, coding C feels like a big PITA where you have to tell the compiler everything over and over again, and need to compile every few minutes just not to get too far into the weeds.
Any big system has some objecty bits, but O-O is a technique, not a religion. Same for anything-oriented: functional, data-oriented, what-have-you. Master the tools, don't be mastered by them.
Working with Eigen was especially painful. All the template magic it uses means what would have taken 1 second to compile if used BLAS/LAPACK, now takes 10-20 seconds.
But that's the thing, it can.
- you can specify compile-time preconditions and use the type system to ensure value are in range (which catches a ton of errors) - you can use constexpr to enforce no UB (UB in constexpr evaluation is a compile error)
For instance, it's trivial to make a type-checked db id in c++ which will give you a hard error if you use the id pertaining to a given type, to another type. That's much harder to do in C (without implementing a custom code generator at least that would basically reimplement templates) and has saved me lots of very costly hours of debugging when I see the amount of time I got compile errors I got from that.
Nope. They have grown too far apart. My C11 codebase simply does not compile when `mv *.c *.cpp && g++ main.cpp`.
If you're writing code where you need to interface with the hardware directly (embedded), or manipulate things in kernel-space efficiently (OpenGL, audio, games), then C makes a lot of sense (and if you try to write C++, you'll end up using C anyway, and your attempts to shoehorn in C++ features will be ineffective).
If you're writing a high-performance C++ version of a Python web service, C++'s nicer memory management and string handling will be very VERY useful, not to mention object types for serializing/deserializing data.
There is a time and a place for all languages, but if I could only have one, I'd probably pick C++. It can do almost anything in whatever programming style you prefer. It really is the most generic language I've ever used. That's both its strength and weakness.
Also, the fact that C++ is standardized (ISO) and not controlled by a company (Google, Microsoft, Apple) I feel it offers greater freedom. No vendor lock-in.
Anyway I suspect that in a few years Zig will be the choice in scenarios like this. It gives you fast compilation times and low level access like C while being a lot safer, but without adding C++ level complexity.
That is an important sweet spot to hit.
dreams wistfully of C++ build that only takes several minutes
I almost sort of envy compilation at times.