There are hundreds of ways to implement and optimize similar functionality that I feel like an artist writing in it. This, of course, comes with many downsides.
There are hundreds of ways to implement and optimize similar functionality that I feel like an artist writing in it. This, of course, comes with many downsides.
The second half of that beef is that most C++ learning journeys bring the learner up to the point of creativity without addressing concepts like Strict Aliasing which are not useful until you start getting "creative". That's big a problem because code containing strict aliasing violations (and/or any of a myriad of other UB) is often fundamentally broken while giving the illusion of behaving as intended.
in modern c++, also teach RAII and unique_ptr, combined they can eliminate so many memory safety problems.
C++ is so tightly bound to memory layouts that once you start getting a solid mental model of what's going on, it's really easy to come up with legitimately neat and creative ideas that work very well and fast on a test bench environment without any complaint from modern compilers, but are fundamentally broken. I feel we do not do a good job at equipping up-and-coming programmers so that they can benefit from that mental model without writing code that only works out of sheer luck.
In the extreme, you can write code that does stuff like dynamically retype an object by swapping out its vtable when vtables are not even thing as far as the language is concerned. It's astounding what it sometimes feels like you are capable of doing in C++.
In any case, Strict Aliasing is not a flippant rule that compilers like to apply. It's a formal component of the ISO standard that C++ is.
You can chose to make a deal with a compiler (via a flag) to compile otherwise non-compliant code in a consistent way, but that does not change the fact that the code is non-compliant.
The basic idea is: don't load your block of code with lots of pointer dereferences. Load the values you need into local variables. Don't proliferate common expressions which dereference the same pointer to get at the same value. Consolidate the assignments through pointers. Don't do this in three places: (*ptr)++. Have that value in a local variable var, and do var++ in three places, then assign it *ptr = var.
Even without no-strict aliasing, a compiler can still assume that local variables whose addresses are not taken are not the targets of any pointers.
In C (99 or later), you have restrict also. restrict is independent of strict aliasing, because it's not based on type.
Furthermore, speaking of restrict, aliasing between like typed objects matters for optimization. It's good and well to optimize based on the idea that a double * cannot be aiming at an object whose declared type is long. But it's insufficient, because code that is manipulating double * pointers is likely working with objects of type double which could be the targets of those pointers. So without some combination of tight coding and possibly using restrict, you will end up with those load-hit-stores in all sorts of code.
This code for removing from a linked list:
node->prev->next = node->next;
node->next->prev = node->prev;
still isn't as good as this: node *next = node->next;
node *prev = node->prev;
next->prev = prev;
prev->next = next;
The problem is that the common expressions can't be eliminated based on aliasing, and the aliasing is between like types, so strict aliasing doesn't help.With gcc (x86) I get:
movl (%eax), %edx
movl 4(%eax), %ecx
movl %ecx, 4(%edx)
movl 4(%eax), %eax
movl %eax, (%eax)
versus: movl (%eax), %edx
movl 4(%eax), %eax
movl %eax, 4(%edx)
movl %edx, (%eax)
We shaved off an instruction through tighter coding, and IMHO (in this case) improved the readability also.That was gcc 7 on Ubuntu. The result is exactly the same like what I saw under gcc 2.7.x a quarter century ago.
How about gcc 11, x86-64? I should probably be using godbolt, but anyway, also five instructions down to four:
movq (%rdi), %rax
movq 8(%rdi), %rdx
movq %rdx, 8(%rax)
movq 8(%rdi), %rax
movq %rax, (%rax)
vs: movq 8(%rdi), %rax
movq (%rdi), %rdx
movq %rax, 8(%rdx)
movq %rdx, (%rax)
(I think in this particular case the compiler could do a better job because even if the assignment "node->prev->next = node->next" clobbers the value of "node->next" due to the nodes being aliases, the assignment can only clobber it with the value that node->next already has! The compiler doesn't analyze it that far though.)C was designed from the start as a language which the programmer does the optimizing, and regardless of the advancements in compilers, that has not been entirely eliminated.
How you write C still makes a difference, even at the microscopic level of individual statements and expressions, not just the level of overall program organization and use of algorithms.
If you write tight code, you can turn off strict aliasing optimizations globally and it won't matter. But you don't have to do that globally. You may be able to confine your type punning hack in its own source file, and just turn it off for that file. (Or may be even on a finer granularity if you have such compiler support.)
That is completely false.
Firstly, a C++ implementation that doesn't optimize based on no aliasing assumptions can be entirely conforming: it can handle all correct, portable programs in the required way.
Secondly, the handling for programs which do type punning doesn't make it not C++ anymore; just a dialect. Programs in that dialect are C++, just not ISO C++.
Every implementation of a standardized language is a dialect of that language; the standard just explains what is the common dialect (theoretically) supported by all of them.
In fact it's common for C and C++ compilers to recognize their own dialect by default.
I suppose but only because C++ is everything and nothing. C++ compilers are borderline self aware because the spec is 'write literally whatever you want'. Skynet was obviously a C++ compiler ;)
There is no standardized programming language that doesn't have vendor specific extensions comprising a local dialect.
This blog effectively agrees with my position: Undefined Behavior is just "Stuff the compiler is allowed to assume never happens". And C++'s set of Undefined Behaviour is a fundamental part of what makes it valuable.
The issue I have is not with them, but rather with the fact that it's too easy to become competent, if not talented, way past the point where one should take them into account without having to even know of their existence.
Perl? Can even write poetry in it. https://docstore.mik.ua/orelly/perl/prog3/ch27_02.htm
[0] https://twitter.com/timur_audio/status/1004017362381795329
The happiness people get from Rust was achieved by the incredibly hard work of library developers who had to deal with all sorts of complex semantics under the hood to make safe Rust safe. The knowledge gap between library user and library writer is even worse than C++, where most people do not feel comfortable enough to actually write low-level libraries (custom data structures, bindings from C, optimized routines) in Rust.
(At least PL people are inventing things like stacked borrows to make writing unsafe Rust easier… but it’s still not easy.)
Also, building low-level libraries that work correctly, efficiently, relatively-safely in C++ is NOT a simple feat, and requires years of experience (on API design). With unsafe Rust you can just fuzz/brute force out the unsoundness with the safe API.
Lol what? Unsafe is literally the only "mode" in C++. I'm not trying to shill Rust, but this is not true, and I don't know where you've read that opinion. Unsafe Rust is just Rust with the ability to use "C++ style" pointers.
> The happiness people get from Rust was achieved by the incredibly hard work of library developers
I'll give you that writing Rust libraries is harder, but that's true for any language. Even C++. Do Rust developers go the extra mile to make libraries ergonomic and have nice APIs? Yes! But that's not really a "problem" with Rust, it's something Rust enables, and now the community expects. C++ libraries in my experience tend to be a very bare minimum, and to use some of them it requires hundreds of lines of set up code. In cases where Rust libs require that much set up code, the authors go the extra mile to create macros or additional succinct APIs. So, yeah, their job is harder because they are held to a higher standard than C++ libraries, if anything.
Also, most of the effort with authoring Rust libraries is writing documentation, which is practically a requirement, whereas with most C++ libs (even some sections of boost!) you practically weep tears of joy when you see five lines written above a one class in a sea of classes.
> who had to deal with all sorts of complex semantics under the hood to make safe Rust safe. The knowledge gap between library user and library writer is even worse than C++, where most people do not feel comfortable enough to actually write low-level libraries (custom data structures, bindings from C, optimized routines) in Rust.
Sorry to be so blunt and call you out, but this is just so devoid of any sensible thought.
Most people don't feel comfortable writing low level libraries. Period. Regardless of language. For every library author, there's dozens of not hundreds of non-libray developers. Hell, most developers don't even contribute anything to open source in general, but just consume it.
So pretending C++ is this magical fairytale land where everyone produces libraries with the flick of a wrist is just bullshit.
Which is why, those of use that want some Rust like confort, when C++ is part of the diet, turn on bounds checking (#define _ITERATOR_DEBUG_LEVEL 1 on VC++), and make use of static analysis during the whole development (/analyze on VC++).
It is bullet proof? No, but it does help surving a couple of shots.
Rust will seem fine until you are writing a library and find no way to express what would make the library nicer to use; or you find using some library awkward and error-prone. Almost every C++ feature is designed to enable delivering more powerful libraries, and they compound.
So, in C++ you as library writer are empowered to make the use of your library correct by construction: what compiles is safe and correct. Rust makes the compiler responsible for memory safety, but offers much less to make using your library pleasant and foolproof.
Not my experience so far, I might go as far and say it makes it more foolproof because when writing your library you have the option to return an `Option` or `Result` enum.
The former is in the Standard C++ library. The latter will be in the next, but anyway the one in Boost has worked fine forever.
Throwing, instead, is a choice unavailable to Rust coders, so you often have no alternative but to return these more complicated things that users are then obliged to unpack. A standard macro makes that easier, and is semantically equivalent to throwing, but imposes substantial overhead on successful calls.
But that's not really all that relevant either, because comparing languages at this level of detail misses the forest for the trees. An interesting comparison takes a step back and compares which problems the languages (try to) solve. Often something that appears to be a clear win for one or the other turns out to be a non-issue.
Overhead is imposed all up and down the call chain, every time through. Seems pretty relevant to me.
What's your rationale for claiming in a swiping generalization that implementing a data type, regardless of all options or approaches and design, has no other option than doing everything wrong?
C++'s standard library optional type is a tagged union implemented from scratch, with a dizzying array of template metaprogramming to meet the standard's requirements- see e.g. Microsoft's here: https://github.com/microsoft/STL/blob/main/stl/inc/optional#.... This isn't a criticism of the team behind this code, it's just what it takes to do this in C++.
Rust gets most of that functionality from the language instead, in a much cleaner way. Sum types are built in, value categories and move semantics are handled automatically, there is little-to-no metaprogramming, and most of the API is just simple convenience combinators with obvious implementations: https://github.com/rust-lang/rust/blob/master/library/core/s...
If nothing else, this is a clear counterexample to the claim that "Option and Result types are as easily coded in C++."
False.
Last time I looked at Rust's std::Result, it's implemented as a tagged union.
Most of C++'s implementations of a Result data type are implemented as tagged unions as well.
What's your point?
> This isn't a criticism of the team behind this code, it's just what it takes to do this in C++.
The only conceivable argument you can possibly make is argue that Rust might support tagged unions as language primitives, but that would be totally pointless as Result types are relevant for the interfaces and higher level abstraction they provide, not your personal opinion of how hard someone else had to work to implement them.
I've developed Result and Either types in a few languages, including C++, and the only hard thing about C++ is doing the usual homework to handle lvalur/revalue/revalue well.
> no alternative but to return these more complicated things that users are then obliged to unpack
Yes, thank you. It is much better this way. I think "substantial overhead" needs to be backed up with some numbers here because setting up exception landing pads is certainly not free.
I prefer result style error handling personally and I am not suggesting that it's worse or shouldn't be used, just thought the findings were interesting and worth considering.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p25...
Of course, porting the benchmark for `sqrt` wasn't trivial because (of course) C++ just modifies the span passed in making it easy to abstract out the allocations whereas Rust says "you cannot pass a mutable slice across a `catch_unwind` barrier", so all of my tests end up returning a new `Vec` inside the benchmarked function. I also wonder how much using an inner function for `fib` would allow some TCO to kick in and change results again.
In any case, I don't think that C++'s has anything that satisfies the use cases that Rust's `Option` or `Result` cover in its standard library, so even if `std::optional` and `std::expected` were magically faster, the fact that `std::optional<inner_field_type const&>` is not supported means I can't use it for what I want anyways and I'm back to Boost or hand-coding classes and dealing with corner cases myself. Which is, unfortunately, a C++-shaped problem that has existed for a long time anyways, so…nothing new.
I do think the idea that exceptions are faster at runtime (at least in micro benchmarks) can be ported across languages though just due to the way exceptions are implemented. In any language, a caller of a function that can return a Result or Option always need to branch to unwrap the returned value, but callers of functions that can throw don't have these branches at all (on platforms with "zero cost" exceptions). Whether or not it actually matters in practice is certainly debatable. I would guess that it will never be a real problem for most people.
Wrt to std::optional and std::expected, I also have found them to be pretty clunky and limited, especially compared to the Rust counterparts! The lack of pattern matching is one issue, the lack of optional references is another, and they just don't integrate with the language as well since most libraries and even std functions don't use them!
Empowered? Sure. But it seems no one likes using that power for long stretches of time. Rust gives the same power and it gets used far more usefully IME.
> Almost every C++ feature is designed to enable delivering more powerful libraries, and they compound.
Well, we can take `std::variant`, `std::optional`, and `std::expected` out of that pool because the committee hamstrung them from their better alternatives that already existed in Boost for silly, misguided reasons.
> Rust makes the compiler responsible for memory safety, but offers much less to make using your library pleasant and foolproof.
C++ libraries have, historically IME, been far more likely to ask you to juggle live grenades while dancing in a minefield than Rust libraries and APIs have.
Some books on C++ teach you those, but I give up very early on learning languages with non-obvious expectations that offers few safeguards.
The same thing goes for the good-old "Do not use raw pointers." The rule is actually: "Do not use raw pointers with implicit ownership semantics attached to them."
To take your example; the same C++ "class" syntax can be used to express different concepts;
a) A user-defined value type eg. ComplexNumber. Therefore the dtor need not be virtual since the class is not designed for inheritance.
b) A pure interface used for "Interface Inheritance" i.e. all methods are "pure virtual". Therefore you need a pure virtual dtor. Java has the "Interface" keyword for this.
c) A class designed for a type hierarchy used for "Implementation Inheritance". Therefore you need a virtual dtor.
d) A class can also be used as a simple namespace eg. all static members; not designed for inheritance and thus no dtor needed.
Might you or someone else recommend a resource that does explain which features are suited for each the programming paradigms C++ support? Is there a modern resource that does teach it correctly like this? Knowing what features are relevant to your paradigm seems like a good way to approach learning C++.
Most of the above design ideas are found only in pre Modern C++ books. I don't know of any newer books which actually teach multi-paradigm design using Modern C++. So you will have to read the older books and then think about how you would map the same ideas into Modern C++. You might find the following useful;
Pre Modern C++:
1) Multi-Paradigm Design for C++ by James Coplien.
2) Scientific and Engineering C++: An Introduction With Advanced Techniques and Examples by John Barton & Lee Nackman.
3) Advanced C++ Programming Styles and Idioms by James Coplien.
4) Modern C++ Design: Generic Programming and Design Patterns Applied by Andrei Alexandrescu.
Modern C++:
1) Functional Programming in C++: How to improve your C++ programs using functional techniques by Ivan Cukic.
For learning Modern C++, you might find these recommendations useful: https://news.ycombinator.com/item?id=32884432
But then you realize it's just all too much, and you're looking for something less complex again.
I wonder how useful a C++ that I could understand in its entirety would be. But I don't understand english in its entirety either. I use what I know and get by just fine.
It has been years since I last coded a virtual destructor.
$ g++ -Wall -W -Wextra deriv.cc -o deriv
$ ./deriv
1.08654e-10
$ cat deriv.cc
#include <cstdio>
struct Base {
public:
double y;
Base() : y(1.0) {}
};
struct Derived : public Base {
public:
int a, b;
Derived() : a(0x3DDDDDDD), b(0x3DDDDDDD) {}
};
int main()
{
Base *b = new Derived[5];
b++;
std::printf("%g\n", b->y);
} int * b = new int;
*++b;
Dereferencing past the end is always UB in C and C++ and it is hard to fix at the language level when using raw pointers.common lisp is cpp's way more talented brother that got addicted to lsd and became a hippy
Of course if you want to talk about performance and stuff then JavaScript is not comparable, but that is not the point
But I agree, C++ is the paintbrush of coding and I grow tired of my Js crayon prison.