2016-02-04 10 views
9

Sto cercando di capire l'implementazione di std::is_class. Ho copiato alcune possibili implementazioni e le ho compilate, sperando di capire come funzionano. Fatto questo, trovo che tutti i calcoli siano fatti durante la compilazione (come avrei dovuto capire prima, guardando indietro), quindi gdb non può darmi dettagli su cosa sta succedendo esattamente.Come funziona questa implementazione di std :: is_class?

L'implementazione sto lottando per capire è questo:

template<class T, T v> 
    struct integral_constant{ 
    static constexpr T value = v; 
    typedef T value_type; 
    typedef integral_constant type; 
    constexpr operator value_type() const noexcept { 
     return value; 
    } 
}; 

namespace detail { 
    template <class T> char test(int T::*); //this line 
    struct two{ 
     char c[2]; 
    }; 
    template <class T> two test(...);   //this line 
} 

//Not concerned about the is_union<T> implementation right now 
template <class T> 
struct is_class : std::integral_constant<bool, sizeof(detail::test<T>(0))==1 
                && !std::is_union<T>::value> {}; 

Ho problemi con le due linee commentato. Questa prima riga:

template<class T> char test(int T::*); 

Che cosa significa il T::*? Inoltre, questa non è una dichiarazione di funzione? Sembra uno, eppure questo compila senza definire un corpo di funzione.

La seconda linea che voglio capire è:

template<class T> two test(...); 

Ancora una volta, questo non è una dichiarazione di funzione con nessun corpo mai definito? Inoltre cosa significa l'ellissi in questo contesto? Pensavo che un'ellissi come argomento di funzione richiedesse un argomento definito prima dello ...?

Mi piacerebbe capire cosa sta facendo questo codice. So che posso semplicemente utilizzare le funzioni già implementate dalla libreria standard, ma voglio capire come funzionano.

Riferimenti:

+0

Le funzioni * dichiarazioni * non contengono un corpo. Questo è per le definizioni. –

+0

Le definizioni di funzione SONO una dichiarazione di funzione, Karoly. Il fatto che tu stia scegliendo è che tutte le definizioni sono dichiarazioni, ma non tutte le dichiarazioni sono definizioni. – Peter

risposta

8

Quello che stai guardando è una tecnologia di programmazione chiamata "SFINAE" che sta per "Errore di sostituzione non è un errore". L'idea di base è questa:

namespace detail { 
    template <class T> char test(int T::*); //this line 
    struct two{ 
    char c[2]; 
    }; 
    template <class T> two test(...);   //this line 
} 

Questo spazio dei nomi fornisce 2 sovraccarichi per test(). Entrambi sono modelli, risolti in fase di compilazione. Il primo prende uno int T::* come argomento. Si chiama Pointer di un membro ed è un puntatore a un int, ma a un int che è un membro della classe T. Questa è solo un'espressione valida, se T è una classe. Il secondo sta prendendo un numero qualsiasi di argomenti, che è valido in ogni caso.

Quindi, come viene utilizzato?

sizeof(detail::test<T>(0))==1 

Ok, passiamo la funzione di un 0 - questo può essere un puntatore e soprattutto un membro-pointer - nessuna informazione acquisite che sovraccarico di utilizzare da questo. Quindi se T è una classe, allora potremmo usare sia il T::* sia il sovraccarico ... qui - e poiché il sovraccarico T::* è il più specifico qui, viene usato. Ma se T non è una classe, allora non possiamo avere qualcosa come T::* e il sovraccarico è mal formato. Ma è un errore che si è verificato durante la sostituzione del parametro template. E poiché "i guasti di sostituzione non sono un errore" il compilatore ignorerà silenziosamente questo sovraccarico.

Successivamente viene applicato lo sizeof(). Notato i diversi tipi di rendimento?Quindi, a seconda di T, il compilatore sceglie l'overload corretto e quindi il tipo di ritorno corretto, risultando in una dimensione di sizeof(char) o sizeof(char[2]).

Infine, poiché utilizziamo solo la dimensione di questa funzione e non la chiamiamo mai realmente, non abbiamo bisogno di un'implementazione.

1

Cosa significa il T::* significa? Inoltre, questa non è una dichiarazione di funzione? Sembra uno, eppure questo compila senza definire un corpo di funzione.

Il int T::* è un pointer to member object. Può essere utilizzato come segue:

struct T { int x; } 
int main() { 
    int T::* ptr = &T::x; 

    T a {123}; 
    a.*ptr = 0; 
} 

Ancora una volta, questo non è una dichiarazione di funzione con nessun corpo mai definito? Inoltre cosa significa l'ellissi in questo contesto?

nell'altra linea:

template<class T> two test(...); 

l'ellissi is a C construct per definire una funzione che prende un numero qualsiasi di argomenti.

Mi piacerebbe capire cosa sta facendo questo codice.

Fondamentalmente si sta controllando se un tipo specifico è un struct o un class controllando se 0 può essere interpretato come un puntatore membro (nel qual caso T è un tipo di classe).

In particolare, in questo codice:

namespace detail { 
    template <class T> char test(int T::*); 
    struct two{ 
     char c[2]; 
    }; 
    template <class T> two test(...); 
} 

si dispone di due overload:

  • uno che è compensata solo quando un T è un tipo di classe (nel qual caso questa è la migliore corrispondenza e "vince" oltre il secondo)
  • il che è compensata ogni volta

Nel primo il sizeof il risultato produce 1 (il tipo restituito della funzione è char), gli altri rendimenti 2 (una struttura contenente 2 caratteri).

Il valore booleano controllato è quindi:

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value 

che significa: ritorno true solo se la costante integrale 0 può essere interpretata come un puntatore a membro di tipo T (in questo caso si tratta di un tipo di classe), ma non è un union (che è anche un possibile tipo di classe).

1

Test è una funzione sovraccaricata che accetta un puntatore al membro in T o altro. C++ richiede che venga utilizzata la corrispondenza migliore. Quindi se T è un tipo di classe è può avere un membro in esso ... allora quella versione è selezionata e la dimensione del suo ritorno è 1. Se T non è un tipo di classe allora T :: * ha senso zero in modo che la versione della funzione è filtrata da SFINAE e non sarà presente. Viene utilizzata la versione qualsiasi e la dimensione del tipo restituito non è 1. Quindi, controllando la dimensione del ritorno della chiamata, tale funzione determina una decisione se il tipo potrebbe avere membri ... l'unica cosa che rimane è assicurarsi che non sia un sindacato decidere se è una classe o no.

8

Parte di ciò che ti confonde, che non è spiegato dalle altre risposte finora, è che le funzioni test non vengono mai effettivamente chiamate. Il fatto che non abbiano definizioni non ha importanza se non le chiami. Come hai capito, il tutto avviene al momento della compilazione, senza eseguire alcun codice.

L'espressione sizeof(detail::test<T>(0)) utilizza l'operatore sizeof su un'espressione di chiamata di funzione. L'operando di sizeof è un contesto non valutato, il che significa che il compilatore in realtà non esegue quel codice (cioè lo valuta per determinare il risultato). Non è necessario chiamare tale funzione per conoscere lo sizeof quale sarebbe il se lo ha chiamato. Per conoscere la dimensione del risultato, il compilatore deve solo vedere le dichiarazioni delle varie funzioni test (per conoscere i tipi di ritorno) e quindi eseguire la risoluzione di sovraccarico per vedere quale è essere chiamato, e quindi trovare ciò che lo sizeof il risultato sarebbe essere.

Il resto del puzzle è che la chiamata di funzione non valutata detail::test<T>(0) determina se T può essere utilizzato per formare un puntatore a membro del tipo int T::*, che è possibile solo se T è un tipo di classe (perché non classi può' t hanno membri e quindi non possono avere riferimenti ai loro membri). Se T è una classe, è possibile chiamare il primo overload test, altrimenti viene chiamato il secondo overload. Il secondo sovraccarico utilizza una lista di parametri dello stile printf ... significa che accetta qualsiasi cosa, ma è anche considerata una corrispondenza peggiore di qualsiasi altra funzione valida (altrimenti le funzioni che usano ... sarebbero troppo "avide" e verranno chiamate tutte le volte , anche se esiste una funzione più specifica che corrisponde esattamente agli argomenti). In questo codice la funzione ... è un fallback per "se nient'altro corrisponde, chiama questa funzione", quindi se T non è un tipo di classe viene utilizzato il fallback.

Non importa se il tipo di classe ha davvero una variabile membro di tipo int, è valido per formare il tipo int T::* comunque per qualsiasi classe (proprio non poteva fare quel membro puntatore a fare riferimento a qualsiasi membro se il tipo non ha un membro int).

+1

più 1 per le ultime tre righe. –