Come gestire mallocs ripetitivi / liberi in modo DRY in C

5

Quindi ho una funzione che è come un costruttore per la mia struttura:

MyStructure* CreateMyStructure(...)
{
    MyStructure *my_structure;
    double *data;
    int *colind, *rowptrs;

    data = malloc(sizeof(double)*N);
    if (data == NULL) { return(NULL); }

    colind = malloc(sizeof(int)*N);
    if (colind == NULL) { free(data); return(NULL); }

    rowptrs = malloc(sizeof(int)*K);
    if (rowptrs == NULL) { free(colind); free(data); return(NULL); }

    my_structure = malloc(sizeof(my_structure));
    if (my_structure == NULL) { 
        free(rowptrs); free(colind); free(data);
        return(NULL);
    }

    return(my_structure);
} 

Questo schema si ripresenta in un sacco di codebase, quindi sto cercando di trovare qualcosa di più pulito. Ho pensato di creare alcune macro come queste:

#define CHECK_MALLOC1(var, dep1) \
    do { \
        if (var == NULL) { free(dep1); return(NULL); } \
    } while(0)

#define CHECK_MALLOC2(var, dep1, dep2) /* pattern continues */

Tuttavia, ho deciso contro di loro perché non voglio cambiare il flusso di controllo all'interno di una macro. Esiste un modello alternativo che potrebbe aiutare a ridurre la ripetizione senza sacrificare la capacità di alcuni di capire cosa sta succedendo?

    
posta cbalos 13.07.2018 - 21:00
fonte

3 risposte

8

Dato che C non ha RAII, usando goto per la gestione degli errori (solo saltare in avanti verso il gestore degli errori, non tornare mai indietro) è consuetudine, specialmente nel codice ad alte prestazioni ad alta affidabilità come i kernel.

Quando si usa invece lo stato, le funzioni aggiuntive o la struttura contorta, il pericolo è che il compilatore non possa in linea e de-convolutare in modo affidabile tutto.

Il tuo codice riscritto:

MyStructure* CreateMyStructure(...)
{
    MyStructure *my_structure;
    double *data;
    int *colind, *rowptrs;

    data = malloc(N * sizeof *data);
    if (!data) goto exit_0;

    colind = malloc(N * sizeof *colind);
    if (!colind) goto exit_1;

    rowptrs = malloc(K * sizeof *rowptrs);
    if (!rowptrs) goto exit_2;

    my_structure = malloc(sizeof *my_structure);
    if (!my_structure) goto exit_3;

    // More work here ...

    return my_structure;

exit_3:
    free(rowptrs);
exit_2:
    free(colind);
exit_1:
    free(data);
exit_0:
    return NULL;
}

Per inciso, evita di usare sizeof (TYPE) : ottenere il tipo sbagliato inizialmente, o non aggiornarlo ovunque in lock-step è un pericolo reale. sizeof expr non ne risente.

Inoltre, return non è una funzione. Attenzione a farlo specialmente se scrivi anche C ++.

    
risposta data 13.07.2018 - 22:09
fonte
4

Il pattern da utilizzare qui è goto CLEANUP . Alcune persone impazziscono per il gotos ma sono l'approccio più semplice in C. Dal momento che devi ripulire le risorse, avere un singolo ritorno e fare tutte le operazioni di pulizia appena prima che il rendimento di solito funzioni meglio.

In questo caso la pulizia sembra essere necessaria solo per errore, non per successo. Quindi non inseriremo il codice cleanup prima del ritorno principale ma aggiungiamo una sezione di errore. Il tuo codice diventerebbe quindi:

