web-dev-qa-db-fra.com

Pourquoi les références ne peuvent-elles pas être replacées en C++?

Les références C++ ont deux propriétés:

  • Ils pointent toujours vers le même objet.
  • Ils ne peuvent pas être 0.

Les pointeurs sont le contraire:

  • Ils peuvent pointer vers différents objets.
  • Ils peuvent être 0.

Pourquoi n’existe-t-il pas de "référence ou pointeur non nullable, réinscriptible" en C++? Je ne vois pas pourquoi les références ne devraient pas être réinscriptibles.

Edit: La question revient souvent parce que j’utilise habituellement des références lorsque je veux être sûr qu’une "association" (j’évite les mots "référence" ou "pointeur" ici) n’est jamais invalide.

Je ne pense pas avoir jamais pensé "bien que cette référence se réfère toujours au même objet". Si les références étaient réinscriptibles, le comportement actuel pourrait toujours se présenter comme suit:

int i = 3;
int& const j = i;

C++ est déjà légal, mais sans signification.

Je reformule ma question comme suit: "Quelle était la raison d'être de la conception" une référence est l'objet "? Pourquoi était-il jugé utile d'avoir des références toujours être le même objet, au lieu de seulement quand il est déclaré comme const? "

Félicitations, Félix

61
TheFogger

La raison pour laquelle C++ ne vous permet pas de relier des références est donnée dans "Conception et évolution du C++" de Stroustrup:

Il n'est pas possible de changer ce à quoi une référence fait référence après l'initialisation. Autrement dit, une fois qu'une référence C++ est initialisée, il ne peut plus être fait référence à un objet différent ultérieurement; il ne peut pas être lié à nouveau. Dans le passé, j'avais déjà été piqué par des références ALGOL68 dans lesquelles r1=r2 pouvait attribuer via r1 à l'objet référencé ou attribuer une nouvelle valeur de référence à r1 (reliure r1) en fonction du type de r2. Je voulais éviter de tels problèmes en C++.

81
Michael Burr

En C++, on dit souvent que "la référence est l'objet". En un sens, c'est vrai: bien que les références soient gérées comme des pointeurs lorsque le code source est compilé, la référence est censée signifier un objet qui n'est pas copié lors de l'appel d'une fonction. Étant donné que les références ne sont pas directement adressables (par exemple, les références n'ont pas d'adresse et renvoie l'adresse de l'objet), il ne serait pas judicieux de les réaffecter. De plus, C++ a déjà des pointeurs, qui gèrent la sémantique de la réinitialisation.

32
rlbond

Parce que vous n’auriez alors aucun type réinscriptible qui ne puisse pas être 0. Sauf si, vous avez inclus 3 types de références/pointeurs. Ce qui ne ferait que compliquer le langage pour un gain minime (Et pourquoi ne pas ajouter le 4ème type également? Référence non réinscriptible pouvant être 0?)

Une meilleure question est peut-être: pourquoi voudriez-vous que les références soient réinscriptibles? S'ils l'étaient, cela les rendrait moins utiles dans beaucoup de situations. Cela compliquerait la tâche du compilateur pour l'analyse des alias.

Il semble que la principale raison pour laquelle les références en Java ou en C # soient réinscriptibles est qu’elles font le travail des pointeurs. Ils désignent des objets. Ce ne sont pas des alias pour un objet.

Quel devrait être l'effet de ce qui suit?

int i = 42;
int& j = i;
j = 43;

En C++ aujourd'hui, avec des références non réinscriptibles, c'est simple. j est un alias pour i, et je termine avec la valeur 43.

