Pourquoi ne pas déduire le paramètre de modèle du constructeur?

102

ma question aujourd'hui est assez simple: pourquoi le compilateur ne peut-il pas déduire les paramètres de modèle à partir des constructeurs de classe, autant qu'il peut le faire à partir des paramètres de fonction? Par exemple, pourquoi le code suivant ne pourrait-il pas être valide:

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
}

Comme je l'ai dit, je comprends que ce n'est pas valide, alors ma question est de savoir pourquoi n'est-ce pas? Autoriser cela créerait-il des trous syntaxiques majeurs? Existe-t-il une instance où l'on ne voudrait pas de cette fonctionnalité (où inférer un type causerait des problèmes)? J'essaie juste de comprendre la logique derrière l'autorisation de l'inférence de modèle pour les fonctions, mais pas pour les classes correctement construites.

GRB
la source
J'inviterais quelqu'un (je le fais, mais pas maintenant), à compiler la réponse de Drahakar et Pitis (au moins) comme de bons contre-exemples de pourquoi cela ne peut pas fonctionner
jpinto3912
2
Notez également que cela est facilement contourné viatemplate<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
Mooing Duck
3
Vous pouvez en quelque sorte obtenir ce que vous voulez var = Variable <decltype (n)> (n);
QuentinUK
18
C ++ 17 permettra cela! Cette proposition a été acceptée: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d Excellent! À propos du temps! Cela me semblait naturel que ce soit la façon dont cela devrait fonctionner, et la source d'irritation que cela n'a pas fait.
amdn le

Réponses:

46

Je pense que ce n'est pas valide car le constructeur n'est pas toujours le seul point d'entrée de la classe (je parle de constructeur de copie et d'opérateur =). Supposons donc que vous utilisiez votre classe comme ceci:

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

Je ne sais pas s'il serait si évident pour l'analyseur de savoir quel type de modèle est MyClass pm;

Je ne sais pas si ce que j'ai dit a du sens, mais n'hésitez pas à ajouter quelques commentaires, c'est une question intéressante.

C ++ 17

Il est admis que C ++ 17 aura une déduction de type à partir des arguments du constructeur.

Exemples:

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

Papier accepté .

Drahakar
la source
8
C'est en fait un excellent point que je n'ai jamais considéré. Je ne vois aucun moyen de contourner le fait que le pointeur devrait être spécifique au type (c'est-à-dire qu'il devrait être MyClass <string> * pm). Si tel est le cas, tout ce que vous finirez par faire est de vous éviter de spécifier le type lors de l'instanciation; quelques simples caractères de travail supplémentaire (et seulement si l'objet est créé sur la pile, pas sur le tas, comme ci-dessus). J'ai toujours soupçonné que l'inférence de classe pouvait ouvrir une boîte syntaxique de vers, et je pense que c'est peut-être ça.
GRB
2
Je ne vois pas tout à fait comment autoriser l'inférence de paramètres de modèle à partir de constructeurs nécessiterait d'autoriser des déclarations non spécialisées sans appels de constructeur, comme dans votre deuxième ligne. Ie, MyClass *pmici serait invalide pour la même raison qu'une fonction déclarée template <typename T> void foo();ne peut pas être appelée sans spécialisation explicite.
Kyle Strand
3
@KyleStrand Oui, en disant 'les arguments de modèle de classe ne peuvent pas être déduits de leurs constructeurs car [exemple qui n'utilise aucun constructeur] ', cette réponse est complètement hors de propos. Je ne peux vraiment pas croire qu'il a été accepté, a atteint +29, a mis 6 ans à quelqu'un pour remarquer le problème flagrant et s'est assis sans un seul vote négatif pendant 7 ans. Personne d'autre ne pense en lisant, ou ...?
underscore_d
1
@underscore_d J'aime comment, dans sa forme actuelle, cette réponse dit "il pourrait y avoir des problèmes avec cette proposition; Je ne suis pas sûr si ce que je viens de dire a du sens (!), n'hésitez pas à commenter (!!); et Oh au fait, c'est à peu près exactement comment fonctionnera C ++ 17. "
Kyle Strand
1
@KyleStrand Ah oui, c'est encore un autre problème, que j'ai remarqué mais que j'ai oublié de mentionner parmi tous les autres amusants. La modification concernant C ++ 17 n'a pas été faite par l'OP ... et l'OMI n'aurait pas dû être approuvée, mais publiée comme une nouvelle réponse: elle aurait été déclinée en tant que `` change le sens du message '' même si le message avait Cela n'avait pas de sens pour commencer ... Je n'étais pas au courant que l'édition dans des sections entièrement nouvelles était un jeu équitable et j'ai certainement eu des modifications moins radicales rejetées, mais je suppose que c'est la chance du tirage au sort en termes de critiques que vous obtenez.
underscore_d
27