MyStructure* CreateMyStructure(int N, int K)
{
    MyStructure *my_structure = NULL;
    double *data = NULL;
    int *colind = NULL
    int *rowptrs = NULL;

    data = malloc(sizeof(double) * N);
    if (!data) goto ERROR;

    colind = malloc(sizeof(int) * N);
    if (!colind) goto ERROR;

    rowptrs = malloc(sizeof(int) * K);
    if (!rowptrs) goto ERROR;

    my_structure = malloc(sizeof(my_structure));
    if !(my_structure) goto ERROR;

    /* .. set up my_structure in a way that takes
     * ownership of data, colind, rowptrs */

    return my_structure;

ERROR:
    free(rowptrs);
    free(colind);
    free(data);
    return NULL;
}

Nota che free() non fa nulla se il puntatore è nullo, quindi possiamo gestirlo con una singola sezione di errore / pulizia. Se devi eseguire una pulizia più complicata, dovrai proteggere le sezioni con i condizionali o disporre di più etichette per le diverse fasi di pulizia.

Se tenere traccia di tutte le risorse allocate è troppo complicato, considera l'utilizzo di C ++. Piuttosto che utilizzare malloc() grezzo o new , è possibile utilizzare i puntatori intelligenti per liberare automaticamente la memoria quando si restituisce o si genera un'eccezione. Ciò rende le perdite di risorse meno probabili e più produttive. Proteggendo le dichiarazioni extern "C" necessarie con ifdefs, è possibile scrivere intestazioni compilate in modo equivalente per C e C ++ e quindi utilizzare C ++ in modo selettivo per alcune unità di compilazione senza dover immediatamente convertire l'intero progetto.

    
risposta data 13.07.2018 - 21:19
fonte
1

FYI, oltre alla eccellente risposta di amon s, ci sono anche altre possibili soluzioni da considerare in base agli altri tuoi requisiti.

È possibile impacchettare le dimensioni utilizzando un array e utilizzare un allocatore orientato all'array:

int sizes [] = { sizeof(double)*N, sizeof(int)*N, sizeof(int)*K, sizeof(my_structure) };
void *allocations[ sizeof(sizes)/sizeof(int) ];
multi_alloc ( allocations, sizes, sizeof(sizes)/sizeof(int) );
if ( allocations[0] == NULL ) return NULL;

multi_alloc allocherà ciascun elemento a sua volta e pulirà se necessario. (Avrai comunque bisogno di inizializzare my_structure che viene restituito, ma quell'inizializzazione è mancante anche nel tuo esempio.)

Se sai che la durata delle tue sub-entità è la stessa, puoi assegnarle tutte in una volta, usando:

my_structure = malloc ( sizeof(double)*N + sizeof(int)*N + sizeof(int)*K + sizeof(my_structure) );

Ora non abbiamo bisogno di fare nulla in caso di fallimento (poiché c'è solo un'allocazione) e, per rilasciarlo, è necessario solo free ( my_structure ); . Dovrai comunque inizializzare my_structure , in questo caso, usando l'aritmetica del puntatore:

my_structure.doubleArray = (void*)my_structure + sizeof(my_structure);
my_structure.intArray = (void*)my_structure.doubleArray + sizeof(double) * N;
...

L'avvertenza principale con questo approccio è il problema dell'allineamento: ogni singolo malloc garantirà il massimo allineamento necessario, ad es. per double s. Ciò spreca spazio (a volte la memoria malloc ed è usata per int 's non per double ' s), così come altri overhead di allocazione di un blocco di memoria. Tuttavia, quando si esegue un singolo malloc , non abbiamo queste caratteristiche (o i loro costi).

Quindi, una strategia generale per gestire l'allineamento, sarebbe quella di sub-allocare al di fuori del singolo malloc nell'ordine di requisiti di allineamento decrescente (quindi prima i doppi array, poi gli array int). Se vuoi che my_structure sia direttamente free in grado (ovvero è il primo elemento), devi assicurarti che abbia almeno un double in esso (anche come unione fittizia).

Questo sarebbe appropriato come ottimizzazione di entrambe le velocità e amp; spazio quando sono allocati milioni di questi oggetti.

    
risposta data 13.07.2018 - 21:40
fonte

Leggi altre domande sui tag