2011-03-14 13 views
10

Carichi di librerie C++, incluso lo standard, consentono di adattare gli oggetti per l'utilizzo nelle librerie. La scelta è spesso tra una funzione membro o una funzione libera nello stesso spazio dei nomi.Meccanica dell'estensione tramite funzioni libere o funzioni membro

Mi piacerebbe conoscere la meccanica e costruisce il codice libreria utilizzato per inviare una chiamata che chiamerà una di queste funzioni di "estensione", so che questa decisione deve aver luogo durante la compilazione e prevede modelli. Il seguente psuedocode di runtime non è possibile/non-sense, le ragioni sono fuori dallo scopo di questa domanda.

if Class A has member function with signature FunctionSignature 
    choose &A.functionSignature(...) 
else if NamespaceOfClassA has free function freeFunctionSignature 
    choose freeFunctionSignature(...) 
else 
    throw "no valid extension function was provided" 

Il codice sopra appare come il codice di runtime: /. Quindi, come fa la biblioteca a capire lo spazio dei nomi in cui si trova una classe, come rileva le tre condizioni, quali altre insidie ​​ci sono che devono essere evitate.

La motivazione per la mia domanda è per me essere in grado di trovare i blocchi di invio nelle librerie e di essere in grado di utilizzare i costrutti nel mio codice. Quindi, risposte dettagliate aiuteranno.

!! PER VINCERE BOUNTY !!

Ok così secondo la risposta di Steve (e dei commenti) ADL e SFINAE sono i costrutti chiave per il cablaggio del dispatch in fase di compilazione. Ho la testa attorno ADL (in primis) e SFINAE (ancora in sordina). Ma non so come si orchestrano insieme nel modo in cui penso che dovrebbero.

Voglio vedere un esempio illustrativo di come questi due costrutti possono essere messi insieme in modo che una libreria possa scegliere in fase di compilazione se chiamare una funzione membro fornita dall'utente in un oggetto o una funzione libera fornita dall'utente fornita nel spazio dei nomi dello stesso oggetto. Questo dovrebbe essere fatto solo usando i due costrutti sopra, nessun invio runtime di alcun tipo.

Diciamo che l'oggetto in questione è chiamato NS::Car e questo oggetto deve fornire il comportamento di MoveForward(int units), come funzione membro di c. Se il comportamento deve essere rilevato dal namespace dell'oggetto, probabilmente assomiglia allo MoveForward(const Car & car_, int units). Definiamo la funzione che desidera inviare mover(NS::direction d, const NS::vehicle & v_), dove direction è un enum e v_ è una classe base di NS::car.

+0

Non è possibile eseguire l'override dell'operatore '' nella classe per l'output in un flusso. Un operatore membro deve avere la classe a sinistra dell'operatore, non a destra. Inoltre, la ricerca dei nomi fa parte del compilatore, non della libreria, e coinvolgerà i modelli solo se è coinvolta una classe template o una funzione template. Se ti stai chiedendo come è fatta la ricerca del nome, ti preghiamo di chiarire la tua domanda. Altrimenti, non so cosa stai chiedendo. –

+0

@david Sì, ho sbagliato su 'operator <<' Ho rimosso l'esempio, ma qualcuno stava modificando la mia domanda e ha sovrascritto la rimozione: D –

risposta

2

Bene, posso dirti come rilevare la presenza delle funzioni membro di un certo nome (e firma) in fase di compilazione.Un mio amico lo descrive qui:

Detecting the Existence of Member Functions at Compile-Time

tuttavia che non sarà arrivare dove si vuole andare, perché funziona solo per il tipo statico. Poiché si desidera passare un "reference-to-vehicle", non c'è modo di verificare se il tipo dinamico (il tipo dell'oggetto concreto dietro il riferimento) ha una funzione membro di questo tipo.

Se ti accontenti del tipo statico, c'è un altro modo per fare una cosa molto simile. Implementa "se l'utente fornisce una funzione libera sovraccaricata, chiamala, altrimenti prova a chiamare la funzione membro". E va in questo modo:

namespace your_ns { 

template <class T> 
void your_function(T const& t) 
{ 
    the_operation(t); // unqualified call to free function 
} 

// in the same namespace, you provide the "default" 
// for the_operation as a template, and have it call the member function: 

template <class T> 
void the_operation(T const& t) 
{ 
    t.the_operation(); 
} 

} // namespace your_ns 

In questo modo l'utente può fornire il proprio sovraccarico di "the_operation", nello stesso namespace come la sua classe, quindi è trovato da ADL. Ovviamente "the_operation" dell'utente deve essere "più specializzato" rispetto all'implementazione predefinita , altrimenti la chiamata sarebbe ambigua. In pratica questo non è un problema, dal momento che tutto ciò che limita lo tipo di parametro più che essere un riferimento a const a qualsiasi sarà "più specializzato".

Esempio:

namespace users_ns { 

class foo {}; 

void the_operation(foo const& f) 
{ 
    std::cout << "foo\n"; 
} 

template <class T> 
class bar {}; 

template <class T> 
void the_operation(bar<T> const& b) 
{ 
    std::cout << "bar\n"; 
} 

} // namespace users_ns 

EDIT: dopo aver letto ancora una volta la risposta di Steve Jessop, mi rendo conto che è fondamentalmente quello che ha scritto, solo con più parole :)

+0

Altre parole e dimostrazione esplicita del concetto nel codice. +1 per quel – Novelocrat

+0

grazie amico, ero davvero interessato al caso di tipo statico. vedere il codice scritto ottiene l'effetto "aha": D –

8

La libreria non esegue nulla di questo in fase di esecuzione, la distribuzione viene eseguita dal compilatore quando viene compilato il codice di chiamata. Le funzioni libere nello stesso spazio dei nomi di uno degli argomenti vengono trovate in base alle regole di un meccanismo chiamato "Argomento dipendente dalla ricerca" (ADL), a volte chiamato "ricerca Koenig".

Nei casi in cui si ha la possibilità di implementare una funzione libera o una funzione membro, ciò potrebbe essere dovuto al fatto che la libreria fornisce un modello per una funzione libera che chiama la funzione membro. Quindi se l'oggetto fornisce una funzione con lo stesso nome da ADL, sarà una corrispondenza migliore rispetto all'istanziazione del modello e quindi verrà scelto per primo. Come dice Space_C0wb0y, potrebbero usare SFINAE per rilevare la funzione membro nel modello e fare qualcosa di diverso a seconda che esista o meno.

Non è possibile modificare il comportamento di std::cout << x; aggiungendo una funzione membro a x, quindi non sono abbastanza sicuro di cosa intendi.

+0

L'altro meccanismo importante per la spedizione è [SFINAE] (http: //en.wikipedia .org/wiki/Substitution_failure_is_not_an_error). –

+0

grazie per i suggerimenti, questo dovrebbe aiutarmi a capire le cose. Inoltre, avevi ragione riguardo alla semantica del flusso e ho rimosso l'esempio. L'ultimo codice che ho visto è stato boost :: serialization, che ti permette di fornire entrambi i tipi di funzioni. –

0

lavori finanziari, a volte, gli sviluppatori possono utilizzati funzioni liberi o funzioni di classe, in modo intercambiabile, ci sono alcune situazioni, per usare l'un l'altro.

(1) funzioni Oggetto/classe ("Metodi), sono preferito quando la maggior parte della sua purpouse influenzano solo l'oggetto o gli oggetti vengono in questione, destinate a comporre altri oggetti.

// object method 
MyListObject.add(MyItemObject); 
MyListObject.add(MyItemObject); 
MyListObject.add(MyItemObject); 

(2) Gratis (" le funzioni globali "o" modulo ") sono preferibili, quando coinvolgono diversi oggetti e gli oggetti non sono parte/composti l'uno dell'altro oppure quando la funzione utilizza dati semplici (strutture senza metodi, tipi primitivi)

MyStringNamespace.MyStringClass A = new MyStringNamespace.MyStringClass("Mercury"); 
MyStringNamespace.MyStringClass B = new MyStringNamespace.MyStringClass("Jupiter"); 
// free function 
bool X = MyStringNamespace.AreEqual(A, B); 

Quando alcuni oggetti di accesso alla funzione modulo comune, in C++, si dispone di e "parola chiave amico" che consente loro di accedere ai metodi degli oggetti, senza riguardo all'ambito.

class MyStringClass { 
    private: 
    // ... 
    protected: 
    // ... 
    // not a method, but declared, to allow access 
    friend: 
    bool AreEqual(MyStringClass A, MyStringClass B); 
} 

bool AreEqual(MyStringClass A, MyStringClass B) { ... } 

In "object oriented quasi puri" linguaggi di programmazione come Java o C#, dove non si può avere funzioni libere, libere funzioni sono sostituiti con metodi statici, il che rende cose più complicate.

1

Se siete solo in cerca di un esempio concreto, si consideri il seguente:

#include <cassert> 
#include <type_traits> 
#include <iostream> 

namespace NS 
{ 
    enum direction { forward, backward, left, right }; 

    struct vehicle { virtual ~vehicle() { } }; 

    struct Car : vehicle 
    { 
     void MoveForward(int units) // (1) 
     { 
      std::cout << "in NS::Car::MoveForward(int)\n"; 
     } 
    }; 

    void MoveForward(Car& car_, int units) 
    { 
     std::cout << "in NS::MoveForward(Car&, int)\n"; 
    } 
} 

template<typename V> 
class HasMoveForwardMember // (2) 
{ 
    template<typename U, void(U::*)(int) = &U::MoveForward> 
    struct sfinae_impl { }; 

    typedef char true_t; 
    struct false_t { true_t f[2]; }; 

    static V* make(); 

    template<typename U> 
    static true_t check(U*, sfinae_impl<U>* = 0); 
    static false_t check(...); 

public: 
    static bool const value = sizeof(check(make())) == sizeof(true_t); 
}; 

template<typename V, bool HasMember = HasMoveForwardMember<V>::value> 
struct MoveForwardDispatcher // (3) 
{ 
    static void MoveForward(V& v_, int units) { v_.MoveForward(units); } 
}; 

template<typename V> 
struct MoveForwardDispatcher<V, false> // (3) 
{ 
    static void MoveForward(V& v_, int units) { NS::MoveForward(v_, units); } 
}; 

template<typename V> 
typename std::enable_if<std::is_base_of<NS::vehicle, V>::value>::type // (4) 
mover(NS::direction d, V& v_) 
{ 
    switch (d) 
    { 
    case NS::forward: 
     MoveForwardDispatcher<V>::MoveForward(v_, 1); // (5) 
     break; 
    case NS::backward: 
     // ... 
     break; 
    case NS::left: 
     // ... 
     break; 
    case NS::right: 
     // ... 
     break; 
    default: 
     assert(false); 
    } 
} 

struct NonVehicleWithMoveForward { void MoveForward(int) { } }; // (6) 

int main() 
{ 
    NS::Car v; // (7) 
    //NonVehicleWithMoveForward v; // (8) 
    mover(NS::forward, v); 
} 

HasMoveForwardMember(2) è un metafunction che verifica l'esistenza di una funzione di membro di questo nome con la firma void(V::*)(int) in una determinata classe V.MoveForwardDispatcher(3) utilizza queste informazioni per chiamare la funzione membro se esiste o se torna indietro per chiamare una funzione libera. mover semplicemente delegata l'invocazione di MoveForward a MoveForwardDispatcher(5).

Il codice come-postato invocherà Car::MoveForward(1), ma se questa funzione membro viene rimosso, rinominato o ha cambiato firma, NS::MoveForward sarà chiamato invece.

noti inoltre che, poiché mover è un modello, un controllo SFINAE deve essere messo in atto per mantenere la semantica di oggetti solo permettono derivati ​​da NS::vehicle da passare per v_(4). Per dimostrare, se uno commenti fuori (7) e uncomments (8), mover saranno chiamati con un oggetto di tipo NonVehicleWithMoveForward(6), che vogliamo impedire nonostante il fatto che HasMoveForwardMember<NonVehicleWithMoveForward>::value == true.

(Nota:. Se la libreria standard non arriva con std::enable_if e std::is_base_of, utilizzare il std::tr1:: o boost:: varianti invece come disponibili)

Il modo in cui questo tipo di codice è di solito utilizzato è quello di chiamare sempre il funzione libera, e implementa la funzione libera in termini di qualcosa come MoveForwardDispatcher in modo tale che la funzione libera chiami semplicemente la funzione membro passata nell'oggetto se esiste, senza dover scrivere sovraccarichi di quella funzione libera per ogni tipo possibile che potrebbe avere un membro appropriato funzione.

0

Se ho capito correttamente il tuo problema viene semplicemente risolto usando (forse più) ereditarietà. Avete qualche parte una funzione libera spazio dei nomi:

namespace NS { 
void DoSomething() 
{ 
    std::cout << "NS::DoSomething()" << std::endl; 
} 
} // namespace NS 

usare una classe base che inoltra la stessa funzione:

struct SomethingBase 
{ 
    void DoSomething() 
    { 
     return NS::DoSomething(); 
    } 
}; 

Se qualche classe A derivanti da SomethingBase non implementa DoSomething() chiamandolo chiamerà SomethingBase :: DoSomething() -> NS :: DoSomething():

struct A : public SomethingBase // probably other bases 
{ 
    void DoSomethingElse() 
    { 
     std::cout << "A::DoSomethingElse()" << std::endl; 
    } 
}; 

Se un'altra classe B derivante dalla SomethingBase implementare DoSomething() chiamandolo chiamerà B :: DoSomething():

struct B : public SomethingBase // probably other bases 

{ 
    void DoSomething() 
    { 
     std::cout << "B::DoSomething()" << std::endl; 
    } 
}; 

Quindi chiamando DoSomething() su un oggetto derivante da SomethingBase eseguirà il membro se esistente, o la funzione libera altrimenti. Si noti che non c'è nulla da buttare, si ottiene un errore di compilazione se non c'è corrispondenza con la chiamata.

int main() 
{ 
    A a; 
    B b; 
    a.DoSomething(); // "NS::DoSomething()" 
    b.DoSomething(); // "B::DoSomething()" 
    a.DoSomethingElse(); // "A::DoSomethingElse()" 
    b.DoSomethingElse(); // error 'DoSomethingElse' : is not a member of 'B' 
}