Hacker News new | past | comments | ask | show | jobs | submit login
C++ by Example (cppbyexample.com)
242 points by synergy20 8 days ago | hide | past | favorite | 119 comments

As my first language, I've yet to experience another language that genuinely lets me be as creative as C++.

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.

And this half of my main beef with the language. To be clear, I absolutely agree that this aspect of it is wonderful, but it is a double-edged sword, and the edge pointing at the programmer's face is very sharp.

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.

or use -fno-strict-aliasing and profile the code to lift it for bottleneck code carefully? linux kernel does not allow strict-aliasing for example. but I agree yes all c and c++ course should teach strict aliasing.

in modern c++, also teach RAII and unique_ptr, combined they can eliminate so many memory safety problems.

C++ without strict aliasing is not C++ anymore. Furthermore, that was just an example. There are other such concepts, such as the illegality of accessing memory as anything else than raw bytes or the type of the object that resides there (which also applies to union fields). Another one would be the fact that signed integer overflow is UB.

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++.

> C++ without strict aliasing is not C++ anymore

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.

> That is completely false.

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 ;)

The meaning of punning memory is obvious if you understand the implementation-specific representations being used. E.g. if you understand what a 64 bit double looks like and what a 64 bit integer looks like, then it makes sense when you look at the memory of one through the type of the other. The C or C++ program to it has only two rationally defensible meanings: "undefined behavior" (ISO C or C++ interpretation, allowing certain optimizations, or run-time diagnosis of a type mismatch or whatever) or else "do the obvious thing and access the bits like the code says". What the code is asking for can certainly be be framed in terms of concepts in the standard. Just what the code is obviously requesting is not granted by any requirements in the standard. The idea that by asking for it, it's not a C++ program any more is silly.

There is no standardized programming language that doesn't have vendor specific extensions comprising a local dialect.

I remember once telling a friend of mine, after I'd spent several years writing Clojure full-time, that upon getting back into writing C++, the feeling was like "sculpting with electricity." There is a strange sense of power you feel from harnessing the language in a creative way, unlike any other language I've used.

Did he mean with rubber gloves and insulated mat, or without?

I understand compiler likes strict aliasing for performance optimizations, use it carefully but _only_ at bottleneck-ed areas seems like a good trade-off between safety and performance, why without-it is no c++ anymore? why do I need do pointer aliasing randomly?

Strict aliasing is what prevents compilers from having to sprinkle Load-Hit-Stores all over the place. This is not a marginal performance difference, and doubly-so for CPUs with deep execution pipelines. It also has massive code-reordering implications.

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.

There are ways of coding coding in C which makes those optimizations unnecessary.

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)

        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)

        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.)

Just in case you posted this as a counterpoint to what I'm saying...

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.

>There are hundreds of ways to implement and optimize similar functionality that I feel like an artist writing in it.

Perl? Can even write poetry in it. https://docstore.mik.ua/orelly/perl/prog3/ch27_02.htm

"If new true friend not protected for explicit private union, break case and try using this."

... I think the perl code actually compiles (err, runs) though. :-)

that is amazing

As someone who tried learning C++ a few years ago this is (personally) my biggest gripe with the language and this gif[0] perfectly sums it up. Gave up and started learning Rust instead, which I'm very happy about in hindsight.

[0] https://twitter.com/timur_audio/status/1004017362381795329

Fortunately, those can almost all be ignored almost all the time.

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.

I think Rust's combination of data-bearing enums and destructive move semantics goes a really long way towards letting library authors express their invariants through the type system. And that's in addition to the borrow checker and mutable aliasing rules. The mutable aliasing rules in particular are often kind of annoying for callers, but they're a godsend for API authors, because they allow for methods like Mutex::get_mut that don't even need to lock the Mutex, in safe code!

> 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.

Option and Result types are as easily coded in C++.

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.