Vous ne pouvez pas faire ce que vous demandez pour les raisons que d'autres personnes ont adressées, mais vous pouvez le faire:

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

ce qui à toutes fins utiles est la même chose que vous demandez. Si vous aimez l'encapsulation, vous pouvez faire de make_variable une fonction membre statique. C'est ce que les gens appellent constructeur nommé. Donc, non seulement il fait ce que vous voulez, mais il s'appelle presque ce que vous voulez: le compilateur déduit le paramètre de modèle du constructeur (nommé).

NB: tout compilateur raisonnable optimisera l'objet temporaire lorsque vous écrivez quelque chose comme

auto v = make_variable(instance);
Lionel
la source
6
Je tiens à souligner qu'il n'est pas particulièrement utile de rendre la fonction membre statique dans ce cas, car pour cela, vous devrez spécifier un argument de modèle pour qu'une classe l'appelle de toute façon, il ne serait donc pas utile de la déduire.
Predelnik
3
Et encore mieux en C ++ 11, vous pouvez le faire auto v = make_variable(instance)pour ne pas avoir à spécifier le type
Claudiu
1
Ouais, lol à l'idée de déclarer la fonction make en tant que staticmembre ... Pensez-y pendant une seconde. Cela mis à part: les fonctions make gratuites étaient en effet la solution, mais c'est beaucoup de passe-partout redondant, que pendant que vous le tapez, vous savez juste que vous ne devriez pas avoir à le faire car le compilateur a accès à toutes les informations que vous répétez. .. et heureusement C ++ 17 canonise cela.
underscore_d
21

À l'ère éclairée de 2016, avec deux nouvelles normes à notre actif depuis que cette question a été posée et une nouvelle juste au coin de la rue, la chose cruciale à savoir est que les compilateurs prenant en charge la norme C ++ 17 compileront votre code tel quel. .

Déduction d'argument de modèle pour les modèles de classe en C ++ 17

Voici (grâce à une modification par Olzhas Zhumabek de la réponse acceptée) le document détaillant les changements pertinents apportés à la norme.

Répondre aux préoccupations d'autres réponses

La réponse actuelle la mieux notée

Cette réponse souligne que "copier le constructeur et operator=" ne connaîtrait pas les spécialisations de modèle correctes.

C'est absurde, car le constructeur de copie standard operator= n'existe que pour un type de modèle connu :

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;

Ici, comme je l' ai mentionné dans les commentaires, il n'y a aucune raison pour MyClass *pmêtre une déclaration juridique avec ou sans la nouvelle forme d'inférence: MyClass n'est pas un type (il est un modèle), il n'a pas de sens de déclarer un pointeur de type MyClass. Voici une façon possible de corriger l'exemple:

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

Ici, pmest déjà du type correct, et donc l'inférence est triviale. De plus, il est impossible de mélanger accidentellement des types lors de l'appel du constructeur de copie:

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

Ici, pmsera un pointeur vers une copie de m. Ici, MyClassest en cours de construction de copie - mqui est de type MyClass<string>(et non de type inexistant MyClass). Ainsi, au moment où pmle type de s est déduit, il y a suffisamment d'informations pour savoir que le type de modèle de m, et donc le type de modèle de pm, est string.

De plus, ce qui suit provoquera toujours une erreur de compilation :

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

C'est parce que la déclaration du constructeur de copie n'est pas basée sur un modèle:

MyClass(const MyClass&);

