Readit News logoReadit News
kjuulh · 3 months ago
I am by no means a C++ expert, a noob rather. It might be possible to make this generic, but it seems quite easy to forget a small detail here, and then be kneecapped because you forgot one of the overloads. So that in very few amount of edge cases your abstraction isn't actually cleaned up.

Better than nothing, and might be the most preferred way of doing things in C++, but it does seem dangerous to me. ;)

znkr · 3 months ago
I used to program a lot in C++ but switched to a number of different programming languages since then. Everything in C++ is this way and it’s hard to understand that things don’t have to be this way when you’re in the trenches of C++.
jiggawatts · 3 months ago
I distinctly remember what a breath of fresh air it was to switch to Java and then later C# where in both languages an "int" is the only 32-bit signed integer type, instead of hundreds of redefinitions for each library like ZLIB_INT32 or whatever.
green7ea · 3 months ago
C++ is a difficult language to use well but it becomes a lot easier when you turn on the 'correct' warnings. Turning on the `-Wdeprecated-copy-dtor` warning would help you not forget one of the cases in the rule of 3.

I would have loved it if this warning was on by default but sadly, you have to know to turn it on.

90s_dev · 3 months ago
Are are all the ideal warnings?
bluGill · 3 months ago
It is possible, but it isn't hard to remember the rules. The scope of where you can mess up is isolated to the single class so it is generally possible to get this right. Manual memory management is easy when the new and delete are near each other, but you often need them far removed. RAII ensures the places you can mess up are close.

A static analisys tool can enforce the rule as well- you should have one.

unfortunately though this is one area that because c++ predates RAII it can't changethe defaults without breaking a lot of code. I am saying the problem is manageable - there is a real problem though. If you make a new language learn from this mistake.

pjmlp · 3 months ago
How can C++ predate the idea it helped to make mainstream regarding resource usage on the 1990's?
nathell · 3 months ago
My pet peeve with C++ RAII is that it uses destructors for cleanup, so it can’t throw an exception from within the destructor if that fails somehow. E.g. from the manual page of close(2):

> A careful programmer will check the return value of close(), since it is quite possible that errors on a previous write(2) operation are reported only on the final close() that releases the open file description. Failing to check the return value when closing a file may lead to silent loss of data. This can especially be observed with NFS and with disk quota.

Last time I checked, GCC’s implementation of ~ofstream() ignored failures from the underlying close().

flohofwoe · 3 months ago
This is also a problem when an object referenced by a shared_ptr is destroyed much later than expected because some obscure part of the code still holds a reference to the object.

That's why in many situations it makes sense to have an explicitly called discard() method, while the destructor is 'empty' and only checks that discard has actually been called.

Of course that also means that RAII isn't all that useful for such situations.

bluGill · 3 months ago
One more reason to aviod shared pointers. They are sometimes useful enough as to be worth it, but they make code analisys harder.
1718627440 · 3 months ago
Why can't you simply call the destructor, where you would call discard?
90s_dev · 3 months ago
About a year ago I bought the official C++ book and read through it fully (including most of the reference part) after finding out that Animal Well was written in it. It honestly seems like a very good and interesting langauge, if carefully used, especially for games where memory can be allocated at startup and held forever. Kinda wish I had an excuse to write some C++.
ninkendo · 3 months ago
> Kinda wish I had an excuse to write some C++.

I had the same feeling in grad school back ~15 years ago… I could pick any language for my coursework, so I picked C++ purely because it seemed an important language to learn. I was also tracking C++0x (later C++11) very carefully just for academic purposes. I’ve still never used C++ at work but I don’t regret using it in grad school, learning it gives a lot of insight into tradeoffs when designing a language, and I felt it was a lot easier to pick up Rust later on having already known C++.

yahoozoo · 3 months ago
The color/holding all over this article is annoying.
green7ea · 3 months ago
Author here, I put that in place for people who skim articles or paragraphs — this is a surprising amount of people.

Would it be better if it was more subtle? Were you reading in dark or light mode?

yahoozoo · 3 months ago
Probably just less of it. I see bolding like that and find myself clicking on them thinking they are links. It seems like a few of them actually do have links though.

Deleted Comment

90s_dev · 3 months ago
Why don't we just use shared_ptr most of the time? Is it really that inefficient?
grumbel · 3 months ago
With shared_ptr you completely lose track who actually owns an object or when it will get destructed. It encourages sloppy programming and makes code much harder to read and reason about.

Unlike most languages that have ref-counting build in, shared_ptr also doesn't provide anything to deal with cyclic dependencies, so you can end up with memory leaks.

The most important reason however is simply that you don't need it like 99% of the time, unique_ptr provides enough functionality to work just fine as a shared_ptr replacement in most situations. And in the rare cases where you really need a shared_ptr, you can just convert a unique_ptr into one.

