Perché non dedurre il parametro del modello dal costruttore?


102

la mia domanda oggi è piuttosto semplice: perché il compilatore non può dedurre i parametri del modello dai costruttori di classi, così come può fare dai parametri delle funzioni? Ad esempio, perché il codice seguente non può essere valido:

template<typename obj>
class Variable {
      obj data;
      public: Variable(obj d)
              {
                   data = d;
              }
};

int main()
{
    int num = 2;
    Variable var(num); //would be equivalent to Variable<int> var(num),
    return 0;          //but actually a compile error
}

Come ho detto, capisco che questo non è valido, quindi la mia domanda è: perché non lo è? Consentire ciò creerebbe dei grandi buchi sintattici? C'è un'istanza in cui non si vorrebbe questa funzionalità (in cui inferire un tipo causerebbe problemi)? Sto solo cercando di capire la logica alla base dell'autorizzazione dell'inferenza dei modelli per le funzioni, ma non per le classi costruite adeguatamente.


Inviterei qualcuno (lo faccio io, ma non adesso), a compilare Drahakar e la risposta di Pitis (almeno) come buoni controesempi del perché non può funzionare
jpinto3912

2
Si noti inoltre che questo è facilmente template<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
risolvibile

3
Puoi ottenere quello che vuoi var = Variable <decltype (n)> (n);
QuentinUK

18
C ++ 17 lo permetterà! Questa proposta è stata accettata: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d

1
@underscore_d Eccellente! Era ora! Mi è sembrato naturale che fosse il modo in cui avrebbe dovuto funzionare, e la fonte di irritazione che non ha funzionato.
amdn

Risposte:


46

Penso che non sia valido perché il costruttore non è sempre l'unico punto di ingresso della classe (sto parlando di copia costruttore e operatore =). Quindi supponi di usare la tua classe in questo modo:

MyClass m(string s);
MyClass *pm;
*pm = m;

Non sono sicuro che sarebbe così ovvio per il parser sapere quale tipo di modello è MyClass pm;

Non sono sicuro che quello che ho detto abbia senso, ma sentiti libero di aggiungere qualche commento, questa è una domanda interessante.

C ++ 17

È accettato che C ++ 17 avrà la deduzione del tipo dagli argomenti del costruttore.

Esempi:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

Documento accettato .


8
Questo è in realtà un grande punto che non ho mai considerato. Non vedo alcun modo per aggirare il fatto che il puntatore dovrebbe essere specifico del tipo (cioè dovrebbe essere MyClass <string> * pm). In tal caso, tutto ciò che si finirà per fare è evitare di specificare il tipo all'istanza; pochi semplici caratteri di lavoro extra (e solo se l'oggetto è in pila, non l'heap, come sopra). Ho sempre sospettato che l'inferenza di classe potesse aprire un barattolo sintattico di vermi, e penso che potrebbe essere questo.
GRB

2
Non vedo bene come consentire l'inferenza dei parametri del modello dai costruttori richiederebbe il permesso di dichiarazioni non specializzate senza chiamate al costruttore, come nella seconda riga. Cioè, MyClass *pmqui non sarebbe valido per lo stesso motivo per cui una funzione dichiarata template <typename T> void foo();non può essere chiamata senza una specializzazione esplicita.
Kyle Strand

3
@KyleStrand Sì, dicendo "gli argomenti del modello di classe non possono essere dedotti dai loro costruttori perché [esempio che non utilizza alcun costruttore] ", questa risposta è completamente irrilevante. Sinceramente non riesco a credere che sia stato accettato, ha raggiunto +29, ci sono voluti 6 anni perché qualcuno notasse il problema evidente e si è seduto senza un singolo voto negativo per 7 anni. Nessun altro pensa mentre legge, o ...?
underscore_d

1
@underscore_d Mi piace come, allo stato attuale, questa risposta dica "potrebbero esserci dei problemi con questa proposta; Non sono sicuro che quello che ho appena detto abbia senso (!), sentiti libero di commentare (!!); e oh, a proposito, questo è più o meno esattamente come funzionerà C ++ 17 ".
Kyle Strand

