Goodbye C++, Hello C
momentsingraphics.de
momentsingraphics.de
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.
auto accumulate = ...;
auto a = ...
auto b = ...
auto result = a*b;
for (int row = 0; row < a.rows) {
accumulate += result[row]
}
This expression might be entirely valid when a and b are both 2x2 but then becomes incorrect when there's a different shape in b.For example:
int a = 1;
a + a = 2; (int)
a * 2 = 2; (int)
auto ma = matrix<2, 4>();
auto mb = matrix<4, 1>();
ma * a is matrix<2, 4>
ma * ma is not possible
ma * mb is matrix<2, 1>
Compilers used to be very bad at telling you what was going on here if you, for example, changed `<2, 4>` to `<3, 4>` or something. g++ now handles it pretty well: https://hastebin.com/idemopikagAlso 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`, `*`, `->`.
Whereas javascript.. Your code could be doing almost anything yet it will run just fine. Also, cus its not compiled the IDE for javascript is much much less helpful (eg. theres no "find all references") and much more permissive.
I also rebuilt our audio streaming system over the course of 48 hours to use the texture streaming subsystem when we exceeded the 64 file handle limit on an certain platform. We needed to hit a date for a TGS demo and I can guarantee you that we had things which were even more YOLO for a fairly decently sized team/game.
Some of these bugs are experience ruining: think Fallout 76.
Also, my real-world experience with wading through the abstraction insanity often seen in C++ (and justified because it's "safer") to find and fix bugs, and even more so with the sheer baroqueness of Enterprise Java (arguably an "even safer language"), shows that "reduce defects" is more like a dream. Maybe the number is reduced but when one is found, it tends to be harder to fix.
Put another way, I'd rather fix relatively simple C (which also tends to be simpler code in general) than the monsters created by "modern C++" because they thought the "added safety" would mean they could go crazy with the complexity without adding bugs. Perhaps there is some sort of risk compensation going on.
The saying "C makes it easy to shoot yourself in the foot; C++ makes it easy to blow the whole leg off" comes to mind.
It's completely possible to write C++ code without it being a mess of a template mostrosity and massively overloaded function names. People who write C++ like that would write C filled with macros, void pointers and all the other footguns that C encourages you to use instead.
I've been working with the sentry-native SDK recently [0] which is a C api. It's full of macros, unclear ownership of pointers (in their callback, _you_ must manually free the random pointer, using the right free method for their type, which isn't type checked), custom functions for working with their types (sentry_free, sentry_free_envelope), opaque data types (everythign is a sentry_value_t created by a custom function - to access the data you have to call the right function not just access the member, and this is a runtime check).
See [1] (their C api example). With function overloading and class methods it would be much more readable.
[0] https://github.com/getsentry/sentry-native [1] https://github.com/getsentry/sentry-native/blob/master/examp...
Maybe the suckless guys are on to something.
We pretty much have exactly that, but better with programming languages composing libs, functions and other abstractions. That’s exactly the same thing but at a different (better) level.
The game industry iterates far faster and the result are programs that can handle far more features than the average tech methodology. It's the classic quantity leads to quality pottery grading experiment. Have you ever considered that these 'best practices' pile on so much unneeded crap that an experienced developer doesn't need?
Also last I heard there was a startup aiming to solve slow browsers by running chrome in a server and streaming a video of the window somewhere. If that's not a setback I don't know what is.
Maybe this should tell you something about the relative complexity of the two problems. And frankly, features in a game are non-comparable to browser features.
> features in a game are non-comparable to browser features
You can't hand-wave this away. I'm certain you need a lot more math knowledge if you want to implement something like physical world. Does a web browser need that?
Some physics and linear algebra, while I’m not saying is easy, but it is not a complex layouting and CSS engine, with a state of the art language runtime, with all the possible requests, sandboxing, etc. — of course you don’t necessarily have to write an optimized browser, but still, just implementing a usable subset of the web is ridiculously hard.
Again you're trying to backtrace the results to the complexity of the task and the linkage simply does not make sense.
> Some physics and linear algebra, while I’m not saying is easy, but it is not a complex layouting and CSS engine, with a state of the art language runtime, with all the possible requests, sandboxing, etc. — of course you don’t necessarily have to write an optimized browser, but still, just implementing a usable subset of the web is ridiculously hard.
Well it seems to be easier because you can literally look it up through the internet and implement it as a set of rules. The hard part would be the combinatorial number of cases. If you don't have the math requirements to make a game with a 3d world from scratch it will not feel good.
In that case you are actually thread safe, though! Just use a language that lets you specify which data can't be sent across threads (Rust isn't the only example, Erlang enforces this entirely dynamically) and use thread locals instead of statics (in a single threaded environment they're effectively the same thing), and tada, you have thread safety that continues to work even if people decide to run your stuff on many different cores.
The tinkerer/hacker ethos is disappearing slowly from PCs. It never caught on in the mobile world. It may perhaps only survive as a remnant in specialised chips and boards designed for learning.
This is a good thing: these languages and runtimes are indeed much safer, and also much more productive than C. You can even still get the same amount of low-level control with Rust.
> in general discouraging peeking under the hood, at the hardware level, as something raw, wild or unsafe.
C is unsafe though. Decades of experience writing software has shown us that even expert C programmers write memory bugs with regularity.
> Yes you can still do it on a RPi
Or any other PC really.
> but perhaps in another decade or so, you might not be allowed to program in 'unsafe' languages on all other mainstream platforms, unless you register for a driver/system developer license or something, or not even that.
Lunacy. What is the evidence for this?
> The tinkerer/hacker ethos is disappearing slowly from PCs.
It was only ever there in the first place with a tiny minority of users, and that minority seems as committed to their craft as they've ever been.
Look at all the locked-down walled-garden platforms proliferating, and this famously prescient story: https://www.gnu.org/philosophy/right-to-read.en.html
20 years ago, many people thought RMS was a completely insane lunatic. Yet now he seems more like a prophet.
It's not hard to see where things are going if you read between the lines. Increasingly, "safety and security" is being used to exert control over the population and destroy freedom. Letting your children play outside unsupervised is "unsafe". Non-self-driving cars are "unsafe". Eating certain food is "unsafe". Having a rooted mobile device is "unsafe". Not using an approved browser by a company that starts with G is "unsafe". ... Programming in C is "unsafe".
"Freedom is not worth having if it does not include the freedom to make mistakes."
I don’t think I’m getting the connection here —- Rust was incubated at Mozilla and is now managed by its own open-source foundation. There’s nothing particularly closed or “walled garden” about it.
By contrast, Apple’s ecosystem is the canonical example of a walled garden. But it’s overwhelmingly programmed in unsafe languages (C, C++, and Objective-C). So what gives?
> It's not hard to see where things are going if you read between the lines. Increasingly, "safety and security" is being used to exert control over the population and destroy freedom
This is an eye-poppingly confusing confabulation: in what world am I any less free because the programs I write and use have fewer trivial vulnerabilities in them? What freedom, exactly, have I lost by choosing to crash less?
You bring up the GNU project; their background is explicitly rooted in Lisp: one of the very first safe, managed languages. The unsafety and comparative messiness of C is one of their standard bugbears. That hasn’t stopped their message of political and software freedom, as you’ve pointed out.
(Now most devs don't care about performance, so they don't see that as a problem. As a user however I can tell you, I hate when my phone lags for seemingly no good reason.)
This is common piece of received wisdom, but I don't think it's held up well over the last decade: both Java and C# have extremely well-optimized runtimes that perform admirably after an initial startup period, and (I believe) Swift compiles to native code with many of the same optimization advantages that Rust has (e.g., simpler alias analysis).
At the same time, C++ has seen a proliferation of patterns that are slightly safer, but perform miserably at scale: smart pointers (unnecessary lock contention), lambdas (code bloat, more I$ pressure), templates (I$), &c. C avoids most of these, but C written by "clever" programmers has similar pitfalls.
Sure, if what you're competing against is some kind of pointer fest, forget about locks, just jumping around the memory will trash your cache, and it won't matter how optimised your local processing is. But if you follow some data oriented principles and take good care of your memory access patterns, I have a hard time imagining Java or C# beating C++ or Rust.
Now there's this peculiar version/subset of C# that Mike Acton was promoting for Unity… though I'm not sure that counts.
> When you want to use a language that gets compiled and runs at high speed, the best language to use is C. C++ is ok too, but please don’t make heavy use of templates. So is Java, if you compile it.
https://www.gnu.org/prep/standards/html_node/Source-Language...
20 years ago, it was more like
> When you want to use a language that gets compiled and runs at high speed, the best language to use is C. Using another language is like using a non-standard feature: it will cause trouble for users. Even if GCC supports the other language, users may find it inconvenient to have to install the compiler for that other language in order to build your program. For example, if you write your program in C++, people will have to install the GNU C++ compiler in order to compile your program. > > C has one other advantage over C++ and other compiled languages: more people know C, so more people will find it easy to read and modify the program if it is written in C.
http://gnu.ist.utl.pt/prep/standards/html_node/Source-Langua...
Thank GNU for C.
Both of these things can be true! GNU has advocated for C for some pretty asinine reasons. At the same time, they’ve ported all kinds of Lisp idiosyncrasies into their style guide.
Plenty of people knew what RMS was talking about.
But GPL was very bad for business, said those there were against it, so the new world of shareware and public domain is here again.
That's only true to a point. Many mistakes are costly, and those costs are often born by other people. So it's reasonable to have protection against mistakes, for the benefit of both the person who would make them and the other people that they would affect.
When it comes to computer security in particular, an easily compromised personal computer can be devastating to the livelihood of the person whose computer was compromised through no fault of their own (remember, most people don't know anything about computer security, and they shouldn't have to), and can also harm others, e.g. if the computer becomes part of a botnet. If that computer is part of an organization, then the mistake made by a programmer can affect the ability of that organization to provide important, even essential, services. This is what's driving the increased focus on safety in this context.
I realize we're drowning in cynicism these days, and it's tempting to think that it's all an evil conspiracy to take away our freedom so a few people can make more money or have more power. Such a narrative resonates with something primal in us that's reinforced by the sort of simplistic good versus evil stories that make up so much of our entertainment. Reality is messier, more nuanced, and not as neatly connected as our puny pattern-seeking brains would prefer.
Rust is not a C analog. The whole value proposition of C is simplicity, and Rust is anything but simple.
>> but perhaps in another decade or so, you might not be allowed to program in 'unsafe' languages on all other mainstream platforms, unless you register for a driver/system developer license or something, or not even that.
> Lunacy. What is the evidence for this?
Look at a platform like Apple. Every release makes it harder to run arbitrary code.
>> The tinkerer/hacker ethos is disappearing slowly from PCs.
>It was only ever there in the first place with a tiny minority of users, and that minority seems as committed to their craft as they've ever been.
What do you mean? In early PC's, the way you ran software was to copy code from a magazine and compile and run it on your workstation. Being a PC user at all meant being a tinkerer/hacker a few decades ago.
Bullshit. Except for the brief period of time when the Altair was the only thing going on in the Micro space… the Apple II, Atari 800, IBM PC and TRS-80 amongst others were marketed in the late 70s/early 80s with off the shelf, ready to run software. While copying code out of a magazine was something you could do, it wasn’t even the common case then.
> Every release makes it harder to run arbitrary code.
I have not experienced this. Yes Mac OS makes it harder to run random stuff downloaded from the internet, but Llvm, clang, cmake, python from the command line works the same as they always have (you are fetishizing code that is entered yourself after all).
All other platforms were hardly any different from Apple, in fact Apple is just like they always have been
But the complexity argument is overblown.
Rust is a very complex language. You can argue about whether it's more or less complex than C++, but it's certainly on that end of the spectrum. C is way on the other end.
That's not a value judgement of Rust, just an observation.
Rust is complex, but I think it’s honest in its complexity: it straddles programmers with lifetime management in exchange for better optimizations (alias analysis is a pain in C!) and memory safety.
This is in contrast to C: it’s very easy to write C that compiles, but very difficult to fully evaluate its correctness. Part of that is the extraordinary complexity of the C abstract machine, combined with its leakiness: writing correct C requires you to understand both your ISA’s memory model and the set of constraints inconsistently layered on it by C. That’s a kind of pernicious complexity that Rust doesn’t have.
Rust targets a much higher level of abstraction than C. The machine itself is at arms-length, and you are mostly thinking in terms of an abstract type system and borrow checker rather than a CPU and memory system. A lot of rust programming is declarative, and a lot of the complexity comes from finding the right way to express your intent to the compiler through the various systems of the language. The tradeoff for that complexity is that you get to write programs with very strong safety guarantees, correctness benefits, and low performance overhead.
C is about having simple, imperative control over the computer hardware. With C you are thinking in terms of the CPU and the memory system, you are mostly just telling the computer exactly what you want it to do.
C definitely does have some failings: for instance as you allude to, C doesn't ensure that all failure modes are encoded in the function signature, so it's not really possible to audit a C program for correctness by reading the source alone, the way you can almost do with Rust.
But that doesn't mean that the level of complexity which comes with Rust is necessary to fix the issues with C. Zig is a good example of trying to plug some of C's holes without increasing the level of abstraction.
I would say the value proposition is control and performance, and more pragmatically ubiquity. If the value proposition were simplicity, why aren't C programmers writing Lisp instead? If it's simplicity and control, why aren't they writing assembly? At this point, C is little more than a bad abstraction that people are nostalgic for.
> Look at a platform like Apple. Every release makes it harder to run arbitrary code.
No, it doesn't. It has not gotten harder to run arbitrary code. It has gotten harder for developers to distribute unsigned applications. I've been using Macs for 10 years and my setup process throughout that whole time has been: xcode-select --install, install Homebrew, get on with my life. The OS never interferes with my programming beyond that.
Because it is hard model hardware in idiomatic Lisp and Assembly in not portable and not very productive. C is somewhat simple, portable, productive and fast language for writing code that is as close to a machine without having to use Assembly. It can be easily combined with Assembly when needed. Barring C++ it has the biggest tooling support of any other language available.
The original claim was that "you might not be allowed to program in 'unsafe' languages on all other mainstream platforms". But Apple restrictions don't distinguish between safe and unsafe languages, they just restrict all arbitrary code, so this is not an example of the point being made, but rather an orthogonal issue.
Apple doesn't distinguish between safe and unsafe languages for now, but it's not impossible to imagine this becoming a restriction in the future, given the broader trend.
There are entire CLASSES of computing devices which you cannot put arbitrary code on without severe obstacles...
This is true in both politics and software.
How do you bootstrap languages like Rust? Another 'safe' language? What about that one?
Someone somewhere has to be working at the asm level.
I'd argue that anyone who thinks language safety is some authoritarian handcuff doesn't really understand low-level programming to begin with.
I think few people in this discussion are worrying about programs directly affecting other programs through memory unsafety, exactly because this doesn't really happen for software that isn't a driver or inside the OS kernel. The problem with memory unsafety is that it often allows exploits that corrupt or steal user data, or gain unauthorized access to a system. That's not a problem when you are the only user of your software and you only have access to your own data, but once you have other peoples data or run on other peoples system I think you should at least consider the advantages of using a safe(r) language.
Most "interesting" programs can perform I/O. Then you run into ambient authority and confused deputies.
Most programs do not need arbitrary access to the file system, and it should be the OS's job to whitelist which files a program has access to. Again, a safe language is not going to save you from bad behavior on the filesystem either. It really only solves the memory problem.
Except that it is often a memory-safety problem that enables an attacker to make a program misbehave, through things like buffer overflows. A memory-safe program is much harder to exploit.
Even with a safe language, there's vulnerabilities like supply chain attacks which allow malicious code to use an escape hatch to access memory outside of the process. I.e. I could be programming in Rust, but one of the crates I depend on could silently add an unsafe block which does nefarious things. OS/hardware level sandboxing could prevent many such classes of exploits.
The problem is not about memory outside of the sandbox, but inside. Please read about return-oriented programming, for example, where a buffer overflow bug of a process can be exploited to hijack said process to do work it was not meant to do normally. If this error happened for example in sudo, it can very well be used to gain privileges and do real harm — and this whole problem domain is almost entirely solved by using a “safe” language.
https://news.ycombinator.com/item?id=27642630
In short, memory unsafety makes programmer bugs exploitable, instead of generally just failing.
Let's say we have two programs, A and B.
Program A by its very nature needs to have write access to the system's file permissions in order to fulfill its core purpose.
Program B only needs R/W access to a sqlite database installed in a specific directory, and the ability to make network calls.
I would agree that for program A, a memory-safe language can provide a very real benefit, given the potential risk compromising this program could expose the system to.
Would you agree that if a buffer overflow exploit in Program B can be used to compromise the system outside of the required resources for that program, this is a failing of the OS and not the programming language?
Nobody's suggesting we replace all the C in the world with signed javascript from Google, we're literally talking about compile time checks for pointers here.
You can also live forever, if you have right combination of genetics and environment.
There should be a list of these language snowball avalanches somewhere. They work every time.
I mean it is possible. In the same way all molecules of air could bunch up in one corner of the room suffocating you. It's a possibility.
Even if you try to only allocate one buffer (for example) to split up and use for all your logical vertex buffers, you still have to manage memory within that buffer, which ends up leading to pretty much the same kinds of complex reasoning you need for ordinary dynamic allocation. Failure to do this properly results in resource leaks on both the GPU and CPU side which can be really nasty.
Of course, this all depends on the complexity of your renderer. For simple usecases you might be able to get away with no deallocation until exit. But they'd have to be really simple.
Exactly. You can just bulk allocate all the memory for the frame at the beginning and drop the entire thing after the frame is finished. This is a very easy case for manual memory management where you have one allocation and one deallocation per frame.
Or you can do one better, and keep an arena for each framebuffer, and then just recycle them and never deallocate at all. If you really need some level of dynamism in terms of memory usage, you can just double the size of the arena every time you fill it, and you still don't need to deallocate, since chances are you will need that space again for some frame in the future.
I think maybe it would be helpful if you would go look at the actual implementation of a nontrivial renderer and try to find one that doesn't allocate. Because the strategies you're describing don't match my experience working on renderers, at all.
> GPU resources in particular need to wait on fences and therefore can't have a neatly defined lifetime like what you're proposing unless you block your whole program waiting for these fences, while other GPU resources are (like I said) immutable and need to live for multiple frames.
I don't quite understand why fences are a problem here. As long as you know what frame a resource corresponds to, you just to have to guarantee that the entire frame is finished before reusing. You don't need to know what's happening at a granular detail within the frame.
As far as static resources, that's easy: another arena for static resources which grows as needed and is shared between frames. Most of that data is living on the GPU anyway, so you just need to keep a list of the resource handles somewhere, and you're going to want a budget for it anyway, so there's no reason not to make it of finite size.
> More generally, how do you track resources other than the framebuffer, which are usually managed very dynamically and often appear and disappear between frames?
See the whole issue is trying to solve the issue "more generally". In many cases, you can know exactly the resources you will need (or at least the upper limit) when you start rendering a given scene. If you need to do any memory management at all, you can do it at neatly defined boundaries when you load/unload a scene.
The only time when you need some kind of super dynamic renderer is when you are talking about an open world or something where you are streaming assets.
Most of the serious renderers are written in C++ using custom allocators, which are just some kind of arena allocator under the hood anyway.
(1) You are manually managing an allocator on top of the GPU resources in the arena (reintroducing the usual use after free problems). (2) You have written a highly specialized renderer that cannot be reused across programs, as it requires all GPU resources used in any scene to fit into memory at once (and for their exact quantity to be known).
Once you acknowledge that asking for dynamic GPU resource allocation, the rest of what I asked about follows; for instance, you can't reuse whatever portion of the already-allocated resources were freed at the end of the frame unless you can track used resources across frames; you can then either trace the open command buffers each frame or whatever (tracing all the live resources) or use something like reference counting and free once the fence is done (tracing all the dead resources). Hopefully you will acknowledge neither of these is the same as arena allocation.
"dynamic GPU resource application isn't needed for a useful renderer" certainly sounds good, but again does not jive with my experience with renderers (admittedly, most of this involves rendering an open world with streaming assets, but most examples I've seen that aren't super tightly constrained examples require resource reuse of some sort).
Zero the buff and reuse this same frame as the new frame.
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.
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.
I don't know about that... C++ (both the language and the library) is orders of magnitude more complex, and the opportunities to make mistakes have grown almost proportionally. (Two characteristic examples recently discussed here on HN: auto references and iterator invalidation.)
However using C in the case of the original poster does not matter much as the application is game-related not subject to untrusted input.
It is pity there is no alternative "standard library" with safer data struct and operations.
There are some attempts, for example, relatively wildly known klib: https://github.com/attractivechaos/klib
C++ on the other hand has good expressivity that can better deal with complexity of programs (eg. just simply having string, vector, etc) at the expense of some added language complexity. But unfortunately most of that is due to backward compatibility.
My opinion is that program complexity is inherent for anything interesting, so the C++ tradeoff is worthwhile. Also, by sticking to a good subset of C++, one can minimize the “bad” complexity of the language, imho.
Look at all the very smart people that tried and completely failed to write libraries that do such basic things as copying strings.
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
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.
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).
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?
[0] And still are! For example, Python refers to them as such, see the "p" (lowercase p) character code here: https://docs.python.org/3/library/struct.html#format-strings
Some of them use a fat pointer instead, with a mix of counter, pointer to the end, and null terminator for C compatibility.
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.
The trouble is, people use templates more and more until they hit compile speed problems. I'd argue that this limit is much higher in D, but the end result is similar.
I get antsy when my test suite takes more than a second or so.
(Yes, I know neither javac nor ghci are particularly fast, but that doesn't detract from the general point.)
https://archive.org/details/byte-magazine-1988-09/page/n148/...
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.
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.
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.
Nope. They have grown too far apart. My C11 codebase simply does not compile when `mv *.c *.cpp && g++ main.cpp`.
* 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.
Language comparisons are almost always BS...
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.
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.
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.
And neither is OpenGL
And, like, Apple ported OpenGL to Apple Silicon macs. I suspect it's going to stick around, it's just never, ever going to get updated.
Yeah, that's how crap Apple support for OpenGL is.
Even with 10 or later, if not using Google or Samsung flagship devices, you will bump into driver issues.
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.
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
[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 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.
I almost sort of envy compilation at times.
It is the COBOL of systems programming languages and must be dealt exaclty on the same terms.
https://gcc.gnu.org/gcc-11/changes.html
> Modules, Requires -fmodules-ts and some aspects are incomplete. Refer to C++ 20 Status
Naturally some C++20 features aren't still fully there, it isn't as if they aren't coming.
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.
double negate(double x){ return x; }
?
Just because a system does not catches all bugs does not make it useless - for me, even if C++'s type system had caught only one bug it'd be worth it (and in most of my code, which is mainly about fairly specific domain objects, the compiler saves me from myself pretty much daily).
In any case, I'd likely have this somewhere
constexpr double negate(double x) {
return -x;
}
// example implementation for the sake of the comment, there are much more exhaustive libs out there
constexpr bool sample(auto func) {
bool ok = true;
double x = -1000.;
while(x < 1000.)
{
ok &= func(x);
x += 1.;
}
return ok;
}
static_assert(sample([] (double x) { return negate(x) == -x; }));
this way I'd get a compile error if my cat walks on my keyboard and inadvertently removes the - (also a relatively common occurence)It removes, however, the need for a lot of tests that are needed when using weaker type systems, mainly because you can make invalid data unrepresentable.
To give a few examples:
1/ in C++ you can write a type positive_int such that invalid values (negative ints) are not representable at all - either the operation a - b gives a correct, positive result (3 - 2 == 1), or you get an error that you can process if for instance you do 2 - 3. You can also write things in a way that the user only has to provide things in a declarative way.
2/ I'm working on an API for defining dataflow nodes for multimedia applications which tries to leave zero margin for error : the user simply has to define the inputs and outputs like this, as types, which the C++ type machinery is able to digest and show to the user :
https://github.com/jcelerier/score-simple-api-2/blob/main/Si...
In contrast, in C, one has to cast things left and right because the only tool you have is void* : see for instance
https://cycling74.com/sdk/max-sdk-7.3.3/html/chapter_msgatta...
or
https://cycling74.com/sdk/max-sdk-7.3.3/html/chapter_msp_adv...
in both cases we are defining a dynamic dataflow node with various inputs / outputs (for instance an audio input and output, plus a control), which is then displayed in an user interface (thus things have to be reflected in some way). But in the C version, the onus is on the programmer to :
- cast the inputs / outputs to the correct types according to the object's specification when using them - cast the values contained in the inputs / outputs to the correct types - access the correct inputs / outputs in the arrays
there is just so much more opportunity for error, which entirely disappears in C++
3/ Another example that you can see in my code: the make_uuid function. Takes a UUID in string form and converts it to binary uint8_t[16] at compile time ; also checks that the UUID is a valid one.
Every time I use this function, this is a test that I do not have to write - I know that all UUIDs in my system are valid, because I use an "uuid" type which can only be created this way by the user.