[RFC] Allow `[[gnu::cleanup]]` to work with `[[clang::overloadable]]`

Background and motivation

Currently, there are 2 main ways to have a “generic” way to automatically cleanup at out of scope depending on the type of the variable:

#include <stdio.h>
#define $automatic(T) [[gnu::cleanup(T##_cleanup)]] T

static void int_cleanup(int *i)
{
    printf("%p: %d\n", i, *i);
}

int main()
{
    $automatic(int) i = 10;
}

or

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#define $automatic [[gnu::cleanup(deallocate)]]

struct Allocation {
    void (*free)(void *);
    uint8_t data[];
};

void *allocate(size_t size, void (*free)(void *))
{
    struct Allocation *allocation = malloc(sizeof(struct Allocation) + size);
    allocation->free = free;
    return allocation->data;
}

void deallocate(void *ptr)
{
    struct Allocation *allocation = (*(void **)ptr) - sizeof(struct Allocation);
    allocation->free(allocation->data);
    free(allocation);
}

static void int_cleanup(int *i)
{
    printf("%p: %d\n", i, *i);
}

int main()
{
    $automatic int *i = allocate(sizeof(int), (void(*)(void *))int_cleanup);
    *i = 42;
}

The first solution requires a macro to concat a type to make an identifier, which wouldnt work for types which aren’t valid identifiers, i.e struct MyStruct or int *.

The second solution isn’t very type-safe, and requires an allocation and a fat pointer.

Clang has [[clang::overloadable]] in order to bring C+±like overloading to C

#include <stdio.h>

[[clang::overloadable]]
static void print(const char *str)
{
    printf("%s\n", str);
}

[[clang::overloadable]]
static void print(int num)
{
    printf("%d\n", num);
}

[[clang::overloadable]]
static void print(float num)
{
    printf("%f\n", num);
}

int main()
{
    print("Hello, World!");
    print(42);
    print(3.14f);
}

Right now it is an error to use [[clang::overloadable]] alongside [[gnu::cleanup]]

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#define $automatic [[gnu::cleanup(cleanup)]]

[[clang::overloadable]]
static void cleanup(const char *s)
{
    printf("(const char *) %p: \"%s\"\n", s, s);
}

[[clang::overloadable]]
static void cleanup(int *i)
{
    printf("(int) %p: %d\n", i, *i);
}

[[clang::overloadable]]
static void cleanup(float *i)
{
    printf("(float) %p: %f\n", i, *i);
}


[[clang::overloadable]]
static void cleanup(void *p)
{
    printf("(void *) %p\n", p);
}

int main()
{
    $automatic int i = 4;
    $automatic float f = 4.2;
    $automatic const char *s = "Hello, World!";

    $automatic double d = 4;
}

This results in

error: 'cleanup' argument 'cleanup' is not a single function

Proposed Solution

Allow for cleanup to deduce which cleanup function to call by the type of the variable. void * should be a “wildcard” for any non-overloaded cleanup types.

With the proposed solution, the above code should output:

(int) <some address>: 4
(float) <some address>: 4.200000
(const char *) <some address>: "Hello, World"
(void *) <some address>

Rationale

This would make creating safe cleanup for types much easier to use and create, for example:

//I only need to overload `cleanup` in order to create a type which would automatically deallocate with this system

#define $automatic [[gnu::cleanup(cleanup)]]

struct MyStruct {
    int i;
    float f;
};

[[clang::overloadable]]
static void cleanup(struct MyStruct *s)
{
    printf("(struct MyStruct) %p: { .i = %d, .f = %f }\n", s, s->i, s->f);
}

int main()
{
    $automatic struct MyStruct s2 = { .i = 4, .f = 3.14 };
}

