Remix.run Logo
charleslmunger 8 months ago

As a relative newcomer to C++, I have found RAII to be fine for writing in object-oriented style. But I would only subject myself to the suffering and complexity of C++ if I really wanted excellent performance, and while RAII does not generally have a runtime cost by itself, engineering for full performance tends to exclude the things that RAII makes easy. If you manage memory via arenas, you want to make your types trivially destructible. If you don't use exceptions, then RAII is not necessary to ensure cleanup. In addition, use of destructors tends towards template APIs that generate lots of duplicate code, when an API that used a C-style function pointer for generic disposal would have produced much smaller code.

And C++'s object model can add additional complexity and sources of UB. In C++20 previously valid code that reads a trivially destructible thread_local after it has been destroyed became UB, even though nothing has actually happened to the backing storage yet.

rerdavies 7 months ago | parent [-]

As an old-timer, I think you have some serious misconception about how RAII works, and what it does for you.

> Arena management

There's nothing that stops you from using arena allocators in C++. (See pmr allocators in C++17 for handling complex non-POD types).

> The cost of RAII_

you're going to have to clean up one way or another. RAII can be zero-overhead, and usually generates less code than the C idiom of "goto Cleanup".

> Use of destructors leads toward template APIs.

Not getting that. Use of destructors leads to use of destructors. Not much else.

> If you don't use exceptions....

Why on earth would you not use exceptions? Proper error handling in C is a complete nightmare.

But even if you don't, lifetime management is a huge problem in C. Not at all trivial to clean things up when you're done. Debugging memory leaks in C code was always a nightmare. The only thing worse was debugging wild memory writes. C++ RAII: very difficult to leak things (impossible, if you're doing it right, which isn't hard), and if it ever does happen almost always related to using C apis that should have been properly wrapped with RAII in the first place.

Granted, wrapping C handles in RAII was a bit tedious in C++89; but C++17 now allows you to write a really tidy AutoClose template for doing RAII close/free of C library pointers now. Not in the standard library, but really easy to roll your own:

    // call snd_pcm_close when the variable goes out of close.
    using snd_pcm_T = pipedal::AutoClose<snd_pcm_t*,snd_pcm_close>;

    snd_pcm_T pcm_handle = snd_pcm_open(....);

> C++ 20 undefined behavior of a read-after-free problem.

That's not UB; that's a serious bug. And C's behavior would also be "UB" if you read after freeing a pointer.

charleslmunger 7 months ago | parent | next [-]

>As an old-timer, I think you have some serious misconception about how RAII works, and what it does for you.

I appreciate the education :-)

>There's nothing that stops you from using arena allocators in C++.

This is true, but arenas have two wonderful properties - if your only disposable resource is memory, you don't need to implement disposal at all; and you can free large numbers of individual allocations in constant time for immediate reuse. RAII doesn't help for either of these cases, right?

>Use of destructors leads to use of destructors

I guess what I mean is... It's totally possible and common to have a zillion copies of std::vector in your binary, even though the basic functionality for trivially copyable trivially destructible types is identical and could be serviced from the same implementation, parameterized only on size and alignment. Destruction could be handled with a function pointer. But part of the reason templates are used so heavily seems to be that there's an expectation that libraries should handle types with destructors as the common case.

>lifetime management is a huge problem in C. Not at all trivial to clean things up when you're done.

Absolutely true if you're linking pairs of many malloc and free calls. But if you have a model where a per-frame or per-request or per-operation arena is used for all allocations with the same lifetime, you don't have this problem.

>And C's behavior would also be "UB" if you read after freeing a pointer.

The specific issue I ran into was the destructor of one thread_local reading the value of another thread_local. In C++17 the way to do this was to make one of them trivially destructible, as storage for thread locals is released after all code on that thread has finished, and the lifetime for trivially destructible types ends when storage is freed. In C++20 this was changed, such that the lifetime of a thread local ends when destroyed (rather than when storage is freed) if it's trivially destructible. C thread local lifetimes are tied to storage only and don't have this problem.

badmintonbaseba 7 months ago | parent | prev [-]

You can use `unique_ptr` with a custom deleter for wrapping C libraries.

  using snd_pcm_T = std::unique_ptr<
    snd_pcm_t,
    decltype([](void* ptr){snd_pcm_close(ptr);})
  >;
  auto pcm_handle = snd_pcm_T(snd_pcm_open(...));
The lambda in decltype is C++20, otherwise the deleter type could be declared out-of-line.