1
@KyleStrand Ah sì, questo è ancora un altro problema, che ho notato ma ho dimenticato di menzionare tra tutte le altre cose divertenti. La modifica su C ++ 17 non è stata dell'OP ... e IMO non avrebbe dovuto essere approvato, ma pubblicato come una nuova risposta: sarebbe stato declinabile come 'cambia significato del post' anche se il post avesse Non ha avuto senso iniziare ... Non sapevo che modificare sezioni completamente nuove fosse un gioco leale e sicuramente sono state rifiutate modifiche meno drastiche, ma immagino che sia la fortuna del sorteggio in termini di revisori che ottieni.
underscore_d

27

Non puoi fare ciò che chiedi per motivi che altre persone si sono rivolti, ma puoi farlo:

template<typename T>
class Variable {
    public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
  return Variable<T>(instance);
}

che a tutti gli effetti è la stessa cosa che chiedi. Se ami l'incapsulamento, puoi rendere make_variable una funzione membro statica. Questo è ciò che la gente chiama costruttore denominato. Quindi non solo fa quello che vuoi, ma è quasi chiamato come vuoi: il compilatore sta deducendo il parametro del modello dal costruttore (denominato).

NB: qualsiasi compilatore ragionevole ottimizzerà l'oggetto temporaneo quando scrivi qualcosa di simile

auto v = make_variable(instance);

6
Vorrei sottolineare che non è particolarmente utile rendere membro statico della funzione in questo caso perché per questo dovresti specificare l'argomento del modello per una classe per chiamarlo comunque, quindi non avrebbe senso dedurlo.
Predelnik

3
E ancora meglio in C ++ 11 puoi farlo in auto v = make_variable(instance)modo da non dover specificare il tipo
Claudiu

1
Sì, lol all'idea di dichiarare la funzione make come staticmembro ... pensaci per appena un secondo. A parte questo: funzioni rendono liberi erano in effetti la soluzione, ma è un sacco di boilerplate ridondante, che mentre si sta digitando esso, basta sapere che non dovrebbe avere a perché il compilatore ha accesso a tutte le informazioni che stai ripetendo. .. e per fortuna il C ++ 17 lo canonizza.
underscore_d

21

Nell'era illuminata del 2016, con due nuovi standard sotto la cintura da quando è stata posta questa domanda e uno nuovo dietro l'angolo, la cosa cruciale da sapere è che i compilatori che supportano lo standard C ++ 17 compileranno il tuo codice così com'è .

Deduzione degli argomenti del modello per i modelli di classe in C ++ 17

Qui (per gentile concessione di una modifica di Olzhas Zhumabek della risposta accettata) è il documento che dettaglia le modifiche rilevanti allo standard.

Affrontare le preoccupazioni da altre risposte

L'attuale risposta più votata

Questa risposta sottolinea che "copia costruttore e operator=" non conoscerebbero le corrette specializzazioni del modello.

Questo non ha senso, perché il costruttore di copia standard operator= esiste solo per un tipo di modello noto :

template <typename T>
class MyClass {
    MyClass(const MyClass&) =default;
    ... etc...
};

// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm;   // WHAT IS THIS?
*pm = m;

Qui, come ho notato nei commenti, non c'è motivo per MyClass *pmessere una dichiarazione legale con o senza la nuova forma di inferenza: MyClass non è un tipo (è un modello), quindi non ha senso dichiarare un puntatore di tipo MyClass. Ecco un modo possibile per correggere l'esempio:

MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

Qui pmè già del tipo corretto, quindi l'inferenza è banale. Inoltre, è impossibile mescolare accidentalmente i tipi quando si chiama il costruttore di copie:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

Qui, pmsarà un puntatore a una copia di m. Qui, MyClassviene costruito da copia, mche è di tipo MyClass<string>(e non di tipo inesistente MyClass). Quindi, nel punto in cui pmviene dedotto il tipo di, ci sono informazioni sufficienti per sapere che il tipo di modello di m, e quindi il tipo di modello di pm, è string.

Inoltre, quanto segue solleverà sempre un errore di compilazione :

MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;

Questo perché la dichiarazione del costruttore di copia non è basata su modelli:

MyClass(const MyClass&);

Qui, il tipo di modello dell'argomento del costruttore di copia corrisponde al tipo di modello della classe nel suo complesso; cioè, quando MyClass<string>viene istanziato, MyClass<string>::MyClass(const MyClass<string>&);viene istanziato con esso, e quando MyClass<int>viene istanziato, MyClass<int>::MyClass(const MyClass<int>&);viene istanziato. A meno che non sia esplicitamente specificato o venga dichiarato un costruttore basato su modelli, non c'è motivo per il compilatore di istanziare MyClass<int>::MyClass(const MyClass<string>&);, il che sarebbe ovviamente inappropriato.

La risposta di Cătălin Pitiș

Pitiș fa un esempio deducendo Variable<int>e Variable<double>, quindi afferma:

Ho lo stesso nome di tipo (variabile) nel codice per due diversi tipi (variabile e variabile). Dal mio punto di vista soggettivo, influisce molto sulla leggibilità del codice.

Come notato nell'esempio precedente, di per Variablenon è un nome di tipo, anche se la nuova funzionalità lo fa sembrare sintatticamente simile.

Pitiș chiede quindi cosa accadrebbe se non fosse fornito alcun costruttore che consentirebbe l'inferenza appropriata. La risposta è che non è consentita alcuna inferenza, perché l'inferenza viene attivata dalla chiamata al costruttore . Senza una chiamata al costruttore, non c'è inferenza .

Questo è simile a chiedere quale versione di fooviene dedotta qui:

template <typename T> foo();
foo();

La risposta è che questo codice è illegale, per il motivo dichiarato.

La risposta di MSalter

Questa è, per quanto ne so, l'unica risposta per sollevare una legittima preoccupazione sulla funzionalità proposta.

L'esempio è:

Variable var(num);  // If equivalent to Variable<int> var(num),
Variable var2(var); // Variable<int> or Variable<Variable<int>> ?

La domanda chiave è: il compilatore seleziona qui il costruttore dedotto dal tipo o il costruttore di copia ?

Provando il codice, possiamo vedere che il costruttore di copia è selezionato. Per espandere l'esempio :

Variable var(num);          // infering ctor
Variable var2(var);         // copy ctor
Variable var3(move(var));   // move ctor
// Variable var4(Variable(num));     // compiler error

Non sono sicuro di come la proposta e la nuova versione dello standard lo specifichino; sembra essere determinato da "guide alla deduzione", che sono un nuovo pezzo di standardese che ancora non capisco.

Inoltre, non sono sicuro del motivo per cui la var4detrazione è illegale; l'errore del compilatore da g ++ sembra indicare che l'istruzione viene analizzata come una dichiarazione di funzione.


Che risposta fantastica e dettagliata! var4è solo un caso di "analisi più irritante" (non correlata alla deduzione degli argomenti del modello). Usavamo solo parentesi extra per questo, ma in questi giorni penso che l'uso di parentesi graffe per denotare in modo univoco la costruzione sia il solito consiglio.
Sumudu Fernando

@SumuduFernando Grazie! Intendi che Variable var4(Variable(num));viene trattata come una dichiarazione di funzione? In caso affermativo, perché Variable(num)una specifica di parametro valida?
Kyle Strand

@SumuduFernando Non importa, non avevo idea che fosse valido: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand

11

Ancora mancante: rende il seguente codice abbastanza ambiguo:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}

Un altro buon punto. Supponendo che esista una variabile definita dal costruttore di copie (Variabile <obj> d), dovrebbe essere stabilita una sorta di precedenza.
GRB

1
Oppure, in alternativa, chiedi al compilatore di lanciare di nuovo un errore di parametro del modello non definito, proprio come ho suggerito per quanto riguarda la risposta di Pitis. Tuttavia, se si prende quella strada, il numero di volte in cui l'inferenza può avvenire senza problemi (errori) diventa sempre più piccolo.
GRB

Questo è in realtà un punto interessante e (come ho notato nella mia risposta) non sono ancora sicuro di come la proposta C ++ 17 accettata lo risolva.
Kyle Strand

9

Supponendo che il compilatore supporti ciò che hai chiesto. Allora questo codice è valido:

Variable v1( 10); // Variable<int>

// Some code here

Variable v2( 20.4); // Variable<double>

Ora, ho lo stesso nome del tipo (Variabile) nel codice per due diversi tipi (Variabile e Variabile). Dal mio punto di vista soggettivo, influisce molto sulla leggibilità del codice. Avere lo stesso nome di tipo per due tipi diversi nello stesso spazio dei nomi mi sembra fuorviante.

Aggiornamento successivo: un'altra cosa da considerare: specializzazione del modello parziale (o completa).

E se mi specializzo Variabile e non fornisco un costruttore come ti aspetti?

Quindi avrei:

template<>
class Variable<int>
{
// Provide default constructor only.
};

Quindi ho il codice:

Variable v( 10);

Cosa dovrebbe fare il compilatore? Utilizzare la definizione della classe Variabile generica per dedurre che si tratta di Variabile, quindi scoprire che Variabile non fornisce un costruttore di parametri?


1
Peggio: cosa succede se hai solo Variable <int> :: Variable (float)? Ora hai due modi per dedurre Variabile (1f) e nessun modo per dedurre Variabile (1).
MSalters

È un buon punto, ma potrebbe essere facilmente superato dal casting: Variable v1 ((double) 10)
jpinto3912

Sono d'accordo che la leggibilità del codice sia un problema soggettivo, tuttavia, sono d'accordo al 100% con quello che stai dicendo sulla specializzazione del modello. La soluzione sarebbe probabilmente quella di fornire un errore di parametro del modello non definito (una volta che il compilatore esamina la specializzazione <int> e non vede costruttori validi, dì che non ha idea di quale modello si desidera utilizzare e che è necessario specificare esplicitamente) ma Sono d'accordo che non sia una bella soluzione. Aggiungerei questo come un altro importante buco sintattico che dovrebbe essere affrontato (ma potrebbe essere risolto se si accettano le conseguenze).
GRB

4
@ jpinto3912 - stai perdendo il punto. Il compilatore deve istanziare TUTTE le possibili variabili <T> per verificare se QUALSIASI variabile ctor <T> :: Variabile fornisce un ctor ambiguo. Liberarsi dell'ambiguità non è il problema: basta istanziare la variabile <doppia> da soli se è quello che vuoi. È innanzitutto trovare quell'ambiguità che lo rende impossibile.
MSalters

6

Lo standard C ++ 03 e C ++ 11 non consente la deduzione dell'argomento del modello dai parametri passati al costitutore.

Ma esiste una proposta per la "deduzione dei parametri del modello per i costruttori", quindi potresti ottenere presto ciò che stai chiedendo. Modifica: in effetti, questa funzionalità è stata confermata per C ++ 17.

Vedere: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html e http://www.open-std.org/jtc1/sc22/wg21/docs/ carte / 2015 / p0091r0.html


La funzionalità è stata aggiunta a C ++ 17, ma non se "presto" si applica a un periodo di tempo compreso tra 6 e 8 anni. ;)
ChetS

2

Molte classi non dipendono dai parametri del costruttore. Esistono solo poche classi che hanno un solo costruttore e parametrizzano in base ai tipi di questo costruttore.

Se hai davvero bisogno dell'inferenza del modello, usa una funzione di supporto:

template<typename obj>
class Variable 
{
      obj data;
public: 
      Variable(obj d)
      : data(d)
      { }
};

template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
    return Variable<obj>(d);
}

1
Ovviamente questa funzionalità si dimostrerebbe utile solo per alcune classi, ma lo stesso si può dire per l'inferenza di funzione. Non tutte le funzioni basate su modelli prendono i loro parametri dall'elenco degli argomenti, tuttavia consentiamo l'inferenza per quelle funzioni che lo fanno.
GRB

1

La detrazione dei tipi è limitata alle funzioni del modello nell'attuale C ++, ma è stato a lungo capito che la deduzione del tipo in altri contesti sarebbe molto utile. Quindi C ++ 0x's auto.

Anche se esattamente ciò che suggerisci non sarà possibile in C ++ 0x, quanto segue mostra che puoi avvicinarti abbastanza:

template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
    // remove reference required for the case that x is an lvalue
    return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}

void test()
{
    auto v = MakeVariable(2); // v is of type Variable<int>
}

0

Hai ragione, il compilatore potrebbe facilmente indovinare, ma non è nello standard o C ++ 0x per quanto ne so, quindi dovrai aspettare almeno altri 10 anni (tasso di turn around fisso degli standard ISO) prima che i fornitori di compilatori aggiungano questa funzione


