web-dev-qa-db-fra.com

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

ma question d'aujourd'hui est assez simple: pourquoi le compilateur ne peut-il pas déduire des paramètres de modèle à partir de constructeurs de classe, tout comme il peut le faire à partir de paramètres de fonction? Par exemple, pourquoi le code suivant ne pouvait-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, donc ma question est pourquoi n'est-ce pas? Cela permettrait-il de créer des trous syntaxiques majeurs? Existe-t-il une instance où l'on ne voudrait pas de cette fonctionnalité (où l'inférence d'un type entraînerait des problèmes)? J'essaie simplement de comprendre la logique derrière l'autorisation de l'inférence de modèle pour les fonctions, mais pas pour les classes convenablement construites.

102
GRB

Je pense que ce n'est pas valable 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 utilisez 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 le MyClass pm;

Je ne sais pas si ce que j'ai dit est logique, mais n'hésitez pas à ajouter un commentaire, c'est une question intéressante.

C++ 17

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

Exemples:

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

Papier accepté .

46
Drahakar

Vous ne pouvez pas faire ce que vous demandez pour des raisons que d'autres personnes ont abordé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);
}

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 à partir du constructeur (nommé).

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

Variable<T> v = make_variable(instance);
27
Lionel

À 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 supportant le C++ 17 standard sera compiler votre code tel quel.

Déduction des arguments de modèle pour les modèles de classe en C++ 17

Ici (avec l'aimable autorisation d'une révision par Olzhas Zhumabek de la réponse acceptée) est le document détaillant les modifications pertinentes de la norme.

Répondre aux préoccupations des autres réponses

La réponse actuellement 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 et operator= n'existent que pour un connu type de modèle:

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 noté dans les commentaires, il y a aucune raison pour que MyClass *pm Soit une déclaration légale avec ou sans la nouvelle forme d'inférence: MyClass n'est pas un type (c'est un modèle), il n'est donc pas logique 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, pm est déjà du type correct, et donc l'inférence est triviale. De plus, il est impossible de accidentellement mix types lors de l'appel du constructeur de copie:

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

Ici, pm sera un pointeur vers une copie de m. Ici, MyClass est en cours de construction à partir de m— qui est de type MyClass<string> (Et pas de type inexistant MyClass). Ainsi, au point où le type de pm est déduit, il y a is 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 toujoursdéclenche une erreur de compilation :

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

En effet, la déclaration du constructeur de copie 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 le type de modèle de la classe dans son ensemble; c'est-à-dire, lorsque MyClass<string> est instancié, MyClass<string>::MyClass(const MyClass<string>&); est instancié avec lui, et lorsque 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 des modèles 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 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 à peu près la lisibilité du code.

Comme indiqué dans l'exemple précédent, Variable lui-même est pas un nom de type, même si la nouvelle fonctionnalité lui donne un aspect syntaxique.

Pitiș demande ensuite ce qui se passerait si aucun constructeur n'était 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 de constructeur . Sans appel constructeur, il y a pas d'inférence.

Cela revient à demander quelle version de foo est 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

Pour autant que je sache, c'est la seule réponse qui soulève une inquiétude 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 le constructeur copie?

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 standard, que je ne comprends pas encore.

Je ne sais pas non plus pourquoi la déduction var4 Est illégale; l'erreur de compilation de g ++ semble indiquer que l'instruction est analysée en tant que déclaration de fonction.

21
Kyle Strand

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>> ?
}
11
MSalters

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 à peu près 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: spécialisation partielle (ou complète) des modèles.

Que se passe-t-il si je spécialise Variable et ne propose aucun constructeur comme vous vous y attendez?

J'aurais donc:

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

Ensuite, j'ai le code:

Variable v( 10);

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

9
Cătălin Pitiș

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

Mais il existe une proposition de "Déduction des paramètres de modèle pour les constructeurs" afin que vous puissiez obtenir ce que vous demandez bientôt. 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/papers/2015/p0091r0.html

6
ChetS

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 paramètrent 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);
}
2
rlbond

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

Alors que exactement ce que vous suggérez ne sera pas possible en C++ 0x, ce qui suit montre que vous pouvez être assez proche:

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>
}
1
James Hopkin

Vous avez raison, le compilateur pourrait facilement le 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é

0
Robert Gould