Ici, le type de modèle de l'argument du constructeur de copie correspond au type de modèle de la classe dans son ensemble; c'est-à-dire, quand MyClass<string>est instancié, MyClass<string>::MyClass(const MyClass<string>&);est instancié avec lui, et quand MyClass<int>est instancié, MyClass<int>::MyClass(const MyClass<int>&);est instancié. À moins qu'il ne soit explicitement spécifié ou qu'un constructeur basé sur un modèle soit déclaré, il n'y a aucune raison pour que le compilateur instancie MyClass<int>::MyClass(const MyClass<string>&);, ce qui serait évidemment inapproprié.

La réponse de Cătălin Pitiș

Pitiș donne un exemple en déduisant Variable<int>et Variable<double>, puis déclare:

J'ai le même nom de type (variable) dans le code pour deux types différents (variable et variable). De mon point de vue subjectif, cela affecte sensiblement la lisibilité du code.

Comme indiqué dans l'exemple précédent, Variable lui-même n'est pas un nom de type, même si la nouvelle fonctionnalité le fait ressembler syntaxiquement.

Pitiș demande alors ce qui se passerait si aucun constructeur n'est donné qui permettrait l'inférence appropriée. La réponse est qu'aucune inférence n'est autorisée, car l'inférence est déclenchée par l' appel du constructeur . Sans appel de constructeur, il y a pas d'inférence .

Cela revient à demander quelle version de fooest déduite ici:

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

La réponse est que ce code est illégal, pour la raison indiquée.

Réponse de MSalter

C'est, pour autant que je sache, la seule réponse pour soulever une préoccupation légitime concernant la fonctionnalité proposée.

L'exemple est:

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

La question clé est la suivante: le compilateur sélectionne-t -il ici le constructeur déduit du type ou la copie constructeur de ?

En essayant le code, nous pouvons voir que le constructeur de copie est sélectionné. Pour développer l'exemple :

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

Je ne sais pas comment la proposition et la nouvelle version de la norme le précisent; il semble être déterminé par des «guides de déduction», qui sont un nouveau morceau de standard que je ne comprends pas encore.

Je ne sais pas non plus pourquoi la var4déduction est illégale; l'erreur du compilateur de g ++ semble indiquer que l'instruction est analysée comme une déclaration de fonction.

Kyle Strand
la source
Quelle belle réponse détaillée! var4est juste un cas de "l'analyse la plus vexante" (non liée à la déduction d'argument modèle). Nous avions l'habitude d'utiliser simplement des parens supplémentaires pour cela, mais ces jours-ci, je pense que l'utilisation d'accolades pour désigner sans ambiguïté la construction est le conseil habituel.
Sumudu Fernando
@SumuduFernando Merci! Voulez-vous dire que cela Variable var4(Variable(num));est traité comme une déclaration de fonction? Si tel est le cas, pourquoi Variable(num)une spécification de paramètre est -elle valide?
Kyle Strand
@SumuduFernando Qu'à cela ne tienne, je ne savais pas que c'était valable: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand
11

Toujours manquant: cela rend le code suivant assez ambigu:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}
MSalters
la source
Un autre bon point. En supposant qu'il existe une variable définie par le constructeur de copie (Variable <obj> d), il devrait y avoir une sorte de priorité établie.
GRB
1
Ou, alternativement, demandez au compilateur de renvoyer une erreur de paramètre de modèle non défini, un peu comme je l'ai suggéré en ce qui concerne la réponse de Pitis. Cependant, si vous prenez cette route, le nombre de fois où l'inférence peut se produire sans problèmes (erreurs) devient de plus en plus petit.
GRB
C'est en fait un point intéressant, et (comme je l'ai noté dans ma réponse) je ne suis pas encore sûr de la manière dont la proposition C ++ 17 acceptée résout ce problème.
Kyle Strand
9

Supposons que le compilateur supporte ce que vous avez demandé. Alors ce code est valide:

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

// Some code here

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

Maintenant, j'ai le même nom de type (Variable) dans le code pour deux types différents (Variable et Variable). De mon point de vue subjectif, cela affecte sensiblement la lisibilité du code. Avoir le même nom de type pour deux types différents dans le même espace de noms me semble trompeur.

