Pourquoi les références ne sont-elles pas «const» en C ++?

86

Nous savons qu'une "variable const" indique qu'une fois assignée, vous ne pouvez pas changer la variable, comme ceci:

int const i = 1;
i = 2;

Le programme ci-dessus échouera à se compiler; gcc invite avec une erreur:

assignment of read-only variable 'i'

Pas de problème, je peux le comprendre, mais l'exemple suivant dépasse ma compréhension:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

Il sort

true
false

Bizarre. Nous savons qu'une fois qu'une référence est liée à un nom / variable, nous ne pouvons pas modifier cette liaison, nous changeons son objet lié. Donc, je suppose que le type de ridevrait être le même que i: quand iest un int const, pourquoi ripas const?

Troskyvs
la source
3
En outre, boolalpha(cout)c'est très inhabituel. Vous pourriez faire à la std::cout << boolalphaplace.
isanae le
6
Voilà pour riêtre un "alias" indiscernable i.
Kaz du
1
En effet, une référence est toujours liée au même objet. iest également une référence mais pour des raisons historiques, vous ne le déclarez pas comme tel de manière explicite. Ainsi iest une référence qui fait référence à un stockage et riest une référence qui fait référence au même stockage. Mais il n'y a pas de différence de nature entre i et ri. Comme vous ne pouvez pas modifier la liaison d'une référence, il n'est pas nécessaire de la qualifier comme const. Et laissez-moi affirmer que le commentaire @Kaz est bien meilleur que la réponse validée (n'expliquez jamais les références à l'aide de pointeurs, un ref est un nom, un ptr est une variable).
Jean-Baptiste Yunès
1
Question fantastique. Avant de voir cet exemple, je me serais attendu is_constà revenir truedans ce cas également. À mon avis, c'est un bon exemple de pourquoi constest fondamentalement en arrière; un attribut «mutable» (à la Rust mut) semble qu'il serait plus cohérent.
Kyle Strand
1
Peut-être que le titre devrait être changé en "pourquoi est is_const<int const &>::valuefaux?" ou similaire; J'ai du mal à voir un sens à la question autre que de poser des questions sur le comportement des traits de type
MM

Réponses:

52

Cela peut sembler contre-intuitif, mais je pense que la manière de comprendre cela est de se rendre compte que, à certains égards, les références sont traitées syntaxiquement comme des pointeurs .

Cela semble logique pour un pointeur :

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Production:

true
false

C'est logique car nous savons que ce n'est pas l' objet pointeur qui est const (on peut le faire pointer ailleurs), c'est l'objet vers lequel pointe.

Nous voyons donc correctement la constness du pointeur lui-même renvoyé comme false.

Si nous voulons créer le pointeur lui-même, constnous devons dire:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Production:

true
true

Et donc je pense que nous voyons une analogie syntaxique avec la référence .

Cependant, les références sont sémantiquement différentes des pointeurs, en particulier sur un point crucial, nous ne sommes pas autorisés à relier une référence à un autre objet une fois liée.

Ainsi, même si les références partagent la même syntaxe que les pointeurs, les règles sont différentes et donc le langage nous empêche de déclarer la référence elle-même constcomme ceci:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

Je suppose que nous ne sommes pas autorisés à faire cela car cela ne semble pas être nécessaire lorsque les règles de langage empêchent la référence d'être rebondie de la même manière qu'un pointeur le pourrait (s'il n'est pas déclaré const).

Donc pour répondre à la question:

Q) Pourquoi «reference» n'est pas un «const» en C ++?

Dans votre exemple, la syntaxe fait référence à constl' objet de la même manière que si vous déclariez un pointeur .

À tort ou à raison, nous ne sommes pas autorisés à faire la référence elle-même, constmais si nous l'étions, cela ressemblerait à ceci:

int const& const ri = i; // not allowed

Q) nous savons qu'une fois qu'une référence est liée à un nom / variable, nous ne pouvons pas changer cette liaison, nous changeons son objet lié. Donc je suppose que le type de ridevrait être le même que i: quand iest un int const, pourquoi ripas const?

Pourquoi le n'est-il decltype()pas transféré à l'objet auquel la référence est liée?

Je suppose que c'est pour l'équivalence sémantique avec des pointeurs et peut-être aussi que la fonction de decltype()(type déclaré) est de revenir sur ce qui a été déclaré avant que la liaison n'ait lieu.

