Citação da documentação do GCC [1]:O atributo cleanup é usado para executar uma função quando uma variável sai do escopo. Este atributo pode ser aplicado apenas a variáveis automáticas e não pode ser usado com parâmetros ou com variáveis estáticas. A função deve levar um parâmetro, um ponteiro para um tipo compatível com a variável. O valor de retorno da função, se houver, é ignorado.
Se a opção -fexceptions estiver ativada, a função cleanup_function será ativada quando a pilha for desenrolada, durante o tratamento da exceção. Observe que o atributo cleanup não captura exceções; ele executa apenas uma ação. Se a função cleanup_ não retornar normalmente, o comportamento será indefinido.
O atributo cleanup é suportado pelos compiladores gcc e clang.Neste artigo, descreverei várias opções para o uso prático do atributo cleanup e considerarei a estrutura interna da biblioteca, que usa a limpeza para implementar os análogos std :: unique_ptr e std :: shared_ptr em C.Vamos tentar a limpeza da desalocação de memória:#include<stdlib.h>
#include<stdio.h>
static void free_int(int **ptr)
{
free(*ptr);
printf("cleanup done\n");
}
int main()
{
__attribute__((cleanup(free_int))) int *ptr_one = (int *)malloc(sizeof(int));
return 0;
}
Começamos, o programa imprime "limpeza concluída". Tudo funciona, aplausos.Mas uma desvantagem imediatamente se torna aparente: não podemos simplesmente escrever__attribute__((cleanup(free_int)))
porque a função chamada pelo atributo cleanup deve levar um ponteiro para a variável liberada como argumento, e temos um ponteiro para a área de memória alocada, ou seja, precisamos definitivamente de uma função que use um ponteiro duplo. Para fazer isso, precisamos de uma função adicional do wrapper:static void free_int(int **ptr)
{
free(*ptr);
...
}
Além disso, não podemos usar uma função universal para liberar nenhuma variável, porque elas exigirão tipos diferentes de argumentos. Portanto, reescrevemos a função da seguinte maneira:static void _free(void *p) {
free(*(void**) p);
printf("cleanup done\n");
}
Agora ela pode aceitar qualquer indicação.Aqui está outra macro útil (da base de código do systemd ):#define DEFINE_TRIVIAL_CLEANUP_FUNC(type, func) \
static inline void func##p(type *p) { \
if (*p) \
func(*p); \
} \
struct __useless_struct_to_allow_trailing_semicolon__
que mais tarde pode ser usado assim:DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, pclose);
#define _cleanup_pclose_ __attribute__((cleanup(pclosep)))
Mas isso não é tudo. Há uma biblioteca que implementa análogos das vantagens exclusivas unique_ptr e shared_ptr usando este atributo: https://github.com/Snaipe/libcsptrExemplo de uso (extraído de [2]):#include <stdio.h>
#include <csptr/smart_ptr.h>
#include <csptr/array.h>
void print_int(void *ptr, void *meta) {
(void) meta;
printf("%d\n", *(int*) ptr);
}
int main(void) {
smart int *ints = unique_ptr(int[5], {5, 4, 3, 2, 1}, print_int);
for (size_t i = 0; i < array_length(ints); ++i) {
ints[i] = i + 1;
}
return 0;
}
Tudo funciona maravilhosamente!E vamos ver o que há dentro dessa mágica. Vamos começar com unique_ptr (e shared_ptr ao mesmo tempo):# define shared_ptr(Type, ...) smart_ptr(SHARED, Type, __VA_ARGS__)
# define unique_ptr(Type, ...) smart_ptr(UNIQUE, Type, __VA_ARGS__)
Vamos em frente e ver a profundidade da toca do coelho:# define smart_arr(Kind, Type, Length, ...) \
({ \
struct s_tmp { \
CSPTR_SENTINEL_DEC \
__typeof__(__typeof__(Type)[Length]) value; \
f_destructor dtor; \
struct { \
const void *ptr; \
size_t size; \
} meta; \
} args = { \
CSPTR_SENTINEL \
__VA_ARGS__ \
}; \
void *var = smalloc(sizeof (Type), Length, Kind, ARGS_); \
if (var != NULL) \
memcpy(var, &args.value, sizeof (Type)); \
var; \
})
Até agora, a clareza não aumentou, diante de nós há uma mistura de macros nas melhores tradições dessa linguagem. Mas não estamos acostumados a recuar. Desvendar o emaranhado:define CSPTR_SENTINEL .sentinel_ = 0,
define CSPTR_SENTINEL_DEC int sentinel_;
...
typedef void (*f_destructor)(void *, void *);
Execute a substituição:# define smart_arr(Kind, Type, Length, ...) \
({ \
struct s_tmp { \
int sentinel_; \
__typeof__(__typeof__(Type)[Length]) value; \
void (*)(void *, void *) dtor; \
struct { \
const void *ptr; \
size_t size; \
} meta; \
} args = { \
.sentinel_ = 0, \
__VA_ARGS__ \
}; \
void *var = smalloc(sizeof (Type), Length, Kind, ARGS_); \
if (var != NULL) \
memcpy(var, &args.value, sizeof (Type)); \
var; \
})
e tente entender o que está acontecendo aqui. Temos uma certa estrutura que consiste na variável sentinel_, um certo array (Type) [Length], um ponteiro para uma função destruidora, que é passada na parte adicional (...) dos argumentos da macro e uma meta-estrutura, que também é preenchida com argumentos adicionais. Em seguida é uma chamadasmalloc(sizeof (Type), Length, Kind, ARGS_);
O que é smalloc? Encontramos mais algumas mágicas de modelos (eu já fiz algumas substituições aqui):enum pointer_kind {
UNIQUE,
SHARED,
ARRAY = 1 << 8
};
typedef struct {
CSPTR_SENTINEL_DEC
size_t size;
size_t nmemb;
enum pointer_kind kind;
f_destructor dtor;
struct {
const void *data;
size_t size;
} meta;
} s_smalloc_args;
__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args);
# define smalloc(...) \
smalloc(&(s_smalloc_args) { CSPTR_SENTINEL __VA_ARGS__ })
Bem, é por isso que amamos C. Há também documentação na biblioteca (pessoal santo, recomendo a todos que tomem um exemplo):A função smalloc () chama o alocador (malloc (3) por padrão), o ponteiro retornado é um ponteiro "inteligente". <...> Se o tamanho for 0, NULL será retornado. Se nmemb for 0, smalloc retornará um ponteiro inteligente para um bloco de memória de pelo menos tamanho bytes e um ponteiro escalar inteligente, se nmemb não for igual a 0, um ponteiro para um bloco de memória de tamanho pelo menos tamanho * nmemb será retornado e o ponteiro será do tipo array.original«The smalloc() function calls an allocator (malloc (3) by default), such that the returned pointer is a smart pointer. <...> If size is 0, then smalloc() returns NULL. If nmemb is 0, then smalloc shall return a smart pointer to a memory block of at least size bytes, and the smart pointer is a scalar. Otherwise, it shall return a memory block to at least size * nmemb bytes, and the smart pointer is an array.»
Aqui está a fonte do smalloc:__attribute__ ((malloc)) void *smalloc(s_smalloc_args *args) {
return (args->nmemb == 0 ? smalloc_impl : smalloc_array)(args);
}
Vejamos o código smalloc_impl, alocando objetos de tipos escalares. Para reduzir o volume, excluí o código associado aos ponteiros compartilhados e fiz a substituição em linha e macro:static void *smalloc_impl(s_smalloc_args *args) {
if (!args->size)
return NULL;
size_t aligned_metasize = align(args->meta.size);
size_t size = align(args->size);
size_t head_size = sizeof (s_meta);
s_meta_shared *ptr = malloc(head_size + size + aligned_metasize + sizeof (size_t));
if (ptr == NULL)
return NULL;
char *shifted = (char *) ptr + head_size;
if (args->meta.size && args->meta.data)
memcpy(shifted, args->meta.data, args->meta.size);
size_t *sz = (size_t *) (shifted + aligned_metasize);
*sz = head_size + aligned_metasize;
*(s_meta*) ptr = (s_meta) {
.kind = args->kind,
.dtor = args->dtor,
.ptr = sz + 1
};
return sz + 1;
}
Aqui vemos que a memória da variável está alocada, mais um cabeçalho do tipo s_meta mais uma área de metadados do tamanho args-> meta.size alinhada com o tamanho da palavra, mais uma palavra (sizeof (size_t)). A função retorna um ponteiro para a área de memória da variável: ptr + head_size + align_metasize + 1.Vamos alocar uma variável do tipo int, inicializada com o valor 42:smart void *ptr = unique_ptr(int, 42);
Aqui inteligente é uma macro:# define smart __attribute__ ((cleanup(sfree_stack)))
Quando o ponteiro sai do escopo, sfree_stack é chamado:CSPTR_INLINE void sfree_stack(void *ptr) {
union {
void **real_ptr;
void *ptr;
} conv;
conv.ptr = ptr;
sfree(*conv.real_ptr);
*conv.real_ptr = NULL;
}
Função sfree (abreviada):void sfree(void *ptr) {
s_meta *meta = get_meta(ptr);
dealloc_entry(meta, ptr);
}
A função dealloc_entry chama basicamente um destruidor personalizado se o especificarmos nos argumentos unique_ptr, e o ponteiro para ele for armazenado em metadados. Caso contrário, apenas gratuito (meta) é executado.Lista de fontes:[1] Atributos variáveis comuns .[2] Uma maneira boa e idiomática de usar GCC e clang __attribute __ ((cleanup)) e declarações de ponteiro .[3] Usando o atributo da variável __cleanup__ no GCC .