> Where C++ gives you many slightly different takes on the same concept (e.g. unique_ptr, shared_ptr and raw pointers), C gives you one way to go and that one has a conveniently compact notation (such as float).
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.
The problem is not even just safety. You can write safe C, if you are careful (and you need to be careful also for C++, smart pointers do not just magically make everything right). The point is that OP makes the point that C requires to write less code, and this doesn't even seem true: you have to remember, each time some non-trivial object goes out of scope, to call its destructor/deallocator, which results in a lot of code (which can at times hard to read, especially if you have not completely linear control flow). Looking at just the declaration is a small part of the issue.
It depends. One approach to avoiding tricky memory management issues is to avoid memory management altogether. In the kind of applications where C is a good choice, you might consider strategies like allocating all the memory you need to work with besides small structs which fit on the stack when the program starts and then never deallocate. If you're worried about performance this is often a very good strategy.
Renderers need to deallocate memory all the time. For example, they often need to store per-frame data that gets dropped when the frame is over, debug strings, track temporary access to texture/buffer data, manage command buffers, etc. They also have to track resources for lots of data types external to the program (such as GPU data) which often forces them to work with external allocators, as well as having to synchronize stuff like resource destruction using GPU-CPU fences. Moreover, most resources are either immutable or can be accessed optimally when they are immutable, so you often end up benefiting significantly from being able to destroy and reclaim the memory of stuff you're no longer using rather than trying to update in place.
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.
> For example, they often need to store per-frame data that gets dropped when the frame is over, debug strings, track temporary access to texture/buffer data, command buffers, etc.
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 love arena allocation (which, incidentally, does entail dynamic resource destruction if you use it per-frame, and which is pretty easy to get wrong if you're just storing raw pointers everywhere--so good bye nice use after free properties), but you absolutely cannot just use arena allocation for everything. 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. Where exactly do you store this data? How do you access it? How do you free it when it's done? How do you perform slow tasks like recomputing pipelines and shaders in the background without being able to dynamically track the lifetimes of the old/new backing GPU allocations? More generally, how do you track resources other than the framebuffer/surface, which are usually managed very dynamically and often appear and disappear between frames?
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.
I am speaking from experience, I have written a lot of renderers.
> 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.
Sorry, but if you think you can just get away with never deallocating any static GPU resources (as you're implying by "just use an arena"!), one of two things is going on:
(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).
I would indeed be surprised. Like I said, point me to a production renderer used by more than one application that actually gets away with pure arena allocation except at blocking scene/frame boundaries. Until then, you're just asserting you can work within these restrictions without providing any examples. Considering that even stuff like hashmaps generally require allocations, it strikes me as fairly unlikely, but I could be convinced with code.
I don't believe that it is possible to write safe C (or C++), even if you are both very careful and also among the most skilled C developers out there. Every sizable project in C has had critical vulnerabilities. It is not possible to train engineers on a team of any meaningful size to consistently write bug free code and catch bugs in code review. Sanitizers, fuzzing, and static analysis all help but are insufficient in the face of the utterly impossible task of writing safe C programs, let alone evolving safe C programs.
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.
My comment seems to have sparked a lot of reaction about safety, but that wasn't the main point. The main point (and the one OP talks about) was about conciseness, and I can't really see how C can be considered a concise language.
Like when someone says "Lisp is homoiconic," and that automagically creates a thread of people tripping over themselves to make the same points they do every time "Lisp" and "homoiconic" appear in the same sentence.
There should be a list of these language snowball avalanches somewhere. They work every time.
It is possible to write safe-C, but C is far more error prone than C++. C has more implicit type conversions than C++, which may results in bugs and undefined behaviours. C lacks RAII (Resource-Acquisition Is Initialization) that is useful for memory and resource management. C will actually require more code than C++ since, the C standard library lacks generic data structures such as vectors, hash maps, linked lists and so on. The implementation of those data structures requires lots of preprocessor macro hacks.
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.)
But C lacks even strings, lots of C bugs and vulnerabilities are related to memory management, memory ownership and string handling. Even the C subset of C++ is better than C since it at least has more explicit type conversions that forces the developer to state his or her intent. One example of the C string problem is the strcpy(buffer, char* string) that copies a string to a buffer. If an external actor discovers how to manipulate the string size, he or she can take advantage of this buffer overflow vulnerability and even execute arbitrary code remotely if it is used in a server. If the application with this problem is a file, one create a specially crafted file to take advantage of this design flaw.
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.
There are two problems here, one being complexity, the other being abstraction power. The two correlates. C is an easy language without much complexity, but with laughably little abstraction power. Text-based macros are the worst thing ever, and other than that, the language can’t even express reusable data structures, only with convention.
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.
People get blinded by features. At the end of the day, what influence your ability to write safe code is the human brain. You have to be able to reason about and understand the code effectively to discover and fix problems.
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.
C++ can introduce bugs by obscuring the flow of code, but it can also alleviate many instances by having proper abstraction power. It’s about balance.
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.
FWIW i've seen a lot of 3D codebases in C++ that avoid operator overloading and instead use something like `A.multiply(B).add(c)` to make more explicit what is going on. Some even break that into multiple calls instead of using a single expression.
The confusion created by operator overloading is easily solved by tooling. The compiler always knows what a given operator resolves to, and environments like Visual Studio Code with the right plugins can handily highlight what you are actually calling.
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`, `*`, `->`.
I fail to understand how that’s more explicit to be perfectly honest. If it’s hard to resolve where overloads are coming from then that’s a problem but otherwise it just seems more verbose for no benefit. Addition and multiplication are the operations in use here, why shouldn’t we use their operators to represent them?
Do you mean type instead of shape? I also don't know what you mean by incorrect. C will also create casts when using two different number types in a math operation.
For matrix and quaternions multiplication does not output the same type as the input.
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/idemopikag
Your original comment was about something being incorrect, now you are talking about error messages, but I don't think either has to do with operator overloading.
I like operator overloading, but it is susceptible to argument dependent lookup while member methods are not. The syntax is literally more explicit about what logic is being called.
You'll find a lot of SIMD code using the multiple calls approach as for far too long C++ compilers did daft things like transfer the SIMD registers back to the stack or refuse to inline when the expression got slightly interesting.
It's actually float* - which is a pointer to a float (hn's formatting can eat these sometimes)
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());
The statement was made about a float pointer. Still float precision is a problem beginners run into, too. So maybe it'll will help one reading it somewhen.
float a = 0.1f * 0.1f;
assert((a - 0.01f) < 0.0001); <-- Works
assert(a == 0.01f); <-- Will fail
He is talking (I believe) about the general trend of nudging, cajoling more and more coders into using managed, very high level, safe languages and runtimes, and in general discouraging peeking under the hood, at the hardware level, as something raw, wild or unsafe. Yes you can still do it on a RPi, 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.
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.
> the general trend of nudging, cajoling more and more coders into using managed, very high level, safe languages and runtimes
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.
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."
> Look at all the locked-down walled-garden platforms proliferating
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.
Actually GNU project is one of the culprits for C spreading into a world of that was already moving into C++ and other safer languages.
> 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.
> 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.
> Actually GNU project is one of the culprits for C spreading into a world of that was already moving into C++ and other safer languages.
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.
With the possible exception of Rust, safety always had performance implications. Forcing people to write their program in C# or Swift or Java causes many programs to be slower than they really need to be, forcing us to either wait on them, or buy a faster palmtop.
(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.)
> With the possible exception of Rust, safety always had performance implications.
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.
It should be tested, but I don't think that a JIT compiler can beat an ahead of time compiler when the memory isn't the bottleneck.
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.
> "Freedom is not worth having if it does not include the freedom to make mistakes."
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.
> 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.
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.
> 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).
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.
There is something true in what you are saying, but I still think the difference in complexity between Rust and C has much more to do with the very different goals of the languages rather than C just hiding complexity from the programmer.
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.
> Look at a platform like Apple. Every release makes it harder to run arbitrary code.
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.
I was responding to the broader point that security is used as justification for making systems less accessible to programmers.
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.
> Rust is not a C analog. The whole value proposition of C is simplicity, and Rust is anything but simple.
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.
> 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.
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.
I agree - C is a sweet-spot language. It shows its age in certain ways, but it remains a relevant language 50 years after its inception because it strikes a very pragmatic balance between being simple and easy to understand, and in being a fairly thin abstraction over the hardware.
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.
How do you bootstrap languages like Rust? Another 'safe' language? What about that one?
Someone somewhere has to be working at the asm level.
Bootstrapping and language safety are orthogonal. C is unsafe and still you can't bootstrap it if you don't already have a compiler which can compile your C compiler.
According to that logic even assembly is not low level enough because you need an assembler to make a runnable program out of it.
Safety is complete orthogonal to being a bare-metal language (See Rust). You can have a completely locked down platform with an unsafe language (See iOS).
I'd argue that anyone who thinks language safety is some authoritarian handcuff doesn't really understand low-level programming to begin with.
I actually think safety should be guaranteed at an OS/hardware level and not at a software level. If it's guaranteed that my process can only make a mess inside it's own memory allocations, let the software be as unsafe as it wants.
Then you'll be happy to learn that what you propose has been the case for consumer computers since protected mode was added to Intel 80286 processors in 1982.
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.
But I don't understand how data stealing can happen if each process is effectively sandboxed. If my process can't read or write to memory outside of what it allocated, how can I corrupt or steal user data?
Well it depends on your definition of sandboxed. Does your program have permission to perform I/O, either by reading/writing to a filesystem or sending/receiving data on a network?
Most "interesting" programs can perform I/O. Then you run into ambient authority and confused deputies.
Yeah I guess it seems like a decent model for "safe" software would be sandboxed memory, and fine-grained file permissions. Arbitrary network traffic is a bit less dangerous - I mean someone could steal CPU cycles to process data and send it over network, but a safe language is not going to save you from that either.
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.
> 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.
Are we talking in circles here? My original point was that memory safety should be ensured by the OS/hardware. That way no matter how badly my program misbehaves, it will not be able to access memory outside of the sandbox. In other words, the CPU should not be able to address memory which has not been allocated to the current process. A buffer overflow should be a panic.
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.
> That way no matter how badly my program misbehaves, it will not be able to access memory outside of the sandbox
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.
In case of a browser, a buffer overflow can be exploited to upload user files for example — which are very much readable without trouble on most linux distros.
I understand what you are saying, and I understand that this is a real security issue in modern computing. However I would put the question to you in a different way:
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?
I agree with that — not having buffer overflows is a good to have but not sufficient thing for security. MAC and sandboxes are a necessity as well, eg SELinux can solve your proposed problem with program A and B.
to be clear, you're claiming that language constructs for avoiding massively prevalant use-after-free bugs (unique_ptr) will lead us all to lose control of our devices?
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.
unique_ptr is a conspiracy, man. You see, the people who make money off of mallocs (those corrupt DRAM manufacturers) want it to stay that way. Just follow the money.
Well, kind of. I would argue that most code does not need to be thread safe because it is not intended to run in a multi-threaded environment. I once worked on a application where it was 'standard' to run everything in a thread pool so basically everything could run simultaneous to everything else. Problem was that there also was lots of state to manage. So then one ends up giving every class one or more locks. Also, this particular application was not the high-performance part of the application. Obvious solution is to run most of the application in a single thread message loop and get rid of all of the locks. This appears to be heresy nowadays though. The high profile C++-ers tell us that everything has to be thread safe.
> Well, kind of. I would argue that most code does not need to be thread safe because it is not intended to run in a multi-threaded environment.
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.
"I know my software just works" is really hubris. The fly-by-the-seat-of-our-pants game industry has cranked up programmers egos and made them ignore a wide range of tools and practices that have been proven time and time again to improve developer velocity and reduce defects.
"proven" sounds like cargo-culting dogma. The same was said of OOP in the 90s, and look what that caused. Hence my distrust of the snake-oil.
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.
> 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.
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.
There's a bug difference between extremely complex c++ templates and std::unique_ptr, std::string_view, and constexpr. Also, I've heard many game devs still saying unit tests either take too long to write or they aren't helpful.
I think that if we focused on building small, simple programs that do one thing well and compose, C would be OK. It’s when we build out behemoths that you really have a hard time reasoning about your code. At that point, vulnerabilities are almost guaranteed. This is true in any language, but more so in unsafe ones.
The complexity that must arise (otherwise the problem we are looking at is not interesting enough) will happen either way. Composing small tools will give you an ugly as hell glue code over them — just imagine a modern browser. Would it really be better to do curl and interpretJS and buildDOM and all these things? Just imagine writing the glue code for that in what, bash?
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.
And on the flip side tech gets Leetcode interviews, shoehorned microservices when you dont need it, slow web browsers.
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?
I would not necessarily say that game development has better quality than web browsers. And the latter are anything but slow — they are engineering marvels no matter what you think of them. It’s just that websites like to utilize it shittily.
There's a lot of games out there, way more than there are web browsers. For a start try comparing games with manpower/dev support levels similar to a web browser like chrome. If we take a AAA open world game, the game somehow gets more features done compared to chrome. There's something that can be learnt there.
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.
> There's a lot of games out there, way more than there are web browsers.
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.
You're missing the point. You can't make such a claim about complexity based off the amount of software there is. Games are by far the more popular software to make. This is why I've narrowed it down for you, hopefully you can understand that.
> 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?
The fact is that a single person with a decent amount of knowledge can write a game engine that is more or less complete, while not even FANG companies can write a web browser from scratch should definitely be proof that the latter is more complex.
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.
> The fact is that a single person with a decent amount of knowledge can write a game engine that is more or less complete, while not even FANG companies can write a web browser from scratch should definitely be proof that the latter is more complex.
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.
I'm a game dev and imo web-devs are the 'seat-of-our-pants' guys. In games we tend to use compiled and statically typed languages. They're pretty strict in what they allow and many errors are picked up by the IDE and/or prevent your code from even compiling.
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 think it depends on a lot on the studio and culture. I don't think I saw a single unit tests before I left gamedev(a few smoketests to make sure the game didn't crash but that was about it).
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 the most buggy software I interface with are games. They crash and break in strange ways. I often wonder what it would be like if someone tested some edge cases or enabled a fuzzer for some functions. Like "what happens if I kill a freed entity", or "what happens if my character is 50% in a wall", etc.
Some of these bugs are experience ruining: think Fallout 76.
Yesterday I finished a complex library in C++20, which I wrote "Rust style" by following the ISO C++ guidelines. I used 0 `new`, no smart pointers, all by-value returns, move semantics and STL containers. It literally took a month to write, but it worked as expected at the second attempt (first failed due to forgetting a `}` in a format string). I did not see a single segmentation fault and it works fine on both Linux and Windows. Rust and modern C++ give you the expressivity to describe what you want to accomplish to the compiler.
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.
Null terminated strings are the root cause of millions, if not billions of security vulnerabilities caused by buffer overflows. All modern languages have shunned null-terminated strings in favour of something in the likes of
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.
Those would be Pascal Strings (length at the start, then the bytes). Those make for very easy to write code, but they come with a few issues, but in general they are simpler and give better behavior than null-terminated strings. However, as for a bit of a historical bagagge, it seems we got used to null-terminated strings and just went on with them.
Only in C, even C++ has since the early days adopted proper strings on its standard library, initially the compiler specific ones from the early 90's, then the standard one.
Hmm, they are tolerable to say the least. I think lots of people complaining with bad compile times are probably developing on underpowered laptops, which are definitely not the right tool for C++ development. I've never had an issue of long compile times on a reasonably specced desktop computer. Setting up ccache sure helps at times.
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.
That's correct. Also see https://github.com/microsoft/GSL, which is a header-only C++ library that implements the Guidelines Support Library as specified by ISO C++. It's extremely useful (especially to get std::span<T> if you don't have C++20 support) because it provides several constructs to explicitly clarify the _intent_of your code, like `gsl::not_null<T>`, `gsl::narrow_cast<T>()` or `gsl::owner<T>`. `gsl::finally()` is also pretty useful.
Sorry but the source is closed sadly, so I can't share it. I would like to write a blog post someday about some of the patterns I've used though, I think they could be useful to someone.
I like to think of a compiler as a virtual machine whose instructions are the tokens in your code, and whose output is object code. Taken that way, you can for sure optimize the input program (your C++) to execute faster on the interpreter (the compiler). Understanding how the language (any language with such features) is implemented lets you stray away from the slow parts.
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.
D is horribly slow to compile template-heavy code for exactly the same reasons as c++. And it encourages lots of monomorphization in e.g. ranges. The enhanced introspection and reflection (and CTFE) capabilities coupled with mixins also encourage code that is very slow to compile.
Not really. For example, D templates are never parsed more than once.
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.
Just like nature abhors a vacuum, my experience is that regardless of how fast your compiler is, user programs will grow to until they reach a point where they are annoyingly slow to compile.
If I told you the first attempt was several million identically named functions, split across several hundred files, and the compile and link times were ~72 hours on a distributed build would you be happier?
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.
> 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.
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.
> You can make your C++ code compile fast by simply not using slow paths through the compiler.
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.
I used C++ for a good while (systems programming). I like it and still use it for some things. But when I started using Go, I used C++ less and less. Go was simple like C and almost as fast.
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.
The author lists GLFW, stb_image, and ImGui as dependencies for their renderer written in C. Those are the same exact dependencies you would use to write a renderer in C++. Maybe you would add glm because you don't want write matrix math operations, but the author will have to do that in C anyway. I don't see how dependencies are an advantage for C.
You can go down the route to say C++ is largely a superset of C. You can always write C++ in the C way, so you don't see simplicity is an advantage of C.
Their second statement is still simply wrong. C has some ways of doing things which are simply unacceptable in C++. In C, you don't cast mallocs. You can initialize structs like `={0}` or `={prop=val}`. There's more, but the point is: if you are writing C++, you can't write it in a "C way". It won't compile at some point.
If you really like the simplicity of C I highly recommend Nim:
* 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.
Use mmap and you won't need to write anything to disk; the operating system will do it for you. That's C's killer feature while also being the last thing on earth languages like Node and Java would ever permit developers to do.
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.
By the way, the point here wasn't to do a great language comparison. It was "I have a thing to do, what language/runtime will let me do it easily and fast". Normally I reach for Node for my scripts, but got curious. Nim ended up being less code than the other options, type safe, and fast.
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 !
Haven’t used Eigen for quite a while, but I do remember it being rather slow to compile once it proliferates. Those templates are clever but also quite taxing.
The bits of eigen that make it slow to compile are the same bits that make it both easy to use and fast. I'm sure there are improvements that could be made, but ultimately eigen is big and slow to compile for a reason.
Unfortunately our example isn't representative, and avoids a lot of things that make eigen slow to work with. Parsing the header file will take a constant time, but template expansion etc. can take a long time if you have more code.
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.
Does it get significantly slower if you use Eigen's fixed size matrices? I have a C++ project which uses Eigen and some other templates (but nothing crazy) and it is quite slow to compile. Not complaining though because I don't compile often.
>So what is the bare minimum? To use the GPU, I need a graphics API. Vulkan is the one that is most widely supported so that is a natural choice
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 by "modern" we really mean released the last few years. For example it doesn't run on my 2013 laptop (has a 660M GPU, Nvidia supports Vulkan only from 7xx series) or my (late) 2016 GPD Win (technically there is Vulkan 1.0 support under Linux but not under Windows and AFAIK even the Linux driver has issues).
It’s OpenGL 3.3. It works as well as OpenGL 3.3 does anywhere else. It’s an old version, but it certainly exists, and it’s what many/most Mac games still use because it’s cross platform.
There are lots of Windows machines around with subpar or buggy OpenGL implementations.
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.
I can relate a lot to this as an old school C++ developer (not anymore). I also remember switching a project over to pure C some years ago and found it a lot more pleasant to work with. But I combined it with Lua so I could use Lua for high level constructs C is not so good at.
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.
I've always been impressed by Go's compile times. The first few times I thought I'd made a mistake, and it was doing nothing (the prompt came back so quickly). Now I know better :-)
Nicholas Nethercote has a series of blog post on speeding up the Rust compiler. [1] There are (were?) also efforts going to to reduce the amount of LLVM work by shifting some of that work further up in the stack.
In addition to the mentioned micro optimizations, I'll add some possibilities from the Rust angle.
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.
Often the C vs C++ debate comes down to a religious war where the C side is arguing that their hammer is best for pounding in nails and the C++ side is arguing that their screwdriver is best for screwing in screws. I don't believe they're suited for the same purposes at all.
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 don’t think this argument holds up. You can create very good abstractions over the lower level libs, like OpenGL. There is no need to call it “naked” like in C.
If you want a good case study in repositories that build quickly then check out cosmopolitan libc. Typing `make -j16` it compiles 15,383 .o objects, 69 .a static libraries, and 548 executables, 297 of which are test executables, which are also run by the make command, and all that takes just 40 sec.
I measured that on an Intel(R) Core(TM) i9-9900. I understand that not everyone has the privilege of being able to afford a $1,000 computer. However those people usually aren't coders. It'd be great if I had one of those 64 core "specialist" workstations that people like googlers use to be able to compile projects like TensorFlow in a reasonable amount of time. My project would probably build in seconds. But alas I don't, so I need to forgo eigen and template metaprogramming.
I can understand the point about compile times. For the rest, C++ is just superior and you cand do what you would do in C, just safer and add constexpr, consteval and careful templates for more type safety plus more type safety by default.
It seems like most of the issues with C++ are solved by not using the parts that suck. You can still write C++ as C-with-classes/RAII/operator overloads, and leave all the smart pointers and template meta-programming and boost (do people still use boost?) magic at the door.
I just freakin' hate .h files...
Writing pretty much the same thing but twice and in two different places for every function. Why?!?
I would love C++ if not for them.
sadly no, modules won't solve this problem. Yes you can write your implementation in a single module but it'll behave as if you were writing them in a .h file. That means that changing a single number in a module will trigger a recompilation of all the dependent modules. At the end of the day you end up with slower compile times for small changes. I was hoping to finally be able to be done with header files but no.. another missed opportunity for C++. Perhaps future C++ compilers will be able to figure out if the external surface of a module has actually changed or not and make this possible.. but alas it doesn't seem to be the case in VS2019 at the moment.
Or use traditional C. The x86_64 System V ABI is well defined enough that I'm honestly not sure why we need prototypes. All we need is a compiler that authorizes us to use an ILP64 data model. Then we'll be able to write C the way it was intended to be written without the quadratic header complexity. C++ showed us where that path leads and I truly hope modules happen but it's about as likely as my hopes of restoring the good parts of K&R C.
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.
The compiler cannot catch logic or design errors. If you made a mistake when designing the algorithm and implemented it as it is, you'd need to debug anyway. Sometimes it's something as simple as missing a minus symbol somewhere in your equations.
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.
> The compiler cannot catch logic or design errors. If you made a mistake when designing the algorithm and implemented it as it is, you'd need to debug anyway
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.
Naturally you first implement your algorithm together with a correctness proof inside the turing-complete type system. After that writing the real implementation can be left as an exercise for the compiler :)
I'm sure that in 1986, before standardizing ANSI C, someone victoriously typed the same thing in front of his tty in a discussion about using types in declarations and not just doing it à la K&R with just the function name being called cowboy-style.
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)
You're effectively just changing where you write your unit tests. So it's hard to agree that the type system is saving your from anything, when it's really just writing tests.
But no one said that type systems disqualifies from writing tests (and C++'s improvements over C aren't only about the type system).
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 :
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.
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.