2012-04-29 27 views
32

In linguaggi dinamicamente tipizzati come JavaScript o PHP, faccio spesso funzioni quali:Restituisce un "riferimento NULL" in C++?

function getSomething(name) { 
    if (content_[name]) return content_[name]; 
    return null; // doesn't exist 
} 

mi restituiscono un oggetto se esiste o se non null.

Quale sarebbe l'equivalente in C++ utilizzando i riferimenti? C'è qualche schema consigliato in generale? Ho visto alcuni quadri che hanno un metodo di isNull() per questo scopo:

SomeResource SomeClass::getSomething(std::string name) { 
    if (content_.find(name) != content_.end()) return content_[name]; 
    SomeResource output; // Create a "null" resource 
    return output; 
} 

Poi il chiamante controllerebbe la risorsa in questo modo:

SomeResource r = obj.getSomething("something"); 
if (!r.isNull()) { 
    // OK 
} else { 
    // NOT OK 
} 

Tuttavia, avendo per implementare questo tipo di metodo magico per ogni classe sembra pesante. Inoltre, non sembra ovvio quando lo stato interno dell'oggetto deve essere impostato da "null" a "not null".

C'è qualche alternativa a questo schema? So già che si può fare usando i puntatori, ma mi chiedo come/se può essere fatto con riferimenti. O dovrei rinunciare a restituire oggetti "null" in C++ e usare un modello specifico per C++? Qualsiasi suggerimento sul modo corretto di farlo sarebbe apprezzato.

+1

"Oltre a utilizzare i puntatori" esclude i puntatori intelligenti? – hmjd

+0

Il ritorno degli iteratori è uno schema che si presenta abbastanza spesso. Si restituisce un iteratore non valido se la cosa non è stata trovata. – Mat

+0

Bene i tuoi suggerimenti 'isNull()' non è tipico C++. Qual è il vero problema che stai cercando di risolvere. Forse possiamo fornire idee migliori se capiamo cosa stai cercando di fare. –

risposta

33