Mise à jour ultérieure: Autre chose à considérer: la spécialisation partielle (ou complète) des modèles.

Et si je me spécialise dans Variable et que je ne fournis aucun constructeur comme prévu?

Donc j'aurais:

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

Ensuite, j'ai le code:

Variable v( 10);

Que doit faire le compilateur? Utilisez la définition de classe Variable générique pour déduire qu'il s'agit d'une variable, puis découvrez que Variable ne fournit pas un constructeur de paramètre?

Cătălin Pitiș
la source
1
Pire encore: que faire si vous n'avez que Variable <int> :: Variable (float)? Vous avez maintenant deux façons de déduire la variable (1f) et aucun moyen de déduire la variable (1).
MSalters
C'est un bon point, mais pourrait être facilement dépassé par le casting: Variable v1 ((double) 10)
jpinto3912
Je suis d'accord que la lisibilité du code est une question subjective, cependant, je suis d'accord à 100% avec ce que vous dites sur la spécialisation des modèles. La solution serait probablement de donner une erreur de paramètre de modèle indéfini (une fois que le compilateur regarde la spécialisation <int> et ne voit aucun constructeur valide, dites-lui qu'il n'a aucune idée du modèle que vous voulez utiliser et que vous devez spécifier explicitement) mais Je conviens que ce n'est pas une jolie solution. J'ajouterais cela comme un autre trou syntaxique majeur qui devrait être traité (mais qui pourrait être résolu si l'on en accepte les conséquences).
GRB
4
@ jpinto3912 - vous manquez le point. Le compilateur doit instancier TOUTES les variables <T> possibles pour vérifier si N'IMPORTE QUELLE variable variable <T> :: Variable fournit un facteur ambigu. Se débarrasser de l'ambiguïté n'est pas le problème - instanciez simplement la Variable <double> vous-même si c'est ce que vous voulez. C'est trouver cette ambiguïté en premier lieu qui la rend impossible.
MSalters
6

Le C ++ 03 et le standard C ++ 11 n'autorisent pas la déduction d'arguments de modèle à partir des paramètres passés au constructeur.

Mais il existe une proposition de "Déduction de paramètres de modèle pour les constructeurs" afin que vous obteniez bientôt ce que vous demandez. Edit: en effet, cette fonctionnalité a été confirmée pour C ++ 17.

Voir: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html et http://www.open-std.org/jtc1/sc22/wg21/docs/ papiers / 2015 / p0091r0.html

Chets
la source
La fonctionnalité a été ajoutée à C ++ 17, mais pas si «bientôt» s'applique à une période de 6 à 8 ans. ;)
ChetS
2

De nombreuses classes ne dépendent pas des paramètres du constructeur. Il n'y a que quelques classes qui n'ont qu'un seul constructeur et qui sont paramétrées en fonction du ou des types de ce constructeur.

Si vous avez vraiment besoin d'une inférence de modèle, utilisez une fonction d'assistance:

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);
}
rlbond
la source
1
Bien sûr, cette fonctionnalité ne serait utile que pour certaines classes, mais il en va de même pour l'inférence de fonction. Toutes les fonctions basées sur un modèle ne prennent pas non plus leurs paramètres de la liste d'arguments, mais nous autorisons l'inférence pour les fonctions qui le font.
GRB
1

La déduction des types est limitée aux fonctions de modèle dans le C ++ actuel, mais on s'est rendu compte depuis longtemps que la déduction de types dans d'autres contextes serait très utile. D'où C ++ 0x auto.

Bien que ce que vous suggérez ne soit pas possible en C ++ 0x, ce qui suit montre que vous pouvez vous en approcher assez:

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>
}
James Hopkin
la source
0

Vous avez raison, le compilateur pourrait facilement deviner, mais ce n'est pas dans la norme ou C ++ 0x pour autant que je sache, vous devrez donc attendre au moins 10 ans de plus (taux de rotation fixe des normes ISO) avant que les fournisseurs de compilateurs ajoutent cette fonctionnalité

