C++ is a great real world example of ignoring the advice "if you find yourself in a hole, stop digging". They have this problem with complexity and the only strategy they have to tackle it is by adding more complexity. Maybe they're going to overflow back to 0. Or they'll come out in China. Who knows. I've moved on.
I feel that this is a feature, not a bug. While I don't use C++ extensively, I think the Industry needs at least one language, which does not constrain the developer in any way. I think such a language is currently C++. It has all kinds of guidelines now to stay in the "safe zone", but there are many developers who (1) need unfettered access to the hardware and/or (2) need all kinds of language capabilities to write an extreme performance code.
I think the Industry rather likes heavily constrained languages. There's a sense in which that's the point of a language - it's an abstraction that cannot be breached, which limits what you can ask the underlying machine to do.
C++ won't let you define new types at runtime, but the advantage there is you know that the rest of your C++ codebase won't be doing that. It won't let you reflect on runtime data, but that means various changes you make to your program are undetectable outside of bounds (class or translation unit, sometimes shared library).
Forth or assembly will let you do whatever the hardware is game for because either can emit raw bytes and then interpret them as machine code. The cost is you have, at best, conventions about what the rest of the codebase is doing.
If you push too hard towards high performance in C++ the language lets you down. Aliasing rules mean data must be always atomic or never, and can't sometimes be an array of simd values. Or fno-strict-aliasing which costs you elsewhere. No control over calling conventions, instruction selection, register allocation or scheduling. On the bright side you can usually force partial evaluation well enough with template instantiations, but you can't have the inverse where you explicitly fold equal machine code implementations on different types. Plus trivial stuff like the embarrassment of unique_ptr imposing overhead if you pass it to a function. So C++ will get you within N% of optimal, most of the time, and that's usually good enough.
Could you elaborate what I can do in C++ that I can't do in Rust (using unsafe, if I must)?
For context: I've been writing code in C++ for most of my career (25+ years) and being for VFX/3D rendering it was mostly performance critical code. Now I write Rust for some very performance critical tasks in finance ...
There are still some things that C++ can do that Rust can't. A few of the larger and common examples would be, specialization, placement and variadic generics.
At least variadic generics isn't really a performance thing, it's just an example of a rough edge you run into here and there. Specialization and placement can be pretty important for performance though!
I was asking in the context of OPs claim. And that was regarding low level optimizations. I think only placement is a valid counterexample here but AFAIK there is support for Rust in that, if a bit elaborate in expression.
> Could you elaborate what I can do in C++ that I can't do in Rust (using unsafe, if I must)?
Use existing C++ libraries/code. I love Rust, or at least what I've seen of it since I haven't gotten the pleasure to use it in a meaningful capacity, but it's naive to pretend existing code can just be re-written and therefore existing languages should stop improving.
I do think it'll be fascinating to see how the performance of Rust and C++ ends up shaking out head to head. That is, how much is Rust actually sacrificing in performance by reducing the amount of UB (if anything, or if other aspects allow for better performance on average)
The only operations I can think of that are undefined behavior in C++ but not Rust are signed integer overflow (which Rust defines to panic in debug builds or wrap around in release builds) and out-of-range casts from floating-point to integer (which Rust defines to saturate).
Out-of-bounds index access is a borderline case; both languages have a safe index operation wherein out-of-bounds accesses trap, and a fast one wherein they're undefined behavior. However, C++ gives the convenient bracket syntax, which programmers are likely to reach for by default, to the fast behavior, and requires the safe behavior to be spelled out ("at"), while Rust does the reverse (the fast behavior is "get_unchecked" or "get_unchecked_mut"). Also it seems that not all relevant data types in the C++ standard library support the safe behavior, which is unfortunate.
In all other cases (that I can think of), Rust prevents undefined behavior at runtime not by changing it to do something defined at runtime instead, but by requiring the programmer to prove to the compiler that the undefined behavior can't happen. This may be annoying, and may encourage programmers to resort to things like RefCell that have runtime costs in order to avoid the difficulty of such proofs, but it should never stop you from doing the unsafe maximum-performance thing if you really want to.
Rust can also do some optimizations that C++ can't; it has additional forms of undefined behavior, like mutable aliasing and invalid UTF-8 strings, that the compiler can in principle exploit. Probably the more important advantage, though, is that Rust's maintainers can freely make changes to its compiler and standard library that don't preserve compatibility with existing compiled code (as opposed to existing source code), because the language has never promised ABI stability and (unlike C++) doesn't have an entrenched community of users who count on ABI stability and will complain if it's broken. Almost all Rust binaries statically link all dependencies except for libc, and build all statically-linked dependencies other than the standard library (which is tightly coupled to the compiler) from source on every compilation, and this has been the case since the language's beginning. For an explanation of why C++'s need to preserve backwards compatibility for compiled code inhibits optimization, see: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p20...
As for your other point, I think Rust will need smoother C++ interop than it currently has before it can displace C++, but there's hope. Here's the project I'm currently most optimistic about: https://github.com/google/crubit
I also use Rust now. If you use unsafe, there is little you can do in C++ that you can't do in Rust. However, some things like data races and mutable aliasing are permitted in C++, but not in unsafe Rust, as they invoke undefined behavior. The Rust nomicon has more details about what's allowed.
> However, some things like data races and mutable aliasing are permitted in C++, but not in unsafe Rust, as they invoke undefined behavior.
Define 'permitteed'. Unsafe is, among other things, exactly that. It tells rustc to permit you to do things that are ... well, unsafe. Including the ones you listed.
Maybe you don't understand the meaning of unsafe in Rust?
Unsafe causing UB is a possibility. As is taking two &mut refs to the same memory location using exactly that keyword. And that example works for multi-threaded Rust code using a shared resource accessed as such too.
The compiler is free to never write out your changes to memory in that shared resource use-case, which you might be depending on. AFAIK rust doesn’t have a standardized memory model which should be needed to write correct code that data races.
This isn't a difference between Rust and C++; data races are undefined behavior in both languages. If you want two different threads to be allowed to access the same memory without locking, that's what std::atomic (in C++) or std::sync::atomic (in Rust) does.
He’s talking about formal memory models, which are a bit different from what you’re talking about (though you’re correct that they are pragmatically similar.)
C/C++ has a formalized memory model that describe the high level logical rules/guarantees for writing safe concurrent code, while Unsafe Rust doesn’t have one explicitly written in spec/paper (although they kinda borrowed a less-formalized version of it from C/C++.) It has to do with ironing out hardware-specific behavior to obey a certain set of rules, while making sure all the compiler optimizations do not perform any invalid transformations against these rules. (The Rustonomicon explains this well in layman terms, and acknowledges the complexity of the issue: https://doc.rust-lang.org/nomicon/atomics.html)
In practice, it seems like the C/C++ model has some glaring flaws anyway, so I can’t say that Rust’s is really “worse”. But since Rust has this mission to be better than C++ in terms of safety, this is one of thorny issues of Rust that need to be tackled to really let its advantages shine.
I thought the C11 memory model was considered correct, possibly modulo 'consume' which seems to be ignored. The implementation in terms of types instead of operations has a performance cost but otherwise works. What glaring flaws do you have in mind?
Nice references. Going to take me a while to make sense of them. A superficial interpretation is one says relaxed doesn't work and the other says sequentially consistent doesn't work. Not great news tbh, hopefully it'll look better on closer examination.
It's undefined behavior. You can write unsafe code that's UB in both Rust and C++ (it's permitted, which I think is your objection), but those are not correct programs and they may behave in surprising ways or break when seemingly unrelated changes are made.
Perhaps you can write a small subset of your program in unsafe Rust, like performance critical inner loop, and then the rest of the program in safe Rust.
Make some mutable objects, lets say a std::vector of Geese. Spin up two threads, A and B. Make a pointer, or a reference, or by whatever means you prefer, like an index, the same Goose from the vector, and give both A and B that pointer/ reference/ index whatever.
Both threads can now change the Goose at the same time. That's a data race. As a result it destroys Sequential Consistency, but that barely matters in C++ because it also is Undefined Behaviour, your program no longer has any meaning.
In (safe) Rust it won't let you give both A and B a way to mutate the same Goose at the same time, thus data races can't occur, thus you have Sequential Consistency, and your program has a defined meaning.
C++ has this IFNDR - "Ill-Formed: No Diagnostic Required" in the standard which marks various things as simultaneously not C++ and so the standard doesn't define what should happen, and yet also your standards conforming compiler is not required to diagnose this and tell you about the problem.
You can presumably argue that all those things aren't "permitted" but there is no way to detect for sure if you wrote any of them, and if you did your entire C++ program has no defined meaning and might do anything at all.
There's a reason this exists. Rice's Theorem says non-trivial Semantic questions are Undecidable. You thus can't always correctly decide whether a program has some non-trivial semantic property, but C++ wants to require a whole lot of such properties. So, when it's difficult they just say the compiler must err on the side of emitting nonsense programs.
[Rust takes the other path here, if the Rust compiler can't be sure that your program has the required semantic properties you get an error. This is rare but annoying, typically you can easily modify your program to satisfy the compiler or when you try to do so you realise actually the compiler was right, this program doesn't have the required semantic properties]
I am increasingly confident that C++ made the wrong choice here, neither of these outcomes is desirable but the Rust outcome has a negative feedback loop - if changes make Rust's compiler annoy more programmers with spurious errors there's pushback. The C++ approach has positive feedback, as C++ gets less well-defined people's compilers accept whatever nonsense they wrote, nobody tells the committee to stop doing this - until one day it blows up in their face.
This is a surface-level meme that just doesn't reflect reality. In old C++ you had to spend all your time worrying about manual ctors and memory management. These problems have been erased by improvements to the language. You don't even use raw pointers any more. The language is less complex to use now than it was 10 years ago.
I think the third definition here sums up what complex means pretty well:
> a group of obviously related units of which the degree and nature of the relationship is imperfectly known[0]
It's impossible for any single person to understand how the C++ language interacts with itself without a reference manual: it's grammar can lead to the most vexing parse; it has metaprogramming builtin using templates or macros, allowing for arbitrary code execution at compile time; it has a ridiculous number of ways to construct an object (move constructors, copy constructors, default constructors, is the object heap or stack allocated, are you using bracket initialization or parenthesis, etc); and more.
Also, I pointed this out in a comment awhile ago, but as of C++17 and over 20 years of writing technical books about C++, Scott Meyers doesn't trust himself to determine whether a given code snippet is or is not valid C++:
> It's not that I'm too lazy to do it. It's that in order to fix errors, I have to be able to identify them. That's something I no longer trust myself to do.[1]
If this language isn't considered complex, I don't know what is.
I mean constructors. It's the exception to have to do anything meaningful in the body of a constructor in modern C++ - you can do a lot with member initialisation lists, delegated constructors and defaulted/deleted constructors (remember boost::noncopyable?). And your assignment operators will actually be exception safe.
Constructors induced exceptions, and once we have exceptions they became the failure reporting mechanism of the standard library, leaving us with goto-some-other-function-using-dynamic-scope as one of the foundation blocks of the language. Thus constructors are a reasonable contender for the worst design mistake in C++.
I don't agree with that analogy. For example, templates were for sure complex, and they're very much a complete feature. That is, they haven't been made more complex over time. If we accept that the committee has already dug themselves into a hole, what they're doing is digging themselves out by digging out the entire field to the same depth, thus eliminating the hole originally dug, which I think it's a perfectly reasonable strategy.
> For example, templates were for sure complex, and they're very much a complete feature. That is, they haven't been made more complex over time.
I agree with your broader point but parameter packs (aka, variadic templates) were an addition to templates made in C++11. So strictly speaking they have gotten more complex.
They were possible and widely used, but a lot less ergonomic. You "just" had to copy-paste some number of instantiatons for the number of arguments between 0 and, say, 50.
Wasn't it also at the cost of compilation memory? I still remember the time when it was easy to crash compilers by just giving them hard enough templates.
Constraints and concepts don't increase the scope or feature set of templates, which was GP's broader point. That is, all the additions are around things that are a smaller, but easier to use, subset of the broader feature. `if constexpr` for example just being a way more approachable std::enable_if, not an expansion of the feature set. And concepts then are about clarifying the contract of the template, they don't actually add new capabilities.
It'd be I think reflection, however that ends up looking, that's the next actual feature set expansion of templates.
They added a whole new keyword just to support it. Just because C++ templates are Turing complete doesn't mean new features don't count as new features.
C++ is a bit of a honey trap for people who love complexity. Unfortunately it's only caught a small fraction of all of the people who worship at that altar.
I love simplicity. I also love performance. This is why I use modern C++ for backend servers. It allows me to write very simple code that is still very performant (actually leaves standard stacks in the dust). I sure do not fall into honeytraps so for me complex languages like C++ is an advantage as I can always find what works the best for particular task.
yes I do: spp-httplib, taopq, spdlog, rapidjson. Also some other extras and alternatives depending on particular needs but the ones mentioned are enough to get one started on generic web app backend. What I do not and likely will not use are big opinionated frameworks.
Yes I do write my own libs as well but those are very domain oriented and mostly serve needs of particular application. Nothing exciting there.
> I have a soft spot for (C) single header libraries
May I ask you why? The whole preprocessor macro thing is just a disgusting hack that compiles slowly, is error prone and is not even expressive enough for the most basic of things.
I just tried changing a single file in my real project that uses those 4 libs. 3 seconds from change to starting debugging session.
Full rebuild of the project took 14 seconds.
While it is not blazing speed like one gets with the likes of Delphi, Go, etc. it is still quite ok. I am a practical person and can tolerate couple of seconds of compile-run in return to convenient libs.
And it is sure expressive enough to give me what I want. Yeah, writing template libs is something but fortunately other than writing template here and there I do not really have to do this kind of work.
Take a deep breath and think about what you've just said.
In reality if the applications are of any decent size there is basically no difference time-wise doing those either in C++ with libs or PHP/JS/Python. I have fair practical experience on this.
As for open source - I run 2 companies and do a lot of development myself. I have family and also do a lot of fitness so I can creak along at my young age of 60. You get the drill.
I don't write C++ for a living, and only have ~2 years' experience writing it for some hobby projects on weekends.
I've found that the way I'm most productive is by using data-oriented design where I put everything in structs and don't use classes. I still use a lot of modern C++ library features, but it reads more like C.
What's nice about having all this stuff thrown into the language is that you're free to pick and choose what you want to adopt from it.
For a long time, I tried to cram everything I wrote in C++ into an OOP pattern with classes, and it just didn't jive with me. Then I stopped doing that and started writing it more like TypeScript/Kotlin/C which I'm more familiar with and it's been a lot more pleasant ever since.
There's been so much great stuff in C++ 17/20/23 and you can just cherry pick all the bits you want to use out of it and ignore the rest.
I mean, if you change the name to "BufferPool::get_record" and you use an implicit this pointer instead of your "pool" argument, you have a method. It seems like you're fighting the paradigm more than you need to.
You're not wrong but for some reason this style is easier for me to work with
I know logically/semantically they're equivalent. The v-table function generated if the method is moved inside the class/struct winds up being identical.
Dunno what it is, some kind of familiarity bias with the way the code is laid out I guess, not coming from OOP languages
C++ uses name mangling to give names to non-virtual methods. They are no different than a C function that takes a pointer, except they may use a different calling convention.
Even if the method is virtual, C++ compilers make an effort to call methods directly without indirection through a vtable wherever possible.
The interesting thing is that you can't call the "function that takes a pointer". Accessing it as a method is the only thing which works.
A long term ambition for C++ and more specifically for Bjarne has been UFCS, Universal Function Call Syntax, which a few other languages have. But C++ can't do this today.
std::mem_fn will effectively give you the underlying function that takes a pointer. Strictly speaking mem_fn is defined to generate the wrapper function, but in practice the optimizer is just going to strip that away and call the name-mangled function directly.
Yup, but then the call syntax is different so it fails to achieve the "Universal Function Call Syntax" that OP was asking for which std::mem_fn provides.
To be more clear, when no "virtual" method is declared, then no table is generated because none is needed.
In this case, the addresses of all the methods are known at compile time and the compiler knows which to choose when anyone is invoked in the source code, based on the type of the object and on the types of the method arguments.
When there is at least one "virtual" method and you have a pointer to an object, the type of the object cannot be known at compile-time, so the compiler cannot determine which method to invoke.
That is why the compiler needs to create a table with pointers to the virtual methods for each class with such methods, and each object of those classes must include a pointer to the corresponding vtable, so that the correct method to invoke can be determined at run time (from the index of the virtual method).
Only classes with one or more virtual functions have virtual method table. Objects of such classes are called polymorphic objects and they (and only they) have hidden pointer to virtual method table.
If I was code reviewing this I would strongly suggest std::array instead of those raw array;)
I’m also curious why you have page_id_to_frame_idx[] without a size. If you are using that paradigm to read off the end of the struct then it needs commenting heavily, if it’s a typo then std::array would have caught it.
I do understand that it’s just a snippet of code to show a style, so ignore this pedant!
The trick with C++ is to use and pick things to work on that haven't devolved into a massive abomination of complexity just to be trendy and "modern". I've found that something like the classic (pun intended) "C with classes" style is a good balance.
Personally, I prefer to use C (C89) which is high-level enough to be reasonably productive, while at the same time not high-level enough to discourage the creation of overly complex code.
There are surprisingly frequent posts here from people who have written their own C compiler. I don't think I've seen a single C++ one yet.
`clang++ -ffreestanding` (without a standard library) is growing on me as a C89 replacement. There's some annoyances like std::launder that you need to implement using compiler intrinsics but it's feasible.
I've got my eye on Cpp2... Herb says he isn't interested in making it an official language but I suspect that once it stabilizes it will be an attractive migration.
Herb doesn't even want to default to immutable local variables (what he as a C++ person calls "const by default"). Obviously Cpp2 isn't finished enough for this to necessarily jump out, but Herb figures if we make a variable then we must intend to vary it, which is one of those claims I expect from newbie C++ programmers keen to defend their new language.
No, come on Herb, Kate Gregory has told us why there are variables, they are names for things. The machine doesn't need names, but the human maintenance programmers do. As a Microsoft employee I'm going to hazard a guess that Herb spends a lot less time staring at C++ real humans wrote before they died/ retired/ got fired without notice than Kate does in her consulting job. She knows what she's talking about.
I'm honestly not convinced that immutability for local variables is a particularly useful feature. Within the scope of a single function, it's usually pretty easy to see if a variable is being reassigned or not; there aren't the same kind of programming-in-the-large preventing-spooky-action-at-a-distance benefits that come from immutability across API boundaries. You increase the language's complexity and learning curve, for a benefit that's rather speculative and unclear.
(The exception is closures; prohibiting a variable from being reassigned after it's been closed over is useful because anyone reading the code may expect one of two conflicting behaviors depending on context, and so it's good to instead write the code in a way that doesn't have that ambiguity. Java got this one right.)
A post articulating this point, by the person primarily responsible for Rust's shared-xor-mutable architecture (so presumably he has some idea what he's talking about, though note that his argument did not carry the day): https://smallcultfollowing.com/babysteps/blog/2014/05/13/foc...
Notice that what you're asking for is to abolish const local variables. Which also isn't what Herb offers, this was the status quo in C during the K&R era because "const" didn't exist in K&R C. If you actually want const local variables, just not by default, you're not actually agreeing with Niko.
But then you have Google with Carbon… which is a direct competitor to cpp2. Apple hasn’t shown interest in either, and MS will of course most likely support cpp2.
Without the giants aligning their interests I’m afraid there will be a split in the community. Clang’s std C++ library is already behind GCC’s because Apple and Google have “moved” on.
Everything starts somewhere. I believe Chandler Carruth is leading the project so that means non-trivial resources are being dedicated to it. Their timeline is also fairly aggressive.
They're also simultaneously looking at Rust as a possible successor to C++ (e.g., by developing better interop tooling). It's believed that at most one of these projects can succeed in the long term.
on the other hand with more options you can express more and write safer code (for example with constinit which solves static order init fiasco...)
And if you don't have enough knowledge you can just stick to const.
What re alternatives for a system programming language?
(Rust seems to be fine, but still it's not super easy...)
Eh, it's gotten more complex sure, but in real world code most of this solves real problems, and even if it's technically adding to the language in practice most of the new stuff replaces old patterns. I don't think anyone that's used c++20 would want to go back to c++98, or c++11.