Non è corretto con il prossimo standard verrà introdotta una parola chiave automatica. Dai un'occhiata al post di James Hopkins in questo thread. stackoverflow.com/questions/984394/… . Mostra come sarà possibile in C ++ 0x.
ovanes il

1
Giusto per correggermi, la parola chiave auto è presente anche nello standard attuale, ma per uno scopo diverso.
ovanes

Sembra che saranno passati 8 anni (dal momento in cui è stata data questa risposta) ... quindi 10 anni non erano una cattiva ipotesi, anche se nel frattempo ci sono stati due standard!
Kyle Strand

-1

Diamo un'occhiata al problema con riferimento a una classe con cui tutti dovrebbero avere familiarità - std :: vector.

In primo luogo, un uso molto comune di vector è utilizzare il costruttore che non accetta parametri:

vector <int> v;

In questo caso, ovviamente, non è possibile eseguire alcuna inferenza.

Un secondo uso comune è creare un vettore pre-dimensionato:

vector <string> v(100);

Qui, se fosse usata l'inferenza:

vector v(100);

otteniamo un vettore di int, non stringhe, e presumibilmente non è dimensionato!

Infine, considera i costruttori che accettano più parametri, con "inferenza":

vector v( 100, foobar() );      // foobar is some class

Quale parametro dovrebbe essere utilizzato per l'inferenza? Avremmo bisogno di un modo per dire al compilatore che dovrebbe essere il secondo.

Con tutti questi problemi per una classe semplice come il vettore, è facile capire perché l'inferenza non viene utilizzata.


3
Penso che tu stia fraintendendo l'idea. L'inferenza del tipo per i costruttori si verifica solo SE il tipo di modello fa parte del costruttore. Supponiamo che il vettore abbia il modello di definizione del modello <typename T>. Il tuo esempio non è un problema perché il costruttore del vettore sarebbe definito come vettore (dimensione int), non come vettore (dimensione T). Solo nel caso del vettore (dimensione T) si avrebbe un'inferenza; nel primo esempio, il compilatore darebbe un errore dicendo che T è indefinito. Essenzialmente identico a come funziona l'inferenza del modello di funzione.
GRB

Quindi avverrebbe solo per i costruttori che hanno un singolo parametro e dove quel parametro è un tipo di parametro del modello? Sembra un numero incredibilmente piccolo di casi.

Non deve essere necessariamente un singolo parametro. Ad esempio, si potrebbe avere un costruttore vettoriale di vector (int size, T firstElement). Se un modello ha più parametri (template <typename T, typename U>), uno potrebbe avere Holder :: Holder (T firstObject, U secondObject). Se un modello ha più parametri ma il costruttore ne prende solo uno, ad esempio Holder (U secondObject), allora T dovrebbe sempre essere dichiarato esplicitamente. Le regole dovrebbero essere il più possibile simili all'inferenza del modello di funzione.
GRB

-2

Rendendo il ctor un modello la variabile può avere una sola forma ma diversi ctor:

class Variable {
      obj data; // let the compiler guess
      public:
      template<typename obj>
      Variable(obj d)
       {
           data = d;
       }
};

int main()
{
    int num = 2;
    Variable var(num);  // Variable::data int?

    float num2 = 2.0f;
    Variable var2(num2);  // Variable::data float?
    return 0;         
}

Vedere? Non possiamo avere più membri Variable :: data.


Non avrebbe senso in nessuno scenario. obj in termini di dati obj non è definito poiché quella classe non è più un modello. Tale codice non sarebbe valido in entrambi i casi.
GRB

Volevo il comportamento del compilatore che descrivi, quindi ho trovato un modo per aggirare quella restrizione (nel mio caso), che potresti trovare interessante, stackoverflow.com/questions/228620/garbage-collection-in-c-why/…
Nick Dandoulakis

-2

Vedere la deduzione dell'argomento del modello C ++ per ulteriori informazioni su questo argomento .


4
Ho letto questo articolo prima e non sembrava parlare molto di quello che sto dicendo. L'unico caso in cui lo scrittore sembra parlare di deduzione degli argomenti riguardo alle lezioni è quando dice che non può essere fatto all'inizio dell'articolo;) - se potessi indicare le sezioni che ritieni rilevanti anche se io " Lo apprezzerei davvero.
GRB
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.