Si les références avaient été réinscriptibles, la troisième ligne lierait la référence j à une valeur différente. Ce ne serait plus alias i, mais le littéral entier 43 (qui n'est pas valide, bien sûr). Ou peut-être un exemple plus simple (ou au moins syntaxiquement valide):

int i = 42;
int k = 43;
int& j = i;
j = k;

Avec des références réinscriptibles. j indiquerait k après avoir évalué ce code. Avec les références non réinscriptibles de C++, j pointe toujours vers i et la valeur 43 est attribuée à i.

Rendre les références résérables change la sémantique de la langue. La référence ne peut plus être un alias pour une autre variable. Au lieu de cela, il devient un type de valeur séparé, avec son propre opérateur d'affectation. Et l’un des usages les plus courants des références serait impossible. Et rien ne serait gagné en échange. La fonctionnalité nouvellement acquise pour les références existait déjà sous la forme de pointeurs. Nous aurions donc maintenant deux moyens de faire la même chose, et aucun moyen de faire ce que font les références dans le langage C++ actuel.

17
jalf

Une référence pouvant être replacée serait fonctionnellement identique à un pointeur.

En ce qui concerne la nullabilité: vous ne pouvez pas garantir qu'une telle "référence réinscriptible" soit non-NULL au moment de la compilation, de sorte qu'un tel test doit avoir lieu au moment de l'exécution. Vous pouvez y parvenir vous-même en écrivant un modèle de classe de type pointeur intelligent qui lève une exception lors de l'initialisation ou de l'attribution de la valeur NULL:

struct null_pointer_exception { ... };

template<typename T>
struct non_null_pointer {
    // No default ctor as it could only sensibly produce a NULL pointer
    non_null_pointer(T* p) : _p(p) { die_if_null(); }
    non_null_pointer(non_null_pointer const& nnp) : _p(nnp._p) {}
    non_null_pointer& operator=(T* p) { _p = p; die_if_null(); }
    non_null_pointer& operator=(non_null_pointer const& nnp) { _p = nnp._p; }

    T& operator*() { return *_p; }
    T const& operator*() const { return *_p; }
    T* operator->() { return _p; }

    // Allow implicit conversion to T* for convenience
    operator T*() const { return _p; }

    // You also need to implement operators for +, -, +=, -=, ++, --

private:
    T* _p;
    void die_if_null() const {
        if (!_p) { throw null_pointer_exception(); }
    }
};

Cela peut être utile à l'occasion - une fonction utilisant un paramètre non_null_pointer<int> communique certainement plus d'informations à l'appelant qu'une fonction utilisant int*.

5
j_random_hacker

Une référence n'est pas un pointeur, elle peut être implémentée en tant que pointeur en arrière-plan, mais son concept de base n'est pas équivalent à un pointeur. Une référence doit être regardée comme elle *is* l'objet auquel elle fait référence. Par conséquent, vous ne pouvez pas le changer et il ne peut pas être NULL. 

Un pointeur est simplement une variable contenant une adresse mémoire. Le pointeur possède lui-même une adresse de mémoire et à l'intérieur de cette adresse de mémoire, il contient une autre adresse de mémoire qu'il est censé désigner. Une référence n'est pas la même, elle n'a pas d'adresse propre et par conséquent, elle ne peut pas être modifiée pour "conserver" une autre adresse.

Je pense que le parashift C++ FAQ sur les références le dit mieux:

Remarque importante: Même si une référence Est souvent implémentée à l'aide de Une adresse dans le langage sous-jacent de , Veuillez ne pas penser à une référence Comme à une drôle de tête. pointeur sur un objet. Une référence est l'objet . Ce n'est pas un pointeur sur l'objet , Ni une copie de l'objet. C'est L'objet.

et encore dans FAQ 8.5 :

Contrairement à un pointeur, une fois qu'une référence est Liée à un objet, elle ne peut pas être "Replacée" vers un autre objet. La référence Elle-même n'est pas un objet (elle n'a pas d'identité. En prenant l'adresse de Une référence vous donne l'adresse de Le référent; rappelez-vous: la référence est son référent).

5
Brian R. Bondy

Il aurait probablement été moins déroutant de nommer "alias" les références C++. Comme d’autres l’ont mentionné, les références en C++ devraient être comme la variable ils se réfèrent, pas comme un pointeur /référence à la variable. En tant que tel, je ne peux pas penser à une bonne raison pour qu'ils devrait être réinitialisable.

lorsqu'il s'agit de pointeurs, il est souvent logique d'autoriser la valeur null (sinon, vous voudrez probablement une référence). Si vous souhaitez spécifiquement interdire le maintien de la valeur null, vous pouvez toujours coder votre propre type de pointeur intelligent;)

4
snemarch

Curieusement, beaucoup de réponses ici sont un peu floues ou même opposées (par exemple, ce n’est pas parce que les références ne peuvent pas être nulles ou similaires, en fait, vous pouvez facilement construire un exemple où une référence est zéro).

La vraie raison pour laquelle il est impossible de rétablir une référence est plutôt simple.

  • Les pointeurs vous permettent de faire deux choses: modifier la valeur derrière le pointeur (via l'opérateur -> ou *) et modifier le pointeur lui-même (attribution directe =). Exemple:

    int a;
    int * p = &a;
    1. La modification de la valeur nécessite le déréférencement: *p = 42;
    2. Changer le pointeur: p = 0;
  • Les références vous permettent uniquement de modifier la valeur. Pourquoi? Puisqu'il n'y a pas d'autre syntaxe pour exprimer le re-set. Exemple:

    int a = 10;
    int b = 20;
    int & r = a;
    r = b; // re-set r to b, or set a to 20?

En d'autres termes, il serait ambigu si vous pouviez redéfinir une référence. C'est encore plus logique en passant par référence:

void foo(int & r)
{
    int b = 20;
    r = b; // re-set r to a? or set a to 20?
}
void main()
{
    int a = 10;
    foo(a);
}

J'espère que cela pourra aider :-)

3
dhaumann

Les références C++ peuvent parfois être forcées à être 0 avec certains compilateurs (c'est une mauvaise idée de le faire * et cela enfreint le standard *).

int &x = *((int*)0); // Illegal but some compilers accept it

EDIT: Selon diverses personnes connaissant la norme beaucoup mieux que moi, le code ci-dessus produit un "comportement non défini". Dans au moins certaines versions de GCC et de Visual Studio, j'ai vu ceci faire ce qui était attendu: l'équivalent de définir un pointeur sur NULL (et provoque une exception de pointeur NULL lors de l'accès).

2
Mr Fooz

Vous ne pouvez pas faire ça:

int theInt = 0;
int& refToTheInt = theInt;

int otherInt = 42;
refToTheInt = otherInt;

... pour la même raison pour laquelle secondInt et firstInt n'ont pas la même valeur ici:

int firstInt = 1;
int secondInt = 2;
secondInt = firstInt;
firstInt = 3;

assert( firstInt != secondInt );
1
John Dibling

Ce n'est pas réellement une réponse, mais une solution de contournement pour cette limitation.

En gros, lorsque vous essayez de "relier" une référence, vous essayez en fait d'utiliser le même nom pour faire référence à une nouvelle valeur dans le contexte suivant. En C++, cela peut être réalisé en introduisant une portée de bloc.

Dans l'exemple de jalf

int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
j = k;

si vous voulez changer, écrivez comme ci-dessus. Toutefois, si vous souhaitez modifier la signification de j en k, vous pouvez procéder comme suit:

int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
//change j!
{
    int& j = k;
    //do what ever with j's new meaning
}
1
Earth Engine

Le fait que les références en C++ ne puissent pas être annulées est un effet secondaire du fait qu’elles ne sont qu’un alias. 

0
hasen

Je me suis toujours demandé pourquoi ils n'avaient pas fait d'opérateur d'affectation de référence (par exemple: =) pour cela.

Juste pour énerver quelqu'un, j'ai écrit du code pour changer la cible d'une référence dans une structure.

Non, je ne recommande pas de répéter mon tour. Il cassera s'il est porté sur une architecture suffisamment différente.

0
Joshua

Il existe une solution de contournement si vous voulez une variable membre qui soit une référence et que vous voulez pouvoir la relier. Bien que je le trouve utile et fiable, notez qu'il utilise certaines hypothèses (très faibles) sur la disposition de la mémoire. C'est à vous de décider si cela correspond à vos normes de codage.

#include <iostream>

struct Field_a_t
{
    int& a_;
    Field_a_t(int& a)
        : a_(a) {}
    Field_a_t& operator=(int& a)
    {
        // a_.~int(); // do this if you have a non-trivial destructor
        new(this)Field_a_t(a);
    }
};

struct MyType : Field_a_t
{
    char c_;
    MyType(int& a, char c)
        : Field_a_t(a)
        , c_(c) {}
};

int main()
{
    int i = 1;
    int j = 2;
    MyType x(i, 'x');
    std::cout << x.a_;
    x.a_ = 3;
    std::cout << i;
    ((Field_a_t&)x) = j;
    std::cout << x.a_;
    x.a_ = 4;
    std::cout << j;
}

Ce n'est pas très efficace, car vous avez besoin d'un type distinct pour chaque champ de référence réaffectable et en faire des classes de base; De plus, il existe une hypothèse faible selon laquelle une classe ayant un seul type de référence n'aura pas de champ __vfptr ni aucun autre champ lié à type_id susceptible de détruire les liaisons d'exécution de MyType. Tous les compilateurs que je connais remplissent cette condition (et cela n'aurait aucun sens de ne pas le faire).

0
lorro

Je suis d’accord avec la réponse acceptée. Mais par constance, ils se comportent cependant beaucoup comme des indicateurs.

struct A{
    int y;
    int& x;
     A():y(0),x(y){}
};

int main(){
  A a;
  const A& ar=a;
  ar.x++;
}

travaux. Voir 

Motifs de conception du comportement des membres de référence des classes passées par const référence

0
Fabio Dalla Libera

Être à moitié sérieux: IMHO pour les rendre un peu plus différents des pointeurs;) Vous savez que vous pouvez écrire:

MyClass & c = *new MyClass();

Si vous pouviez aussi écrire plus tard:

c = *new MyClass("other")

serait-il judicieux d'avoir des références avec des pointeurs?

MyClass * a =  new MyClass();
MyClass & b = *new MyClass();
a =  new MyClass("other");
b = *new MyClass("another");
0
anon

J'imagine que c'est lié à l'optimisation. 

L'optimisation statique est beaucoup plus facile lorsque vous pouvez savoir sans ambiguïté le bit de mémoire qu'une variable signifie. Les pointeurs rompent cette condition et la référence pouvant être réinitialisée le serait également.

0
dmckee

Parce que parfois les choses ne doivent pas être re-pointables. (Par exemple, la référence à un Singleton.)

Parce que c'est bien dans une fonction de savoir que votre argument ne peut pas être nul.

Mais surtout, parce que cela permet d’utiliser quelque chose qui est vraiment un pointeur, mais qui agit comme un objet de valeur local. C++ s'efforce, pour citer Stroustrup, de faire en sorte que les instances de classe "fassent comme les ints d". Passer un int par valeur n'est pas cher, car un int s'inscrit dans un registre de machine. Les classes sont souvent plus grandes que les ints et leur passage par valeur entraîne une surcharge considérable. 

Le fait de pouvoir passer un pointeur (qui a souvent la taille d'un entier, ou peut-être de deux pouces) qui "ressemble à" un objet valeur permet d'écrire un code plus propre, sans le "détail d'implémentation" des déréférences. Et, avec la surcharge des opérateurs, cela nous permet d’écrire une syntaxe similaire à la syntaxe utilisée par les classes. En particulier, cela nous permet d’écrire des classes de modèles avec une syntaxe qui peut s’appliquer de la même manière aux primitives, comme les ints, et aux classes (comme les classes de nombres complexes).

Et, avec la surcharge des opérateurs en particulier, il y a des endroits où nous devrions retourner un objet, mais encore une fois, il est beaucoup moins coûteux de retourner un pointeur. Encore une fois, renvoyer une référence est notre "out".

Et les pointeurs sont difficiles. Pas pour vous, peut-être, et pour personne qui réalise qu'un pointeur n'est que la valeur d'une adresse mémoire. Mais rappelant ma classe de CS 101, ils ont fait trébucher un certain nombre d'étudiants. 

char* p = s; *p = *s; *p++ = *s++; i = ++*p;

peut être déroutant.

Heck, après 40 ans de C, les gens ne peuvent toujours pas accepter si une déclaration de pointeur devrait être:

char* p;

ou

char *p;
0
tpdi