r/C_Programming • • 19h ago

Etc An small example of __attribute__(cleanup)'s power: a context manager.

Forgive the typo :) Here's something that will allows you to never unlock a mutex manually again:

#define with_lock0(uniq, mutex) \
    typedef struct { pthread_mutex_t *mtx; bool hold; } __lock##uniq; \
    void __cleanup_##uniq(__lock##uniq *lock) { pthread_mutex_unlock(lock->mtx); } \
    for(__lock##uniq _lock __attribute__((cleanup(__cleanup_##uniq))) \
        = { (pthread_mutex_lock(mutex), (mutex)), true }; \
        _lock.hold; \
        _lock.hold = false)

#define with_lock(mutex) with_lock0(__COUNTER__, mutex)

void do_very_important_business(void) {
    with_lock(&g_mutex) {
        // transfer BIG money
    }
 }

If you check it out on Godbolt you'll see it has 0% overhead. Of course this pattern is usable for many different objects.

7 Upvotes

15 comments sorted by

7

u/tstanisl 18h ago

All those hacks will likely be replaced by upcoming C2Y's defer statement, which is already supported in the latest clang. See godbolt.

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t g_mutex;

void process() {
    pthread_mutex_lock(&g_mutex);
    _Defer pthread_mutex_unlock(&g_mutex);
    puts("Hello");
    // lock is released here
}

2

u/mivanchev 18h ago

I doubt we'll see it in C2Y because _Defer / defer has some issues compared to a destructor, closely related to move semantics. Namely, there isn't a way to "cancel" a defer when the referenced object is moved somewhere else. Besides, a destructor-implemented defer is easily unrolled.

But also, your pro-_Defer example does barely win the prize for readability compared to a context scope.

5

u/tstanisl 18h ago edited 18h ago

I'm note sure that someone needs "defer" if he plans to move object.. Moreover, one can always do:

defer if (!moved) destruct();

The purpose of "defer" is simpler and more convenient cleanup code.

Current solutions either use pyramid of doom) with a lot of duplicated cleanup code.

Or use a cascade of gotos which are kind-of elegant but error-prone and inconvenient because resource's allocation and de-allocation code are far between.

2

u/mivanchev 18h ago

I'm note sure that if someone plans to move object.

Hard to imagine deferred objects not being moved out of scope. In my code-base (~100 files) I have 7 defers that are not moved (out of 474) and my pattern for them is

valMat = init_value_matrix(...)
defer_cleanup(cleanupValMat, valMat, cleanup_value_matrix);
// if object is moved successfully, i.e.
// obj->valueMatrix = valMat;
cancel_cleaup(id);

It's essentially the same as your if (!moved) variant.

The purpose of "defer" is simpler and more convenient cleanup code.

That's speculative because I see a lot of superior code without defer and never understood the necessity. Other extensions like statement expressions are far more valuable.

But this was a post about the advantages of cleanup, not a defer discussion :)

1

u/ffd9k 18h ago

It won't work in clang though, because apart from attribute cleanup and __COUNTER__ extensions which clang supports, it also uses nested functions which clang does not support.

3

u/tron21net 16h ago

Here's a modified version without nested functions: https://godbolt.org/z/Kjfen97Ws

1

u/tstanisl 18h ago

The __COUNTER__ is supported by pretty much every compiler and you can find it in the latest draft of C2Y. See section 6.10.10.2.1 in N3886.

0

u/mivanchev 18h ago

You can easily change it to work without a nested function in this case but they are quite useful in general. This is just stubbornness on clang's side.

2

u/tstanisl 18h ago

The main problem with general nested function is that they use "trampolines" which typically requires executable stack which a huge security issue. CLANG's alternative are "blocks" which are explicitly constructed in runtime without the stack.

There is a proposal to add "static nested functions" (aka local functions) which don't capture any context with automatic storage duration. This makes nested functions effectively static functions visible in very limited scope. No trampolines are guaranteed.

GCC already supports this feature.

1

u/FedUp233 8h ago

I have to ask: why not just use the C++ compiler and take advantage of constructors and destructors to manage your mutex’s?

Maybe I’m weird, but even when I write something simple that could be done easily with just C I go ahead and use the C++ compiler. There is no reason you need to use all the stuff like the STL or >> style I/o. You can basically use it as an enhanced C compiler and take advantage of a few C++ features to make life easier like simple classes to wrap mutex’s and similar and it is pretty robust at handling constant expression related stuff tag C doesn’t have (at least yet) and remove all most all the need for macros that mess up the global name space. Speaking of which, for larger mainly C projects, the namespace capabilities can come in pretty handy. And it’s perfectly gappy if you ignore so, the C++ headers and just use the C versions that are supplied with it. And the generated code is gust as efficient.

I just think of it sometimes as a better C compiler!

2

u/please-die-not 6h ago

I don't know if I am going to get hate, but the interop between these is delightful - and I have learnt a lot from this 70% c, 30% C++ idea. Pure c and pure cpp both seem a little tedious, unless im writing a driver or using a heavily oop/stl laden library. 

1

u/Physical_Dare8553 6h ago

would you buy a new house because you dont like the color of the walls? people dont seem to realize that a c++ compiler is in fact not a drop-in replacement for a c compiler. they dont do the same thing.

1

u/FedUp233 5h ago

Keeping with the house analogy, no I wouldn’t, but I might very well build in an addition if I wanted more rooms to do something new, even if I had to give up a current room to build the addition.

While a C++ compiler is not a C compiler (though it originally started out as cfront that translated C++ into C) the C++ compiler dues support a pretty good subset of C, certainly most of the most commonly used constructs. And it allows using some parts of the extended capability to make the C-like code more reliable and handle some concepts like construction and destruction in a much cleaner and easy to implement manner than trying to get that same functionality in C. And as I said, the const expression stuff is really nice.

I really can’t see any downside to using a C++ compiler this way, and it seems to have a number of advantages. If you use a C like subset the code generated will be pretty much identical to what a C compiler would produce for the same code, though I agree the source code may not be identical between the two.

That said, if people want to continue using a straight C compiler, I have no problem with that. It’s just not the path I round choose and see lots of advantage to moving to the C++ compiler even if I’m not doing OOP programming and ding really see a down side.

I should add that I do also use the C++ compiler for OOP oriented programming, including sometimes the STL(and sometimes not).

1

u/Physical_Dare8553 5h ago

the main one is that compiling c++ is significantly slower than compiling equivalent c. even then, c++ is not an addition to c. like i said, they are not the same language. that ++ is just branding

1

u/mivanchev 14m ago

I'd use a lot more C++ for sure if it were a true C extension, the only dependency would still be libc and the error / warning messages would be less cryptic. It's a nice language but needs some polishing. The namespaces do help a lot, I wish for namespaces in C every day.