green7ea · 3 months ago
Author here, parent comment describes it very well — shared_ptr are a last resort, not a first one.

They are quite heavily (and badly) used in some code bases (ROS). I'm planning a future article that covers shared_ptr in more details.

The surprising thing about shared pointer is that a `const shared_ptr<T>` means that you can modify the contents of T. This makes the problem, mentioned in the parent, of keeping track of who can modify the object where impossible. I've never encountered a `const shared_ptr<const T>` but that would be a better approach.

ajross · 3 months ago
For the same reason that you don't use Rc for everything in Rust. Putting all your heap management behind reference counted pointers isn't "that inefficient", no. But if you don't need the direct control over heap behavior, you shouldn't be using C++ (or Rust) in the first place.

Languages with GC-managed runtimes (Java, C#, Go, Swift, et. al.) are actually significantly more performant for almost all heap-bound use cases, actually. Reference counting kinda sucks for typical code.

ninkendo · 3 months ago
> Languages with GC-managed runtimes (Java, C#, Go, Swift, et. al.)

Swift does not have a GC managed runtime in the same sense as the other languages you listed [0], it’s basically a bunch of refcounts inserted by the compiler, with similar performance characteristics to Arc in Rust or shared_ptr in C++. (For classes, at least. Structs are value types and stack-allocated.)

[0] Yes, automatic refcounting is a form of garbage collection, but Java/C#/Go use tracing GC’s and not direct refcounting, whereas with Swift it’s more like the compiler is wrapping all objects in a shared_ptr for you, and so destruction is explicit and happens at exact points.

pjmlp · 3 months ago
Manual reference counting to be more precise.

When a GC-managed language uses reference counting as implementation algorith, the compiler might be able to optimize the reference counting to only occur when it is unavoidable or too costly to reason about, in a similar vein to bounds checking.

When using library types for reference counting, there is no way for the compiler to implement such optimizations, unless the types are somehow tagged with compiler intrisics so that they could apply the same kind of optimizations.

William_BB · 3 months ago
I take the following approach: - Stack by default - Unique ptr if needed on the heap - Shared ptr if needed to share ownership

Although unique ptr is zero cost after make_unique(), I avoid polluting my heap unnecessarily. I've never benchmarked this though (keeping various objects on stack vs heap as unique ptr and how that impacts memory accesses)

I'm quite junior. Appreciate anyone pointing out if anything I said doesn't make sense.

grues-dinner · 3 months ago
> Stack by default - Unique ptr if needed on the heap - Shared ptr if needed to share ownership

Sounds about right. Shared ownership is fairly rare though, and you often only need shared access (reference/pointer if nullable) and can provide other, more explicit, ways of managing the lifetime.

> unique ptr is zero cost after make_unique()

Kind of, but compared to the stack, it could cause caching inefficiency because your heap-allocated thing could be almost anywhere, but your stack-allocated thing is probably in the cache already.

green7ea · 3 months ago
Author here, that's a good approach :-). I see shared_ptr as a code smell since shared ownership makes life difficult.
flohofwoe · 3 months ago
Because shared_ptr (and also unique_ptr) nudges you towards keeping each object in its own heap allocation, and that quickly gets inefficient with large number of objects. E.g. a handful of large objects managed through shared_ptr is usually fine, but managing many tiny C++ object individually through smart pointers usually isn't. Instead store large groups of objects of the same type and similar lifetime by value in a std::vector (and then maybe put that std::vector behind a smart pointer).

Also if you lean in too much on shared/unique pointers for large amounts of tiny objects you'll most likely end up in a situation where Java-style garbage collection would be more efficient.

E.g. manual or semi-manual memory management is mostly about controlling the overall memory layout of your application's data to improve throughput, reducing the number of individual heap allocations is just a useful side effect.

fh973 · 3 months ago
Yes, it is very inefficient, as it is thread-safe and uses atomic operations internally.

So instead of just accessing memory, you have the cost of cross-cache coherency operations between CPU cores.

znkr · 3 months ago
It’s mostly fine, until you run into memory leaks due to circles or because some part of your program holds onto the root of some large shared pointer graph and you have no idea which part. If you take it very far, like some code bases I worked with did, you discover that everything needs to be shared pointer now, because most lifetimes are no longer explicit but implicitly defined by the life time of the shared pointer that holds it.
majoe · 3 months ago
Reasons I can think of:

- Wrapping all objects in shared pointer is annoying. - If you stick to that convention, you have to do it on every call side, while you only have to implement RAII once. - You can enforce invariants of your class with RAII, that you can't with a plain shared_ptr - Regarding efficiency: It has the overhead of reference counting plus you have to store all objects on the heap instead of the stack. In hot loops this may hurt cache locality.

bluGill · 3 months ago
Most of the time unique-ptr is faster and makes it easier to reason about your program. There is more than heap managemant to performant code and shared ptr makes it harder to reason about those other areas.
oytis · 3 months ago
Why, we do. Whenever there is data on heap that needs to be shared that is
rjinman · 3 months ago
unique_ptr is much better because then each object has a sole owner, which makes object lifetimes much easier to reason about and you can't end up with cyclic references causing memory leaks.
pjmlp · 3 months ago
It is a diservice to C++, to paint RAII as modern C++.

We were already making use of RAII in C++ compilers for MS-DOS during the C++ARM (the C++ equivalent of K&R C book), one such example would be Turbo Vision C++ variant and Borland International Datastructures Library (BIDS).

If anything, it is a pity that 30 years later, it is still something we need to educate people about, as it isn't as adopted as it should be.

EDIT:

If this is supposed to be Modern C++, at the very least provide C++23 example in 2025, instead C++ from 2011.

Meaning import std, using a custom deleter in std::unique_ptr (it doesn't manage only heap types), making use of =default to get back compiler generated member functions, as it would do the right thing in the example for the fd handle.

grues-dinner · 3 months ago
Modern C++ does give you move semantics, which makes RAII a lot more expressive, especially with smart pointers.

One things about RAII that I find is that, a bit like inheritance, it's actually not something you have actually do nearly as much as the emphasis it gets in the first chapters of all the books implies. How often do you actually write a move constructor or even a destructor in workaday code? Most of it is done for you by the stdlib (unique_ptr, say) or libraries. Of course it needs to be understood, and understood well, but it feels like landing a plane: you have know it well and it's not unusual and certainly not wrong, but also flying is mostly doing things other than landings.

ninkendo · 3 months ago
RAII, like a lot of C++ concepts, finds the most use in library-like code (stdlib especially), where it needs to be super generic. Application developers get the benefits of these concepts by simply using these libraries, even if they don't have to care about them themselves. As an application developer, you mostly focus on classes as plain-old-data and functions to work on them, so you don't need to mess about with custom destructors or move-constructors or other RAII stuff.
tialaramex · 3 months ago
The C++ 11 move is terrible because it's not the move semantic people actually wanted - which is what people took to calling "destructive" move, the feature Rust provides - instead it's this weird confection where we're scooping the guts out of one object and transferring them into a new object, leaving something hollow behind in order to satisfy the requirements of 1980s C++ programs.

In some cases this can be optimised to the same machine code, but not always, and semantically it's not what we actually wanted anyway. In terms of RAII this means arranging that after the move the "moved from" object remembers it no longer has any resources and so when it is destroyed it won't clean up - much easier to just design the language properly.

AlexeyBrin · 3 months ago
How many modern compilers will let you write import std; in a C++ file and build the executable without shenanigans ? GCC 15 still does not let you compile a C++23 code with:

    g++ -std=c++23 -fmodules foo.cpp
Without manually generating the std module first. Until all compilers will directly accept the import keyword it won't be adopted on a large scale by regular programmers.

pjmlp · 3 months ago
Visual C++ and clang.

Many projects do just fine sticking to a single compiler.

Not for everyone, but they exist nonetheless, including on FOSS world.

motorest · 3 months ago
> It is a diservice to C++, to paint RAII as modern C++.

I think you're missing context. The term "modern C++" is used to refer to idiomatic C++ introduced in C++11. It has been that way for a couple of decades.

https://learn.microsoft.com/en-us/cpp/cpp/welcome-back-to-cp...

The article touches on the topic of move semantics and move constructors. That's modern c++. That's not your C++98 RAII.

In the same vein, "modern CMake" is also the term used to refer to the declarative style that followed the release of CMake 3, which dates back years.

pjmlp · 3 months ago
I also own Andrei's book, first edition.
green7ea · 3 months ago
Author here, what you say is entirely correct — I didn't emphasize enough that this was all possible before in a more limited way. Those parts were cut out to try to make the article more focused on understanding RAII.

The modern parts covered in the article are the move semantics making RAII more expressive and the standard library embracing the concept throughout.

I think the idea of modern is to convince everyone to give these 'new' techniques a try if they aren't already using them.

pjmlp · 3 months ago
It is always a good thing to spread knowledge, however when C++11 is modern, what C++14, C++17, C++20, C++23, C++26 are supposed to be?

There are already plenty of memes on C++ conferences regarding that.

stingraycharles · 3 months ago
There’s still a huge part of the industry that writes a mix of C-style C++.

Modern C++ is all about template metaprogramming, concepts, move semantics, ranges, etc.

But there’s a significant cognitive load will all of that.

bluGill · 3 months ago
The term modern C++ means RAII. Those other things are common and sometimes useful in new code that can use them. However anyone who says 'modern C++' is talking about RAII not those other things.

Deleted Comment

Dead Comment