The fact that `std::optional<T&>` is verbotten makes them quite a bit more useless. The Boost one is better because it doesn't have this misguided limitation. It's really quite a sad story and a loss to all of C++ that it is this way (and not for lack of trying on ThePhD's part).

> 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.

According to this link (posted below and includes benchmark results and a benchmark you can run), they compared result style error handling with conventional exceptions in C++, and the result style error handling (using an impl of std::expected) was "more than four times slower". According to the link and my understanding, exceptions are essentially "free" at runtime in the non-exceptional case on certain platforms such as Linux, but slower than result style error handling in the exceptional case.

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.


So I did some testing with Rust and I don't think that the C++ "four times slower" applies to it at first glance. Maybe Rust can just do better optimizations or something because Rust knows that it is a discriminated union, but these kinds of conclusions can't just be ported across languages blindly.

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 agree that "four times slower" may not apply in Rust specifically.

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!

C++'s option and result types are much larger, uglier, and harder to use correctly than Rust's. The shift from throwing to returning a Result is not really an increase in complexity- closer to a reshuffling of syntax.

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.

> C++'s option and result types are much larger, uglier, and harder to use correctly than Rust's.

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?

I mean, have you looked at them?

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++."

> I mean, have you looked at them?

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.

> it's just what it takes to do this in C++


Do you have any examples of how std::optional could be implemented more simply?

The std::optional in MSVC was implemented in a language considerably behind current C++, for obvious reasons. The need to use template metaprogramming in implementations is much less, today.

How so? Some of the "enable_if"s can probably be replaced with "requires" syntax, as well as the "conjunction_v"s and "disjunction_v"s, but the majority of the complexity seems to be in expressing the various requirements imposed by the Standard. Are there recently-added concepts (or other features) that would express them more naturally?

The substantial performance overhead, in Rust, of code to handle those types is unavoidable, even where source-level inconvenience has been minimized.

Overhead is imposed all up and down the call chain, every time through. Seems pretty relevant to me.

> 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

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.

Same here... I got through Stroustrup and thought I'm not smart enough to write this error free. Been happy with Rust ever since.

It’s all fun and games until you find the need to unsafe Rust, which opens up a bunch of eldritch monstrosities that are sometimes even harder to tame than C++.

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.)

on the other hand, that makes Rust's learning curve way way more gradual than C++. An intermediate programmer with 3 months of Rust experience can reliably and confidently cobble up the libraries together to contribute to the project. Whereas such programmer will be a heavy burden (for other seasoned devs to guide/review/feedback) in a C++ project.

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.

> It’s all fun and games until you find the need to unsafe Rust, which opens up a bunch of eldritch monstrosities that are sometimes even harder to tame than C++

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.

It is true, it certainly is.

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.

Which Stoustrup, principles or the c++ language tome?

The C++ Language book. I enjoyed it up to the first half, but then was deer in headlights for the rest. Maybe it felt like all the rules were too big to fit in my head, but it personally felt like an overwhelming language.

How do you fell about the pitfalls, like "if you inherit from a class, make sure the base destructor is virtual, or you just added a memory leak"

Some books on C++ teach you those, but I give up very early on learning languages with non-obvious expectations that offers few safeguards.

That's not even particularly good advice, which just goes to show how subtle the pitfalls are. The rule really should be "If it is at all possible to delete instances from a pointer to the base class, then you need a virtual destructor". The overhead of adding a virtual destructor to a base class with no other virtual functions is far from negligible in many cases, so the distinction is meaningful (though modern guidance tends discourage non-polymorphic inheritance in the first place anyways).

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."

In retrospect I wish C++ used fat pointers rather than inheritance for virtual dispatch, like Rust but where interfaces/traits can have fields (Rust traits don't have fields and are less powerful than C++ base classes). This prevents accidentally overriding a base class method (eg. QWidget) by declaring a subclass method of the same name, eliminates the need to add a vtable pointer to object instances which you never cast to an interface pointer, and eliminates the need to juggle pointer offsets when performing multiple inheritance method calls, which can cause UB when performing C-style pointer casts and makes decompiling the code a nightmare (possibly fields in traits will add this requirement again, I'm not sure).

That is because C++ is not taught properly. It is not just a collection of syntactical features but a multi-paradigm language (i.e. supporting Procedural, Object-Oriented, Generic and Compile-time) where a feature is best suited to a particular paradigm and can also be used to express different concepts. Once you grasp this, things become quite clear and you understand how to design in C++.

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.

>"That is because C++ is not taught properly. It is not just a collection of syntactical features but a multi-paradigm language (i.e. supporting Procedural, Object-Oriented, Generic and Compile-time) where a feature is best suited to a particular paradigm and can also be used to express different concepts. Once you grasp this, things become quite clear and you understand how to design in C++."

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++.

C++ actually has two axes; 1) Paradigm axis (i.e. Procedural, Object-Oriented, Generic, Compile-time and Functional) and 2) Usage axis (i.e. Low-level bare-metal, Kernel, Library, Application). Every feature has an ideal application at the intersection of these two axes. Another way to look at it is how best to map concepts from the Problem-domain directly onto language features. Here techniques like Commonality/Variability analysis (bedrock of DDD, Patterns etc.) are used to express concepts using specific language features and then combine those into a coherent design.

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.