Non è possibile eseguire questa operazione durante i riferimenti, poiché non dovrebbero mai essere NULL. Ci sono fondamentalmente tre opzioni, una con un puntatore, le altre con la semantica del valore.

  1. Con un puntatore (nota: questo richiede che la risorsa non viene distrutto mentre il chiamante ha un puntatore ad esso, anche fare in modo che il chiamante sa che non ha bisogno di eliminare l'oggetto):

    SomeResource* SomeClass::getSomething(std::string name) { 
        std::map<std::string, SomeResource>::iterator it = content_.find(name); 
        if (it != content_.end()) 
         return &(*it); 
        return NULL; 
    } 
    
  2. Uso std::pair con un bool per indicare se l'articolo è valido o meno (nota: richiede che SomeResource ha un costruttore di default appropriato e non è costoso da costruire):

    std::pair<SomeResource, bool> SomeClass::getSomething(std::string name) { 
        std::map<std::string, SomeResource>::iterator it = content_.find(name); 
        if (it != content_.end()) 
         return std::make_pair(*it, true); 
        return std::make_pair(SomeResource(), false); 
    } 
    
  3. Utilizzando boost::optional:

    boost::optional<SomeResource> SomeClass::getSomething(std::string name) { 
        std::map<std::string, SomeResource>::iterator it = content_.find(name); 
        if (it != content_.end()) 
         return *it; 
        return boost::optional<SomeResource>(); 
    } 
    

Se si vuole semantica di valore e hanno la capacità di utilizzare Boost, mi consiglia l'opzione tre. Il vantaggio principale di boost::optional su std::pair è rappresentato dal fatto che un valore boost::optional unitario non crea il tipo di incapsulamento. Ciò significa che funziona per i tipi che non hanno un costruttore predefinito e salva il tempo/memoria per i tipi con un costruttore predefinito non banale.

Ho anche modificato il tuo esempio in modo da non cercare due volte la mappa (riutilizzando l'iteratore).

+0

+1: Ovviamente non è troppo difficile eseguire il rollover della propria classe "opzionale" se non si desidera una dipendenza di boost – Troubadour

+2

@Troubadour: è davvero sorprendentemente difficile eseguire il proprio "optional" che può contenere qualsiasi type: è necessario incorporare un oggetto POD con le dimensioni e l'allineamento corretti per il tipo, che non lo è affatto semplice. (Naturalmente è molto più semplice se lo si limita ai tipi costruttivi predefiniti, nel qual caso è essenzialmente uguale all'opzione 2). –

+1

Bella risposta. In C++ 11, anche std :: unique_ptr o std :: shared_ptr potrebbe essere restituito. Il primo corrisponderebbe alla semantica del valore in quanto la risorsa dovrebbe essere copiata, mentre il secondo corrisponderebbe all'opzione 1 (ma probabilmente richiede il refactoring del contenuto di SomeClass_ per contenere anche shared_ptrs). Ad ogni modo, in entrambi i casi la gestione della memoria è molto più chiara rispetto all'opzione 1. – Reunanen

6

Un approccio semplice e relativamente non intrusivo, che evita il problema se si implementano metodi speciali per tutti i tipi, è quello utilizzato con boost.optional. È essenzialmente un wrapper di template che ti permette di controllare se il valore posseduto è "valido" o meno.

BTW Penso che questo sia ben spiegato nei documenti, ma attenzione di boost::optional di bool, questa è una costruzione che è difficile da interpretare.

Modifica: la domanda richiede "riferimento NULL", ma lo snippet di codice ha una funzione che restituisce per valore. Se quella funzione effettivamente restituito un riferimento:

const someResource& getSomething(const std::string& name) const ; // and possibly non-const version 

allora la funzione avrebbe senso solo se il someResource essere di cui ha avuto una vita, almeno fintanto che quella dell'oggetto restituire il riferimento (altrimenti si woul Dhave un penzoloni riferimento). In questo caso, sembra perfettamente bene per restituire un puntatore:

const someResource* getSomething(const std::string& name) const; // and possibly non-const version 

ma è necessario rendere assolutamente chiaro che il chiamante non prende la proprietà del puntatore e non dovrebbe tentare di eliminarlo.

+0

Mi piace molto questo approccio, darò un'occhiata a questo. –

+0

IMO un po 'complicato - usa i puntatori come dice @jalf, non c'è bisogno di librerie aggiuntive e perfettamente C++. facoltativo richiede semantica del valore, cioè ciò che viene restituito è una copia (sono ritornato per riferimento in passato, ma non so se è ub!) – Nim

+0

@Nim Sono d'accordo, è probabilmente più semplice nel contesto del titolo della domanda. Il corpo della domanda suggerisce che l'OP ha anche bisogno di un mezzo per verificare la validità dei valori restituiti. Inoltre, quando si ritornano i puntatori, è necessario assicurarsi che la proprietà sia ben definita. – juanchopanza

22

Perché "oltre a utilizzare i puntatori"? L'utilizzo dei puntatori è come lo si fa in C++. A meno che tu non definisca un tipo "opzionale" che ha qualcosa come la funzione isNull() che hai menzionato. (o utilizzare uno esistente, ad esempio boost::optional)

I riferimenti sono progettati e garantiti per mai essere nulli. Chiedere "così come faccio a renderli nulli" è privo di senso. Si utilizzano puntatori quando è necessario un "riferimento nullable".

+2

+1: Spot on. :-) –

+0

Re "I riferimenti sono progettati e garantiti per * mai essere nullo *." Devo ancora imbattersi in un'implementazione che garantisca che i riferimenti non siano nulli. Lo standard non garantisce che i riferimenti non siano mai nulli. Questo è un mandato, non una garanzia. Il peso ricade sul programmatore. Sfortunatamente è abbastanza facile (e altamente UB) fare un riferimento nullo. MA +1. Se vuoi un "riferimento" che potrebbe essere nullo, la cosa da fare è usare un puntatore piuttosto che un riferimento. –

+1

@DavidHammen: lo standard dice più o meno quello che ho fatto sopra: quel riferimento punta a un oggetto. In tal modo, lo standard garantisce che i riferimenti facciano riferimento a un oggetto in un codice C++ valido e ben definito. Certo, * puoi * ingannare il compilatore nella creazione di un "riferimento null", ma il programma non è più vincolato dal comportamento specificato dallo standard e la garanzia non si applica. Dipende dal tuo punto di vista, davvero. :) Lo standard non garantisce che i riferimenti non siano nulli nei programmi che presentano UB. Ma garantisce che saranno non nulli in programmi ben definiti. :) – jalf

2

a differenza di Java e C# in C++ l'oggetto di riferimento non può essere nullo.
quindi consiglierei 2 metodi che uso in questo caso.

1 - invece di riferimento utilizzano un tipo che ha un null come std :: shared_ptr

2 - ottenere il riferimento come fuori parametro e tornare booleano successo.

bool SomeClass::getSomething(std::string name, SomeResource& outParam) { 
    if (content_.find(name) != content_.end()) 
    { 
     outParam = content_[name]; 
     return true; 
    } 
    return false; 
} 
+0

'shared_ptr' può essere usato solo se il pointee può sopravvivere all'oggetto che lo ha creato. – juanchopanza

5

Mi vengono in mente un paio di modi per gestire questo: (! Yuk)

  • Come altri hanno suggerito, utilizzare boost::optional
  • rendere l'oggetto ha uno stato che indica che non è valido
  • Uso puntatore invece riferimento
  • Avere un caso particolare della classe che è null oggetto
  • un'eccezione per indicare il fallimento (non sempre applicabile)
+6

Una spiegazione per andare avanti con il downvote sarebbe bella. Soprattutto per qualcosa che ha più suggerimenti. :) – vhallac

+0

Stavo per suggerire: avere un'istanza speciale della classe che è l'oggetto nullo. Un po 'come C# lo fa con String.Empty. Basta avere una singola istanza statica della versione "null" di ogni classe. Quindi è possibile confrontare il valore restituito con l'istanza 'null'. – Neil

+0

@Neil Sì, può essere un approccio pratico in determinate situazioni. Personalmente, non mi piace: una volta che lo usi, il tuo codice non può assumere (banalmente) che l'oggetto che hai in mano sia valido. Questo è vero anche per il mio secondo suggerimento sopra. – vhallac

1

Qui ci sono un paio di idee:

Alternativa 1:

class Nullable 
{ 
private: 
    bool m_bIsNull; 

protected: 
    Nullable(bool bIsNull) : m_bIsNull(bIsNull) {} 
    void setNull(bool bIsNull) { m_bIsNull = bIsNull; } 

public: 
    bool isNull(); 
}; 

class SomeResource : public Nullable 
{ 
public: 
    SomeResource() : Nullable(true) {} 
    SomeResource(...) : Nullable(false) { ... } 

    ... 
}; 

Alternativa 2:

template<class T> 
struct Nullable<T> 
{ 
    Nullable(const T& value_) : value(value_), isNull(false) {} 
    Nullable() : isNull(true) {} 

    T value; 
    bool isNull; 
}; 
1

Questo codice di seguito mostra come restituire riferimenti "non validi"; è solo un modo diverso di usare i puntatori (il metodo convenzionale).

Si sconsiglia di utilizzare questo codice in codice che verrà utilizzato da altri, poiché l'aspettativa è che le funzioni che restituiscono riferimenti restituiscono sempre riferimenti validi.

#include <iostream> 
#include <cstddef> 

#define Nothing(Type) *(Type*)nullptr 
//#define Nothing(Type) *(Type*)0 

struct A { int i; }; 
struct B 
{ 
    A a[5]; 
    B() { for (int i=0;i<5;i++) a[i].i=i+1; } 
    A& GetA(int n) 
    { 
     if ((n>=0)&&(n<5)) return a[n]; 
     else return Nothing(A); 
    } 
}; 

int main() 
{ 
    B b; 
    for (int i=3;i<7;i++) 
    { 
     A &ra=b.GetA(i); 
     if (!&ra) std::cout << i << ": ra=nothing\n"; 
     else std::cout << i << ": ra=" << ra.i << "\n"; 
    } 
    return 0; 
} 

La macro Nothing(Type) restituisce un valore, in questo caso rappresentata da nullptr - è possibile anche utilizzare 0, a cui è impostato l'indirizzo del riferimento. Ora questo indirizzo può essere controllato come-se hai usato i puntatori.

+0

Su Xcode 6.3 questo non funzionerà mai: if (! & Ra), l'indirizzo di un riferimento sarà sempre valutato come falso. – kakyo

Problemi correlati