Galik
la source
13
On dirait que vous dites "les références ne peuvent pas être const parce qu'elles sont toujours const"?
ruakh
2
Il peut être vrai que " sémantiquement toutes les actions sur eux après leur liaison sont transférés à l'objet auquel ils se réfèrent", mais l'OP s'attendait à ce que cela s'applique decltypeégalement et a constaté que non.
ruakh
10
Il y a beaucoup de suppositions sinueuses et d'analogies douteuses aux pointeurs ici, ce qui, je pense, confond le problème plus que nécessaire, lorsque la vérification de la norme comme sonyuanyao l'a fait va droit au but réel: une référence n'est pas un type qualifiable cv, donc il ne peut pas être const, donc std::is_constdoit revenir false. Ils auraient pu à la place utiliser un libellé qui signifiait qu'il devait revenir true, mais ils ne l'ont pas fait. C'est ça! Tout ce truc sur les pointeurs, "je suppose", "je suppose", etc. ne fournit aucune explication réelle.
underscore_d
1
@underscore_d C'est probablement une meilleure question pour le chat que pour la section commentaires ici, mais avez-vous un exemple de "confusion inutile" de cette façon de penser les références? S'ils peuvent être mis en œuvre de cette façon, quel est le problème à y penser de cette façon? (Je ne pense pas que cette question compte car ce decltypen'est pas une opération d'exécution, donc l'idée "à l'exécution, les références se comportent comme des pointeurs", qu'elle soit correcte ou non, ne s'applique pas vraiment.
Kyle Strand
1
Les références ne sont pas non plus traitées syntaxiquement comme des pointeurs. Ils ont une syntaxe différente à déclarer et à utiliser. Donc je ne suis même pas sûr de ce que tu veux dire.
Jordan Melo
55

pourquoi "ri" n'est-il pas "const"?

std::is_const vérifie si le type est qualifié const ou non.

Si T est un type qualifié par const (c'est-à-dire const ou const volatile), fournit la valeur de constante de membre égale à true. Pour tout autre type, la valeur est false.

Mais la référence ne peut pas être qualifiée const. Références [dcl.ref] / 1

Les références qualifiées Cv sont mal formées sauf lorsque les qualificatifs cv sont introduits via l'utilisation d'un typedef-name ([dcl.typedef], [temp.param]) ou decltype-specifier ([dcl.type.simple]) , auquel cas les qualificatifs cv sont ignorés.

Donc is_const<decltype(ri)>::valueretournera falseparce que ri(la référence) n'est pas un type qualifié const. Comme vous l'avez dit, nous ne pouvons pas relier une référence après l'initialisation, ce qui implique que la référence est toujours "const", d'un autre côté, une référence qualifiée par const ou une référence non qualifiée par const peut ne pas avoir de sens en fait.

songyuanyao
la source
5
Droit à la norme et donc droit au but, plutôt que toute la supposition gaufrante de la réponse acceptée.
underscore_d
5
@underscore_d C'est une bonne réponse jusqu'à ce que vous reconnaissiez que l'op demandait également pourquoi. «Parce qu'il le dit» n'est pas vraiment utile pour cet aspect de la question.
simpleuser
5
@ user9999999 L'autre réponse ne répond pas vraiment non plus à cet aspect. Si une référence ne peut pas être rebondie, pourquoi ne is_constrevient pas true? Cette réponse tente de faire une analogie avec la façon dont les pointeurs peuvent éventuellement être réinstallés, alors que les références ne le sont pas - et, ce faisant, conduit à une auto-contradiction pour la même raison. Je ne suis pas sûr qu'il y ait une véritable explication de toute façon, autre qu'une décision quelque peu arbitraire de ceux qui rédigent le Standard, et parfois c'est la meilleure que nous puissions espérer. D'où cette réponse.
underscore_d
2
Un autre aspect important, je pense, est que ce decltypen'est pas une fonction et donc travaille directement sur la référence elle-même plutôt que sur l'objet référencé. (Ceci est peut-être plus pertinent pour la réponse "les références sont essentiellement des pointeurs", mais je pense toujours que cela fait partie de ce qui rend cet exemple déroutant et mérite donc d'être mentionné ici.)
Kyle Strand
3
Pour élucider davantage le commentaire de @KyleStrand: decltype(name)agit différemment d'un général decltype(expr). Ainsi, par exemple, decltype(i)le type déclaré iest-il const int, alors que le decltype((i))serait int const &.
Daniel Schepler
18

Vous devez utiliser std::remove_referencepour obtenir la valeur que vous recherchez.

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

Pour plus d'informations, consultez cet article .

chema989
la source
8

Pourquoi les macros ne le sont-elles pas const? Les fonctions? Littéraux? Les noms des types?

const les choses ne sont qu'un sous-ensemble de choses immuables.

Étant donné que les types de référence ne sont que cela - des types - il aurait peut-être été judicieux d'exiger le constqualificatif-sur tous pour la symétrie avec d'autres types (en particulier avec les types de pointeur), mais cela deviendrait très fastidieux très rapidement.

Si C ++ avait des objets immuables par défaut, nécessitant le mutablemot clé sur tout ce que vous ne vouliez pas être const, alors cela aurait été facile: ne permettez simplement pas aux programmeurs d'ajouter mutabledes types de référence.

En l'état, ils sont immuables sans réserve.

Et, comme ils ne sont pas constqualifiés, il serait probablement plus déroutant pour is_constun type de référence de donner true.

Je trouve que c'est un compromis raisonnable, d'autant plus que l'immuabilité est de toute façon imposée par le simple fait qu'aucune syntaxe n'existe pour muter une référence.

Courses de légèreté en orbite
la source
6

C'est une particularité / fonctionnalité en C ++. Bien que nous ne considérions pas les références comme des types, elles «se trouvent» en fait dans le système de types. Bien que cela semble gênant (étant donné que lorsque des références sont utilisées, la sémantique de référence se produit automatiquement et la référence «s'écarte»), il y a des raisons défendables pour lesquelles les références sont modélisées dans le système de types plutôt que comme un attribut séparé en dehors de type.

Tout d'abord, considérons que tous les attributs d'un nom déclaré ne doivent pas nécessairement être dans le système de types. Du langage C, nous avons "classe de stockage" et "liaison". Un nom peut être introduit comme extern const int ri, où le externindique la classe de stockage statique et la présence d'un lien. Le type est juste const int.

C ++ embrasse évidemment la notion que les expressions ont des attributs qui sont en dehors du système de types. Le langage a maintenant un concept de «classe de valeur» qui est une tentative d'organiser le nombre croissant d'attributs non-type qu'une expression peut présenter.

Pourtant, les références sont des types. Pourquoi?

Il était expliqué dans les didacticiels C ++ qu'une déclaration comme const int &riintroduite ricomme ayant un type const int, mais faisant référence à la sémantique. Cette sémantique de référence n'était pas un type; c'était simplement une sorte d'attribut indiquant une relation inhabituelle entre le nom et l'emplacement de stockage. De plus, le fait que les références ne sont pas des types a été utilisé pour expliquer pourquoi vous ne pouvez pas construire de types basés sur des références, même si la syntaxe de construction de type le permet. Par exemple, des tableaux ou des pointeurs vers des références n'étant pas possibles: const int &ari[5]et const int &*pri.

Mais en fait, les références sont des types et decltype(ri)récupèrent ainsi un nœud de type référence qui n'est pas qualifié. Vous devez descendre au-delà de ce nœud dans l'arborescence des types pour accéder au type sous-jacent avec remove_reference.

Lorsque vous utilisez ri, la référence est résolue de manière transparente, de sorte que ri"ressemble et se sent comme i" et peut être appelée un "alias" pour elle. Dans le système de types, cependant, a rien fait un type qui est " référence à const int ".

Pourquoi les types de références?

Considérez que si les références n'étaient pas des types, ces fonctions seraient alors considérées comme ayant le même type:

void foo(int);
void foo(int &);

Cela ne peut tout simplement pas être pour des raisons qui vont de soi. Si elles avaient le même type, cela signifie que l'une ou l'autre des déclarations conviendrait à l'une ou l'autre définition, et donc chaque (int)fonction devrait être soupçonnée de prendre une référence.

De même, si les références n'étaient pas des types, alors ces deux déclarations de classe seraient équivalentes:

class foo {
  int m;
};

class foo {
  int &m;
};

Il serait correct qu'une unité de traduction utilise une déclaration et qu'une autre unité de traduction du même programme utilise l'autre déclaration.

Le fait est qu'une référence implique une différence d'implémentation et il est impossible de séparer cela du type, car le type en C ++ a à voir avec l'implémentation d'une entité: sa «disposition» en bits pour ainsi dire. Si deux fonctions ont le même type, elles peuvent être appelées avec les mêmes conventions d'appel binaire: l'ABI est la même. Si deux structures ou classes ont le même type, leur disposition est la même ainsi que la sémantique d'accès à tous les membres. La présence de références modifie ces aspects des types, et c'est donc une décision de conception simple de les incorporer dans le système de types. (Cependant, notez un contre-argument ici: un membre struct / class peut être static, ce qui change également la représentation; mais ce n'est pas du type!)

Ainsi, les références sont dans le système de types en tant que «citoyens de seconde classe» (pas contrairement aux fonctions et tableaux dans ISO C). Il y a certaines choses que nous ne pouvons pas «faire» avec des références, comme déclarer des pointeurs vers des références ou des tableaux de celles-ci. Mais cela ne veut pas dire qu'ils ne sont pas des types. Ils ne sont tout simplement pas des types d'une manière logique.

Toutes ces restrictions de seconde classe ne sont pas essentielles. Étant donné qu'il existe des structures de références, il pourrait y avoir des tableaux de références! Par exemple

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

Ce n'est tout simplement pas implémenté en C ++, c'est tout. Les pointeurs vers des références n'ont cependant aucun sens, car un pointeur soulevé d'une référence va simplement vers l'objet référencé. La raison probable pour laquelle il n'y a pas de tableaux de références est que les gens de C ++ considèrent les tableaux comme une sorte de fonctionnalité de bas niveau héritée de C qui est cassée de nombreuses manières qui sont irréparables, et ils ne veulent pas toucher les tableaux comme le base pour quelque chose de nouveau. L'existence de tableaux de références, cependant, serait un exemple clair de la façon dont les références doivent être des types.

constTypes non qualifiables: également présents dans l'ISO C90!

Certaines réponses suggèrent que les références ne prennent pas de constqualificatif. C'est plutôt un hareng rouge, car la déclaration const int &ri = ine tente même pas de faire une constréférence qualifiée: c'est une référence à un type qualifié const (qui n'est pas lui-même const). Tout comme const in *ridéclare un pointeur vers quelque chose const, mais ce pointeur n'est pas lui-même const.

Cela dit, il est vrai que les références ne peuvent pas porter le constqualificatif elles-mêmes.

Pourtant, ce n'est pas si bizarre. Même dans le langage ISO C 90, tous les types ne peuvent pas l'être const. À savoir, les tableaux ne peuvent pas l'être.

Premièrement, la syntaxe n'existe pas pour déclarer un tableau const: int a const [42]est erronée.

Cependant, ce que la déclaration ci-dessus essaie de faire peut être exprimé via un intermédiaire typedef:

typedef int array_t[42];
const array_t a;

Mais cela ne fait pas ce à quoi il ressemble. Dans cette déclaration, ce n'est pas ace qui est constqualifié, mais les éléments! C'est-à-dire que a[0]c'est un const int, mais ac'est juste un "tableau d'int". Par conséquent, cela ne nécessite pas de diagnostic:

int *p = a; /* surprise! */

Cela fait:

a[0] = 1;

Encore une fois, cela souligne l'idée que les références sont en un certain sens «de seconde classe» dans le système de types, comme les tableaux.

Notez comment l'analogie est encore plus profonde, puisque les tableaux ont également un "comportement de conversion invisible", comme les références. Sans que le programmeur n'ait à utiliser d'opérateur explicite, l'identifiant ase transforme automatiquement en int *pointeur, comme si l'expression &a[0]avait été utilisée. C'est analogue à la façon dont une référence ri, lorsque nous l'utilisons comme expression principale, désigne comme par magie l'objet iauquel elle est liée. C'est juste un autre "decay" comme le "array to pointer decay".

Et tout comme nous ne devons pas nous confondre avec la désintégration «tableau vers pointeur» en pensant à tort que «les tableaux ne sont que des pointeurs en C et C ++», nous ne devons pas non plus penser que les références sont juste des alias qui n'ont pas de type propre.

Lorsque decltype(ri)supprime la conversion habituelle de la référence en son objet référent, ce n'est pas si différent de sizeof asupprimer la conversion tableau en pointeur et d'opérer sur le type de tableau lui-même pour calculer sa taille.

Kaz
la source
Vous avez beaucoup d'informations intéressantes et utiles ici (+1), mais c'est plus ou moins limité au fait que "les références sont dans le système de types" - cela ne répond pas entièrement à la question d'OP. Entre cette réponse et la réponse de @ songyuanyao, je dirais qu'il y a plus qu'assez d'informations pour comprendre la situation, mais cela semble être le contexte nécessaire pour une réponse plutôt qu'une réponse complète.
Kyle Strand
1
Aussi, je pense que cette phrase mérite d'être soulignée: "Lorsque vous utilisez ri, la référence est résolue de manière transparente ..." Un point clé (que j'ai mentionné dans les commentaires mais qui jusqu'à présent ne figure dans aucune des réponses ) est que cela decltype n'effectue pas cette résolution transparente (ce n'est pas une fonction, donc rin'est pas «utilisé» dans le sens que vous décrivez). Cela correspond très bien à votre concentration sur le système de type - leur connexion clé est qu'il decltypes'agit d' une opération de type-système .
Kyle Strand
Hmm, le truc sur les tableaux ressemble à une tangente, mais la dernière phrase est utile.
Kyle Strand
..... mais à ce stade, je vous ai déjà voté pour et je ne suis pas l'OP, donc vous ne gagnez ni ne perdez vraiment rien en écoutant mes critiques ....
Kyle Strand
De toute évidence, une réponse facile (et correcte) nous indique simplement la norme, mais j'aime la réflexion de fond ici même si certains pourraient affirmer que certaines parties de celle-ci sont des suppositions. +1.
Steve Kidd le
-5

const X & x »signifie que x alias un objet X, mais vous ne pouvez pas changer cet objet X via x.

Et voir std :: is_const .

Sotter.liu
la source
cela n'essaie même pas de répondre à la question.
bolov