Thank you for the wonderfully insightful and detailed response. This is really helpful. Cheers!

You are welcome.

For learning Modern C++, you might find these recommendations useful: https://news.ycombinator.com/item?id=32884432

Speaking of inheritance, how do you feel about this not requiring any diagnostics. It produces a weird result without containing any cast notation to subvert the type system.

  $ g++ -Wall -W -Wextra deriv.cc -o deriv
  $ ./deriv

  $ cat deriv.cc
  #include <cstdio>

  struct Base {
    double y;
    Base() : y(1.0) {}

  struct Derived : public Base  {
    int a, b;
    Derived() : a(0x3DDDDDDD), b(0x3DDDDDDD) {}

  int main()  
    Base *b = new Derived[5];
    std::printf("%g\n", b->y);

This is bad, but it is really no different than:

    int * b = new int;
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.

For me this is the least of c++’s problems. Today I wouldn’t let that past a review as it should be using a std::array. If a c++ dev has a reason to drop down to this unsafe code they should know what the consequences are.

Inheritance is a specialist technique, use of virtual members moreso. Most destructors in modern C++ are left to the compiler to generate.

It has been years since I last coded a virtual destructor.

You'd want to read more than one book. And in my opinion you also need to digest the C++ Core guidelines and also use something like clang-tidy with high sensitivity, and when issues are flagged go dig into the details.

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.

> As my first language, I've yet to experience another language that genuinely lets me be as creative as C++

common lisp is cpp's way more talented brother that got addicted to lsd and became a hippy

Try modern pure JavaScript. It's like floating through air. An absolute joy to code in.

Of course if you want to talk about performance and stuff then JavaScript is not comparable, but that is not the point

The lack of static typing will be a non-starter for many.


What eldritch horror am I going to unleash thanks to g++ today?

But I agree, C++ is the paintbrush of coding and I grow tired of my Js crayon prison.

There are hundreds of ways to do something, each one more importunely verbose and loaded with extraneous syntax than the next.

don't worry Perl will let you be 100 times more creative

I like it. It uses uncomplicated language, doesn't try to frontload you with the complexity beneath, and shows clean and well-principled code (e.g. no `using namespace std;` - they say that all code snippets compile).

And I say this as someone who gets a kick out of template metaprogramming.

Yes, you will commit atrocities if you try to learn C++ from just this resource. It is very much a nice facade around the bottomless pit that is C++. But in terms of getting someone started on their first small C++ projects, this is extremely to-the-point, and infinitely better than requiring people to dig through "how do I read a file" StackOverflow answers or needlessly elaborate articles that show terrible code "for the sake of exposition".

It might go against the intent of the site, but some kind of notice like "C++ is big and has many sharp edges, so you should eventually look to curate your understanding beyond this site" would round it out.

The "official" reference if anyone is interested: https://en.cppreference.com/w/

"Official", quote, alludes to very active curation by participants in the standardization process, with example code frequently updated to reflect changing best practice.

Recognized best practice changes with both experience and with newly implemented language features. New features are proposed, and some standardized, specifically because they enable better ways of coding. So, best practice may change markedly with each new Standard.

Generally, the newest way to do something is usually best, for exactly that reason. (A common misconception has one sticking with older language features for better code. The reductio ad absurdum of this would prefer the C subset.) This does not oblige you to use every feature in every program; e.g., std::shared_ptr has sharply limited value. And, the absolutely newest features may not be very portable yet.

Sometimes more field experience with a feature and its interaction with others leads to changes to best practice. This should be expected particularly in use of concepts and asynchronous facilities.

As an example of change, the present universal preference for "std::" vs "using" took a long time to gel because writers (of articles, slides, textbooks) begrudged the extra space it took on their page.

Examples found on cppreference may usually be relied upon to produce sound results and to steer well clear of both risky usage and bad performance.

Definitely THE website to bookmark if you're interested in C++ at all.

Notice that due to SEO fuckery, your first search result might be cplusplus.com, which is not the same and not as good. cppreference.com is where it's at.

And for those who want offline copy: https://en.cppreference.com/w/Cppreference:Archives

Definitely the best C++ reference. It's not official in the sense that it is not the authoritative standard text itself, but it is very precise and complete.

So far I’ve used six languages to build everything from web apps to desktop applications and C++ is still by far my favourite language.

It is sad that it is still haunted by prejudice due to pre-C++11 era but modern C++ is easy to learn and use, and I recommend every programmer should give it a try.

The problem is that many keep teaching C++ with code that a C compiler won't have any issues with.

That is why Bjarne keeps doing those advocacy talks about best practices.

Related to this, maybe: an issue I had was also the fact that online many of the help pages are really outdated.

I can't forget an old accepted "much voted" answer I saw on stackoverflow which claimed that raw pointers should be owning resources, because that's how the language always did.

I think the answer was dated like 2008 or so, can't find the reference anymore. That's when I started to pay a lot more attention to the published date whenever I search for help and I stumble upon a very old article. If only we could get rid of old resources written before maybe 2018-2019 and only select those that are still "modern", that'd be already a great step forward.

Luckily we have this now [0].

They should invest more in tools like the GSL extension at least as a clang warning which really helps, and things like that. That's how I believe Java was also able to modernize. You can write pretty crappy Java too, but tools like findBugs or checkstyle do help, so why not!

[0]: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...

That is what using VC++ with integrated support for Core Guidelines feels like, naturally that is a minority.


Yeah, I kind of envy that.

Although with Clang-tidy I sometimes see GSL warnings: https://clang.llvm.org/extra/clang-tidy/ which is pretty cool - I wonder how up-to-date that is compared to the GSL extension.

The more you invest in this kind of automated tools, the earlier you catch such bugs. Sure, sometimes it's "false positives, I know what I am doing", but you cut maybe a good > 90% of crap that comes from just writing old-style code.

The real problem as I see it is that people think the path to safe C++ is to tell everyone "you're holding it wrong" (just use modern C++), when it fact being able to hold it wrong is regarded as a feature (C++ maintains backward compatibility) by the C++ community.

By C expats on C++ country.

Nice educational resource. Things like this help a lot relative to cppreference which often assumes the reader knows many things already. For example:



The former spells out the rationale for the design - such as why you might need get or why make_unique is better than new, and why copies are forbidden but moves are allowed - that the official reference sort of assumes the reader already understands.

I always loved C++ as a language. Most of the use cases for it nowadays are when performance is key, and people use python for everything else. But I miss the structure that C++ provides without the heavy hand that Java and C# require. I definitely do not miss memory leaks and investigating core dumps though.

Good news, with modern usage, memory leaks are a thing of the fast-receding past. Occasion to investigate core dumps is lately vanishingly rare. (I have spent strictly more time filing compiler bug reports, in recent years, than debugging memory usage violations.)

Today, it is fun to code in C++. Restricting it to performance-critical uses would be a bad mistake. Displacing C with simpler, more reliable, often faster C++ is an easy choice, but reaching for C++ in place of Python is also often a great idea.

Yeah I literally have not had a single memory leak since I started using unique_ptr and shared_ptr everywhere.

I have had a couple resource leaks with C APIs, though even they could have been avoided with unique_ptr and a custom deleter.

Hmm, not sure I'd trust this. I took a look at the shared_ptr page and didn't see it mention reference cycles or weak_ptr


I have an other gripe with that page:

> [about std::make_shared] This function is useful because objects might throw exceptions during construction and if they do we still need to call delete on the pointer we received from new otherwise we will leak the pointer.

This is oversimplified and just plain incorrect. There was never a possibility that a statement like `auto sh_ptr = shared_ptr<Widget>(new Widget);` could leak.

However possibility for leak existed for calls like `foo(shared_ptr<Widget>(new Widget), shared_ptr<Widget>(new Widget))`, because evaluation of function arguments were unsequenced. This hole was fixed in the language in C++17, where function arguments became indeterminately sequenced.

And possible advantage for `make_shared` is the shared allocation for the control block and the object. It is not a clear advantage, as remaining `weak_ptr`s keep the whole allocation alive, even after the object itself is destroyed. But I wouldn't include this to a tutorial.

One annoying place where you can't use `make_shared` and `make_unique` is for invoking private constructors, even in a context where you could use the private constructor directly (like a static factory function within the same class). Since `make_shared` and `make_unique` are typically not friends of your class, they can't call the constructor indirectly. There are some workarounds for this, but the easiest way is just to use `new` here, it's fine.

It seems more robust to just make std::make_*() a friend function than to pepper the code with news.

That only works if `std::make_*` is actually where `new` is called. It could just as well be called deeper in some library implementation detail like `std::__gnu_cxx_v3::__construct_as(...)` (made up example), in which case you're SOL.

Hmm... Good point. I don't think it's a portable solution.

It also would allow anyone to construct your class via make_* which kind of defeats the point of making the constructor private in the first place.

I agree. If you can, just use the make_* functions. It also reduces needless repetition. For `make_*<Widget>(args...)` I need to write the type once, for `*_ptr<Widget>(new Widget)` I need to write the same type twice. Not counting cases for `Base/Derived`.

The site is very clearly not a complete guide, and has to leave out 90% of what a competent C++ programmer should know about any given topic (in order to remain accessible). I mean, the word "undefined" only appears in two of the examples.

But I'd say it doesn't teach any bad (or outdated) habits, which is a big step up from most tutorials/articles/SO answers.

Yeah idk, I prefer when C++ teaching resources tell the reader where the bodies are buried. I couldn't give this to juniors and trust them not to leak memory

Agree balancing accessibility and comprehensiveness is difficult though. I could see an argument for leaving shared ptr out of this guide if they think cyclic referencing is too advanced, and I can also see the argument that without knowing shared ptr exists, the user may write hacks much worse to get around the problem

I'm surprised this comments section isn't made up of people talking about how we should never use C++ again and only use Rust from now on.

That only happens in specially marked Rust-advocacy posts.

More people pick up C++ to use professionally in any given week than the total population of working Rust coders. Rust is practically invisible outside hipster media.

We should minimize the use of C and C++, and use safer languages instead, regardless of which.

However in many domains the only mature options are C and C++, in which case my option will always be C++.

There are lots of areas where Rust is still quite behind managed languages and C++, in IDE tooling, libraries, infrastructure.

this page reminds me of https://learnxinyminutes.com/

Learn X in Y minutes in my go-to when I’m writing code in a new language. Just the examples of syntax of a language are very useful and surprisingly hard to come by in formal documentation.

I must be a Scheme "snot", because the definition of "lambda" in C++ (and other languages) is not semantically what lambda in Scheme is.

(There's a different definition of closure than in Scheme, with different implications as a result.)

C++ lambda is particularly adapted to the needs of C++, and C++ programmers. It would be very surprising if it matched any Lisp's. Lisps differ from one another, too.

This looks like a nice start for a good guide. Some hierarchy for the examples would be nice, instead of an alphabetically sorted list. There are some minor inaccuracies here and there, but overall looks alright.

Is there something like this for Java?

I feel like the more popular languages get the more people write blogspam that gets in the way.

E.g. If you search "libs for rust" you get https://lib.rs/ but try doing "libs for java" and uh, it's not great.

I think that probably has more to do with the fact that Rust libraries basically all come from the same place, since there's been centralized package management since the very early days. It's a lot harder to support something like that after the fact than designing for it in advance; new languages like Rust have an advantage in that regard.

Where's the template metaprogramming section?

What about the double bracket operator?


Or, to put it another way: very nice website for basic exposure but mostly demonstrates very basic stuff.

Yeah, it's fairly basic, and most of this is in the usual c++ docs like cppreference.com

But... Is template meta-programming a required feature of a C++ dev?

double-bracket is ... an edge case of usual [] overloading, isn't it?

These are just examples to make a point.

And: https://arne-mertz.de/2016/12/modern-c-features-attributes/

Templates are a major feature of the language, leaving it out seems a little weird.

Looks like an ongoing work so more will probably be available in the future. I see an example for templates. Template metaprogramming is more advanced but would be nice to include.

That's just templates.

I'm talking about stuff like this:


It has been years since I was tempted to template metaprogramming. It is a specialist technique being made redundant by straightforward language features that compile much faster.

Yeah. I've never seen this in practice except as premature optimization or solving a problem in code that can be solved easier elsewhere.

I don't see what special about double brackets? It's just calling the operator again on the result, completely intuitive.

He means attributes, eg [[nodiscard]]. Of course you shouldn't call them operators.

See, I assumed they meant the >> operator. That used to (long ago) be cause for grief in e.g. std::vector<std::vector<int>> where the parser would see a >> operator instead of two closing angle brackets (since fixed by changing the grammar).

So, there are four things which might get colloquially called "Brackets", they each have other names but...

(Round) brackets are Parentheses

{Curly} brackets are Braces

<Angle> brackets are Chevrons

[Square] brackets are often just Brackets

Ah, so there's the problem. Now, this actually also differs depending on English variant. In England it would be pretty normal to say "Brackets" and mean Parentheses, whereas in the US you'd mean the square brackets. Some English speakers outside the US might call the Square Brackets "Crotchets" to distinguish them, not so many. US English dominates in technology.

In Firefox the code text is only readable in dark mode. In Light mode it's dark grey on black with a bright page around it.

Cool site! I rarely use C++ now, but I still miss C++ from time to time!

this is great

They should do: C++ by Problems.

Applications are open for YC Winter 2023

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact