API della libreria C ++: nuova o getters?

1

Sto scrivendo un wrapper C ++ per la mia libreria C (le mie competenze in C ++ sono un po 'arrugginite) e mi chiedo cosa sia preferibile: permettere all'utente di costruire oggetti o dare loro oggetti tramite getter?

Quale sarebbe l'equivalente idiomatico in C ++ di quella funzione C?

result* process_foo(library_handle*, foo*);

un costruttore come questo:

Result *res = new Result(library_handle, foo);

o un getter come questo? (che sostanzialmente racchiude il codice sopra):

Result *res = library->get_result(foo);

La risposta cambia se la creazione del risultato non riesce?

Getter potrebbe restituire NULL , ma cosa dovrebbe fare la versione del costruttore in caso di errore? AFAIK ad alcune persone non piacciono le eccezioni. Un metodo extra come init() o is_valid() non sembra elegante.

OTOH l'oggetto Result si attacca a RAII, è piccolo con i metodi inline, quindi potrebbe essere un buon candidato per l'allocazione dello stack.

    
posta Kornel 15.03.2013 - 13:22
fonte

4 risposte

3

Sarei in grado di restituire un shared_ptr o un puntatore intelligente personalizzato, o consentire la creazione automatica (stack) di oggetti. Potrei avvolgere un puntatore condiviso in un oggetto risultato se il risultato era grande.

Che sia la libreria che sta elaborando il foo o il foo che viene elaborato usando le risorse della libreria dipende dalle informazioni che non hai fornito, ma in genere ti aspetteresti qualcosa del tipo:

class Foo {
    public:
        Result Process ( Library& library );
};

o, se la libreria è fissa per la durata di Foo

class Foo {
    public:
        explicit Foo ( Library& library );
        Result Process ();
};

o Risultato è una piccola struttura, o se è grande allora vorrei tenere un puntatore intelligente ai dati.

Lanciare un'eccezione se Process fallisce. Dato che new genera un'eccezione in caso di errore, come molte funzioni standard di C ++, le persone a cui non piacciono le eccezioni probabilmente non dovrebbero funzionare in C ++.

Se è previsto che il processo possa restituire un risultato non valido, al posto di un'eccezione aggiungerei una funzione al risultato per verificarne la validità e, eventualmente, una conversione in bool in modo da poter scrivere

Result res ( foo.Process() );

if ( res.IsValid() ) {
    ...
}

o

if ( res ) {
    ...
}

L'accesso ai dati da un risultato non valido genererebbe un'eccezione.

    
risposta data 15.03.2013 - 13:36
fonte
3

Non ci si aspetta che il costruttore esegua molte elaborazioni, in generale, quindi eviterei di farlo almeno.

Result oggetto dovrebbe essere solo il risultato, creato e restituito da qualche altra classe (o semplicemente dalla funzione, se si interfaccia C). A seconda di quante cose ci saranno nel risultato, puoi costruirlo sia con i parametri del costruttore, o se ce ne sono troppi, quindi con il costruttore senza parametri (o parametri obbligatori), e quindi impostare tutti i parametri (o facoltativi) di Result istanza dopo la costruzione, prima di restituirla.

Se library->get_result(foo) riguarda l'elaborazione effettiva e non si ottiene solo il valore del risultato corrente, allora utilizzerei un nome alternativo. Almeno per me "get" suggerisce che si tratta di un'operazione leggera.

Quindi, in questo caso probabilmente uno di questi (presupposto di C ++ 11), prima dichiarazione di una funzione (C ++ è un linguaggio multi-paradigma, niente di sbagliato con le funzioni specialmente quando si interfaccia con le librerie C):

namespace library {
   class Result {...}
   std::unique_ptr<Result> process(library_handle*, foo*); // note: uses move semantics
}

//usage
auto result = library::process(handle, foo)

Nota anche come ho rimosso "foo" dal nome della funzione. C ++ supporta l'overloading, nessun punto nella duplicazione delle informazioni, che è già dato dal tipo di secondo argomento.

O un approccio più orientato agli oggetti:

namespace library {
    class Result {...}
    class Library {
        Library(library_handle*); // constructor
        std::unique_ptr<Result> process(foo*); // library handle already known
    }
}

//usage
auto result = library->process(foo);

Nell'ultimo esempio, il risultato potrebbe anche essere memorizzato come variabile membro nella classe Library, nel qual caso avresti getter per esso. Inoltre, a seconda di chi possiede il risultato e di quanto è grande, dovresti considerare di passarlo per valore, dopo aver appreso sulla semantica del movimento C ++ 11 che eviterebbe un sacco di copie.

Pensieri generali: se non sei sicuro del tuo C ++, considera di fornire solo un sottilissimo C ++ wrapper attorno a C, fondamentalmente solo alcune classi di "valore" di convenienza come quel Result e semplici funzioni in un namespace, quasi 1: 1 mappatura alla tua API C. Potrebbe non essere bello come una buona API OOP C ++, ma sarà un lotto migliore di una crappy API C ++ progettata mentre si impara il moderno C ++;)

    
risposta data 15.03.2013 - 15:03
fonte
0

Hai respinto l'approccio del metodo constructor + init, ma non è necessariamente una cattiva scelta. Infatti, Cocoa (l'impostazione Objective C utilizzata ampiamente nella programmazione iOS / MacOS) utilizza l'approccio "init" post-allocazione ovunque, quindi esiste un precedente ampiamente utilizzato e di successo. Se si dispone di un'inizializzazione sostanziale e non garantita, e si è riluttanti a generare eccezioni (e anch'io sarei riluttante!), Allora forse in realtà si desidera eseguire la route "init".

Ecco una descrizione del pattern di init di Cocoa, so che stai facendo C ++, ma potrebbe essere informativo per analogia:

link

    
risposta data 15.03.2013 - 17:01
fonte
0

C'è un'altra soluzione alternativa a questo problema. Invece di usare getter e restituire il puntatore all'oggetto, è possibile utilizzare un getter e restituire handle all'oggetto. Ad esempio:

struct H { int id; };
H Library::get_result(int i);
H h = library->get_result(10);

Quindi Library :: get_result () manterrebbe std :: vector degli oggetti reali e handle di ritorno:

   H Library::get_result(int i) {
     vec.push_back(new Result(i)); // or find existing object
     H h;
     h.id = vec.size()-1;
     return h;
   }

La maggior parte delle persone userebbe unique_ptr < > come handle, ma volevo dare un'altra soluzione alternativa, che non mantenga la proprietà dell'oggetto nell'handle, ma invece std :: vector mantiene la proprietà.

La cosa buona di struct H {int id; } Gli handle di stile sono che sono liberamente copiabili (dato che è solo un int), e passare l'handle nel codice diventa considerevolmente più semplice quando è possibile passare copie dell'int anziché passare oggetti reali. Inoltre non è possibile modificare le copie temporanee dell'oggetto e perdere le modifiche quando si passano oggetti nel codice. (se usi direttamente la nuova soluzione, qualcuno la creerà sullo stack.)

    
risposta data 19.03.2013 - 09:55
fonte

Leggi altre domande sui tag