Robert Gould
la source
Ce n'est pas correct avec la norme à venir, un mot-clé automatique sera introduit. Jetez un œil au post de James Hopkins dans ce fil. stackoverflow.com/questions/984394/… . Il montre comment cela sera possible en C ++ 0x.
ovanes
1
Juste pour me corriger, le mot-clé auto est également présent dans la norme actuelle, mais à des fins différentes.
ovanes
On dirait que ça fera 8 ans (à partir du moment de cette réponse) ... donc 10 ans n'était pas une mauvaise estimation, même s'il y a eu deux normes entre-temps!
Kyle Strand
-1

Regardons le problème en référence à une classe que tout le monde devrait connaître - std :: vector.

Tout d'abord, une utilisation très courante du vecteur est d'utiliser le constructeur qui ne prend aucun paramètre:

vector <int> v;

Dans ce cas, évidemment aucune inférence ne peut être effectuée.

Une deuxième utilisation courante consiste à créer un vecteur prédimensionné:

vector <string> v(100);

Ici, si l'inférence était utilisée:

vector v(100);

nous obtenons un vecteur d'entiers, pas de chaînes, et vraisemblablement il n'est pas dimensionné!

Enfin, considérez les constructeurs qui acceptent plusieurs paramètres - avec "inférence":

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

Quel paramètre doit être utilisé pour l'inférence? Nous aurions besoin d'un moyen de dire au compilateur que ce devrait être le deuxième.

Avec tous ces problèmes pour une classe aussi simple que vectorielle, il est facile de comprendre pourquoi l'inférence n'est pas utilisée.


la source
3
Je pense que vous ne comprenez pas l'idée. L'inférence de type pour les constructeurs ne se produirait que SI le type de modèle fait partie du constructeur. Supposons que le vecteur possède le modèle de définition de modèle <nom de type T>. Votre exemple n'est pas un problème car le constructeur du vecteur serait défini comme vecteur (taille int), pas comme vecteur (taille T). Ce n'est que dans le cas du vecteur (taille T) qu'une inférence se produira; dans le premier exemple, le compilateur donnerait une erreur indiquant que T n'est pas défini. Essentiellement identique au fonctionnement de l'inférence de modèle de fonction.
GRB
Donc, cela n'aurait lieu que pour les constructeurs qui ont un seul paramètre et où ce paramètre est un type de paramètre de modèle? Cela semble un nombre infiniment petit de cas.
Il ne doit pas nécessairement s'agir d'un paramètre unique. Par exemple, on pourrait avoir un constructeur de vecteur de vector (taille int, T firstElement). Si un template a plusieurs paramètres (template <typename T, typename U>), on pourrait avoir Holder :: Holder (T firstObject, U secondObject). Si un modèle a plusieurs paramètres mais que le constructeur n'en prend qu'un, par exemple Holder (U secondObject), alors T devrait toujours être explicitement indiqué. Les règles seraient conçues pour être aussi similaires que possible à l'inférence de modèle de fonction.
GRB
-2

Faire du ctor un modèle la Variable ne peut avoir qu'une seule forme mais plusieurs cteurs:

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;         
}

Voir? Nous ne pouvons pas avoir plusieurs membres Variable :: data.

Nick Dandoulakis
la source
Cela n'aurait aucun sens dans aucun scénario. obj en termes de données obj n'est pas défini car cette classe n'est plus un modèle. Un tel code serait invalide dans les deux cas.
GRB
Je voulais le comportement du compilateur que vous décrivez, donc je trouve un moyen de contourner cette restriction (dans mon cas), que vous pourriez trouver intéressant, stackoverflow.com/questions/228620/garbage-collection-in-c-why/...
Nick Dandoulakis
-2

Voir la déduction d'argument de modèle C ++ pour plus d'informations à ce sujet.

Igor Krivokon
la source
4
J'ai lu cet article avant et il ne semblait pas beaucoup parler de ce que je dis. La seule fois où l'écrivain semble parler de déduction d'arguments en ce qui concerne les cours, c'est quand il dit que cela ne peut pas être fait en haut de l'article;) - si vous pouviez indiquer les sections que vous pensez pertinentes bien que je '' J'apprécie vraiment ça.
GRB