5

Sto progettando una classe di tipo std::vector per scopi di autoapprendimento, ma mi sono imbattuto in un problema di allocazioni di memoria all'interno dei costruttori.Allocazione di memoria all'interno dei costruttori?

Il costruttore di capacità di std::vector è molto conveniente, ma ha il potenziale di lanciare un'eccezione std::bad_alloc e abbattere l'intero programma con esso.

Sto lottando per decidere quale sarebbe il modo più elegante per gestire lo scenario improbabile del failover del costruttore di capacità o per informare meglio l'utente che utilizzando il costruttore, acconsentono che la struttura dati sia in grado di giù l'intero programma attraverso un'eccezione.

Il mio primo pensiero è stato quello di aggiungere un avviso in fase di compilazione ogni volta che viene chiamato il costruttore, ricordando che il costruttore può fallire e che dovrebbero assicurarsi di gestirlo o essere consapevoli dei rischi connessi con l'uso del costruttore .

Questa soluzione sembrava male perché, se applicata su scala globale, causerebbe troppi avvertimenti, e fare una cattiva impressione.

La mia seconda idea era rendere privato il costruttore e richiedere che il costruttore fosse raggiunto attraverso un metodo statico "requestConstruct", simile al modello Singleton.

Questa soluzione rende l'interfaccia strana.

Ho avuto qualche altra idea, ma tutte sembrano danneggiare la "sensazione" dell'interfaccia. Queste idee sono:

  • spinta simile maybes boost::optional
  • Haskell-like
  • costringere l'utente a darmi un pool di memoria che la struttura è autorizzata a. Questa idea sembra molto elegante ma non riesco a trovare un'implementazione di pool di memoria statica pulita/popolare per C/C++. Ho fatto comunque un test al https://github.com/dmitrymakhnin/MemoryPools.
  • usa asserzioni descrittive che si scusano per aver fatto saltare in aria il mondo, e spiegano cosa è successo in dettaglio. Che è quello che sto facendo at the moment.
  • provare ad allocare alcune altre volte prima di richiamare e arrestare l'intero programma. Questa sembra una buona strategia, ma sembra più come incrociare le dita, poiché il programma può ancora bloccarsi a causa del comportamento della struttura (piuttosto che del programma). Questo approccio potrebbe essere solo paranoico e sbagliato, dal momento che un'assegnazione fallita non ti darà necessariamente la possibilità di "riassegnare", e semplicemente spegnerà il programma.
  • inventare una sorta di meccanismo di stress test per dare fiducia al proprietario della struttura che può gestire la maggior capacità che l'utente si aspetta (che può essere difficile perché questi stress-test possono essere molto fuorvianti, come la memoria può essere disponibile ora, ma in tempi più intensi di memoria, potrebbe non esserlo).

C'è anche una divertente possibilità di non avere abbastanza memoria per catturare effettivamente l'eccezione. Questo programma sembra non prenderlo (che potrebbe essere correlato ad avere abbastanza memoria).

#include <stdint.h> 
#include <exception> 
#include <iostream> 
#include <stdlib.h> 

int main(int argc, char **argv) 
{ 
    uint_fast32_t leaked_bytes = 0; 

    while (1) {  
     try { 
      new uint8_t; 
     } catch (const std::bad_alloc& e) { 
      std::cout << "successfully leaked" << leaked_bytes << " bytes." << '\n'; 
      exit(0); 
     } catch (const std::exception& e) { 
      std::cout << "I caught an exception, but not sure what it was...\n"; 
      exit(0); 
     } 

     ++leaked_bytes;  
    } 
} 

Questo programma non mi permetta di gestire il fallimento prima che il programma è terminato però:

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

int main(int argc, char **argv) 
{ 
    uint_fast32_t bytes_leaked = 0; 

    while (1) { 
     if (malloc(1) == 0) { 
      printf("leaked %u bytes.\n", bytes_leaked); 
      exit(0); 
     }   
     ++bytes_leaked; 
    } 

    return 0; 
} 

nothrow funziona correttamente così:

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

int main(int argc, char **argv) 
{ 
    uint64_t leaked_bytes = 0; 

    while (1) {   
     uint8_t *byte = new (std::nothrow) uint8_t; 
     if (byte == nullptr) { 
      printf("leaked %llu bytes.\n", leaked_bytes); 
      exit(0); 
     }   
     ++leaked_bytes; 
    } 

    return 0; 
} 

EDIT:

penso Ho trovato una soluzione a questo problema. C'è un'innata ingenuità nella memorizzazione di strutture dati dinamiche sul processo principale. Cioè, queste strutture non sono garantite per avere successo e potrebbero rompersi in qualsiasi momento.

È preferibile eseguire la creazione di tutte le strutture dinamiche in un altro processo e disporre di una sorta di criterio di riavvio nel caso in cui abbia un problema imprevisto.

Questo impone un sovraccarico per la comunicazione alla struttura dati, ma impedisce al programma principale di dover gestire tutto ciò che può andare storto con la struttura dati, che può rapidamente assumere la maggioranza del codice in main.

fine EDIT.

Esiste un modo corretto per gestire i problemi di allocazione in tali classi dinamiche, aumentare la consapevolezza del mio utente in merito a tali rischi?

+1

Per quanto ne so, std :: vector non ha un costruttore di capacità. Ha un costruttore di dimensioni che in realtà assegna gli oggetti non solo riserva una memoria –

+13

Per me, lanciare un 'std :: bad_alloc' è il modo più idiomatico per segnalare un problema di" memoria insufficiente ". Dopotutto, il tuo costruttore non può realmente fare il suo lavoro se non riesce a recuperare la memoria. –

+1

Ricorda che non è solo 'std :: vector' che può esaurire la memoria. Così può 'std :: string' o qualsiasi altra classe che usa la memoria. Ma la maggior parte dei sistemi a 64 bit non esaurisce la memoria. Invece, la memoria viene scambiata su disco e il programma si ferma a un arresto virtuale. – MSalters

risposta

0

Sulla base del feedback fornito, sono convinto che non esiste un modo elegante per impedire alle classi che gestiscono la memoria dinamica di eliminare il programma senza introdurre complessità.

Le eccezioni di lancio sono discutibili perché una volta che la classe non ha memoria che può allocare, potrebbe non essere in grado di gestire std :: bad_alloc che un utente può catturare.

Da un punto di vista più ampio, mi rendo conto che un modo per impedire al programma di arrestarsi in modo anomalo dai moduli che utilizza è spostare le parti del programma che utilizzano questi moduli in un altro processo e condividere tale memoria con il processo principale, in modo che se l'altro processo in qualche modo va giù, è sufficiente riavviare quel processo e fare un'altra richiesta.

Finché si utilizzano classi in grado di fallire in casi estremi, è impossibile impedire loro di fallire a meno che non si è in grado di controllare i loro casi estremi.

Nel caso della memoria dinamica, è difficile controllare esattamente la memoria dinamica a cui il processo ha accesso, quale sia il criterio di allocazione e la quantità totale di memoria utilizzata dall'applicazione. È quindi difficile controllarlo senza essere più rigorosi sui pool di memoria contigua che si utilizzano, o spostando la struttura dei dati per essere gestita da un altro processo che può essere riavviato in caso di errore.

Risposta: È intrinsecamente impossibile sia fornire una semplice coppia di interfaccia/implementazione di una classe che gestisce la memoria dinamica. Avete una semplice interfaccia/implementazione che farà cadere il vostro programma come std :: vector; o un'interfaccia/implementazione complessa che richiede uno dei seguenti o qualcosa di più intelligente:

  1. Il programmatore deve fornire blocchi di memoria che sono già disponibili. Dal momento che la memoria è già lì e sai quanta parte hai, non puoi allocare più di quanto puoi permetterti.
  2. La struttura dati utilizza il proprio schema di gestione della memoria utilizzando un processo separato che si arresterà in modo anomalo invece del processo su cui è ospitata la struttura.
  3. La struttura dati è gestita da un processo separato del tutto simile a quello dei server di database. Ciò semplifica il riavvio del processo in caso di errore.
+3

"Le eccezioni di lancio sono discutibili". Se non ti fidi della tua lingua e dei suoi progettisti di librerie standard, forse hai scelto una lingua sbagliata. –

+0

Puoi lanciare e catturare bad_alloc senza allocare memoria. Quindi non c'è nessun problema con il lancio. – rici

+0

Verificherò se provare nuovamente il test di perdita dopo aver controllato alcuni flag del compilatore mi consentirà di rilevare l'eccezione. Per ora dice "termina chiamata in modo ricorsivo Questa applicazione ha richiesto al Runtime di interromperlo in un modo insolito Per ulteriori informazioni, contattare il team di supporto dell'applicazione. ." EDIT: g ++ leak.cpp -o leak -O3 -s -DNDEBUG non ha modificato nulla. – Dmitry