Thanks for bringing up this topic! The only thing that gives me pause is the fallback behavior via void *. On the one hand, these attributes are focused on C code and the address of just about anything can be passed as a void *, so it makes sense that it would be a catch-all in the overload set. On the other hand, if you need to handle cleanup functionality based on the type of what’s passed, then the size of what you need to clean up is really important and void * doesn’t carry enough information in that case.

Also, I think the behavior may be a bit too surprising for folks. Consider something like:

[[clang::overloadable]] void cleaner(int *);
[[clang::overloadable]] void cleaner(float *);
[[clang::overloadable]] void cleaner(void *);

$automatic short s = 2;

I can imagine users expecting that to call cleaner(int *) because short will promote to int. But it actually calls cleaner(void *) due to picking the overload by pointer.

I sort of wonder if another design to consider is that we’d allow anything in the overload set except for delegating to void *. (If there’s not an overload set, then it’s fine for void * because it’s clear what’s being called.)

Thank you very much for responding!

The fallback behaviour is how it currently works right now (quite annoyingly sometimes)

#include <stdio.h>
#include <stdlib.h>

[[clang::overloadable]]
void cleaner(int *ptr)
{
    printf("(int *)%p: %d\n", ptr, *ptr);
}

[[clang::overloadable]] void cleaner(float *f)
{
    printf("(float *)%p: %f\n", f, *f);
}

[[clang::overloadable]] void cleaner(void *ptr)
{
    printf("(void *)%p\n", ptr);
}

int main()
{
    int i = 42;
    float f = 3.14;
    short s = 0x7FFF;

    cleaner(&i);
    cleaner(&f);
    cleaner(&s);
    cleaner("Hello, World!");
}

outputs

(int *)0x7ff7b2b499dc: 42
(float *)0x7ff7b2b499d8: 3.140000
(void *)0x7ff7b2b499d6
(void *)0x10d3b8a80

I agree with your rationale of why it wouldn’t be a good idea though

Yeah, I’m aware that’s how overloadable works in isolation. Because cleanup passes a pointer to the object that needs cleaning, I was talking specifically about how cleanup should work when given a function designating an overload set – with overloadable by itself, the user passes the arguments directly and so the overload set is easy to reason about, but the same is not true with cleanup.

Ohhh I see what you mean, cv qualifiers would also be included in this issue right? Maybe then, allowing void ** for any pointer type? i.e

#include <stdio.h>
#include <stdlib.h>

[[clang::overloadable]]
static void cleanup(int **ptr)
{
    printf("(int **)%p: (int *)%p: (int)%d\n", ptr, *ptr, **ptr);
    free(*ptr);
}

[[clang::overloadable]]
static void cleanup(void **ptr)
{
    printf("(void **)%p: (void *)%p\n", ptr, *ptr);
    free(*ptr);
}

#define $automatic [[gnu::cleanup(cleanup)]]
int main()
{
    $automatic int *pt = malloc(sizeof(int));
    *pt = 42;

    $automatic short *pt2 = malloc(sizeof(short));
    *pt2 = 654;
}

Yes, things like:

void func(int **ipp);
void foo() {
  __attribute__((cleanup(func))) const int *ip;
}

also trips users up because it gives an error while:

void func(void *vp);
void foo() {
  __attribute__((cleanup(func))) const int *ip;
}

“works” fine.

I thought on this a bit overnight and I’m not certain we want to be too clever here. If we’re going to support overload sets, then I think you get whatever behavior you’d usually get out of having a void * function included in the set.

Users can already simulate this today without the overloadable attribute: Compiler Explorer and so I think it’s fine for users to use the attribute to avoid having to pick unique names for the cleanup functions even if the behavior may be surprising in the case of having a void * in the overload set.

So, the decision is to allow fro void * cleanups for overloads?

I think so, yeah.

I’m late here, but I sort of wonder if having NO fallback is acceptable/workable? Treat this like a normal overload set, and if you try to use a cleanup where no member of the overload set is valid, it is an error.

Then, you’d be required to either add a new overload, or change your type.