OVH Cloud OVH Cloud

[newbie] problème débile

64 réponses
Avatar
Azuriel
Est-ce possible de déclarer une propriété de même classe que celle qui
la possède ?

class A
{
A a;
};

ne compile pas.

10 réponses

3 4 5 6 7
Avatar
Sylvain
Manuel Zaccaria wrote on 17/10/2006 14:02:

Est ce que *first a un comportement defini si first vaut NULL ?
quelle importance puisqu'il N'est PAS déréférencé ...





Bien sur que si... petit rappel:


bien sur que non...

class A {
A& a;
A(A another) : a(another) { ... }


ici ce doit être A(A&) (indiqué le 28/09 à 22:11)

/*virtual*/ void execute() { ... }

void crash() {
a.execute();
}


personne n'a indiqué qu'un "vrai" déréférencement ne plantera pas.
c'est même une évidence et donc qui s'autorisera à avoir des références
nulles devra les tester, or "tester cette valeur ne conduira pas à un
code très clean mais bon ..." (indiqué le 28/09 à 20:49)

Ceci dit, je ne vois aucun intérêt à initialiser une référence
avec un pointeur nul. Comme il a déjà été dit et répété, c'est
un comportement indéfini.


si tu savais comme je m'en cogne de ces indéfinitions là !!
pour qui écrit un sample jamais publié (surtout pas utilisé) c'est
surement d'une haute importance; quand on est derrière un vrai compilo
pour du vrai code, le comportement est tout à fait complètement défini.

et pour résumer les 2 pts: je me cogne de savoir ce qu'un compilo que je
n'utiliserais jamais estime indéfini.

J'ajoute que ce serait contre-productif
(une pessimisation). Il devient tout-à-coup nécessaire de tester
dans les fonctions membres l'adresse de la référence à coup de :


et pourquoi donc "nécessaire" ??? si (dans l'esprit des réponses données
au PO) il s'agit d'un noeud d'un btree, je sais surement par une autre
donnée membre qui est le noeud principal, je sais qu'il n'a pas de père
et n'aurait aucune "nécessité" à déréferencer ce pointeur (!) que je
sais null.

AMHA, le but des références est justement de GARANTIR que l'objet
existe (this est toujours != 0).


non le premier but est de fluidifier l'écriture, accessoirement de
forcer l'initialisation avec une valeur non nulle.

j'ai dit "forcer", je ne dis pas empêcher; certain évoquent des compilos
qui se rendrait compte de cela, je préférerais pour ma part les compilos
qui n'introduisent pas du soi-disant code de contrôle totalement inutile
et couteux.

quant à "garantir que l'object existe" ... dis moi que "l'erreur"
ci-après est détectée par ton compilo et on parlera de l'argument.

int* p = new int[1];
int& r = *p;
delete [] p;
r;

Sinon on utilise un pointeur, pas une référence. Faut pas mélanger
des concepts qui visent des buts différents.


tu peux nous parler un peu plus de ces "buts" ?
pour moi (et pour le compilo quand il l'utilise) une référence est un
pointeur.

Quand on se bat contre le compilateur, on est toujours perdant!


?!??

Mais quel est l'intérêt de faire ça ? Je n'en vois aucun.


ne pas le voir n'implique pas que cela puisse exister.

Mes 2 centimes suisses,


je croyais la Suisse plus riche ;)

Sylvain.





Avatar
Falk Tannhäuser
Sylvain schrieb:
qui s'autorisera à avoir des références nulles devra les tester


On les teste comment, sachant que le compilateur a le droit d'éliminer
le test ? De plus, rien ne garantit que la référence reste nulle lorsque
l'héritage entre en jeu ! Question à 0,02 € : Qu'affiche-t-il le
programme suivant :
_______________________________________
#include <ostream>
#include <iostream>

struct Base
{
int i;
};

struct Derived : public Base
{
virtual ~Derived() {}
};

void toto(Base& b)
{
if(&b == 0)
std::cout << "Null referencen";
else
std::cout << b.i << 'n';
}

void titi(Derived& d)
{
toto(d);
}

int main()
{
Derived* pd = 0;
titi(*pd);
return 0;
}
_______________________________________

Réponse : Chez moi cela donne
Frtzragngvba snhyg (pber qhzcrq)

Falk

Avatar
Michel Decima
Sylvain schrieb:
qui s'autorisera à avoir des références nulles devra les tester


On les teste comment, sachant que le compilateur a le droit d'éliminer
le test ? De plus, rien ne garantit que la référence reste nulle lorsque
l'héritage entre en jeu ! Question à 0,02 € : Qu'affiche-t-il le
programme suivant :


Interessant. J'ai teste sur trois environnements distincts, et obtenu
trois resultats differents:

g++-4.0.3/linux-2.6: Segmentation fault
g++-4.1.0/AIX-5.2 : 0
xlC-6/AIX-5.2 : Null reference


Avatar
kanze
wrote:

wrote:

S'il y a déréférencement dans ce cas-là, il y aura crash à
l'exécution. Je n'ai encore rencontré aucun compilateur qui
sacrifie la rapidité du code (génère une instruction de plus)
dans le but de provoquer un crash.


Il te manque de l'expérience, alors, parce que la plupart des
compilateurs aujourd'hui ont des options pour insérer du code de
vérification dans certains cas. Et ce n'est pas une nouveauté.


Vérification, oui. Crash intentionnel, non.


C'est l'équivalent d'un assert. Avec g++, j'ai bien un core dump
sous Unix ; avec VC++, j'ai le pop-up d'une erreur sous
Windows, avec le choix d'aborter complétement, ou de continuer
sous le debuggeur.

Et moi, je peux te dire que je me suis déjà servi d'un
compilateur où il ne marchait pas -- le compilateur
généré systèmatiquement des tests pour un pointeur null
chaque fois qu'on se servait de l'opérateur * unaire.


Si tu peux te souvenir du nom de ce compilateur,
j'aimerais vraiment étudier la manière dont il génère ce
genre de test.


Le compilateur, c'était le compilateur de Green Hills. Je ne
sais pas ce qui lui en est devenu aujourd'hui.


Merci. Je vais effecteur quelques recherches sur ce
compilateur.


Il se vendait encore il y a peu. Je ne sais pas si l'option pour
générer les vérifications y est toujours, mais j'imagine que si.
En revanche, dans le mesure qu'il n'ont plus leur propre
front-end pour le C++, et que le C aujourd'hui permet
explicitement l'expression &*p, où p est un pointeur nul, c'est
peu probable que la vérification se fait encore lors de
l'initialisation d'une référence (qui doit bien apparaître au
back-end comme un &*p).

Avec la plupart des compilateurs, tu risque d'avoir des
problèmes si tu fais des choses du genre :

Derived* pd = NULL ;
Base& r = *pd ;
Base* pb = &r ;
if ( pb == NULL ) ...

La conversion d'un Derived* en Base* peut exiger en fait une
changement de l'adresse. Qui ne doit pas avoir lieu si le
pointeur est null. Normalement, donc, le compilateur génère du
code pour tester ce cas. S'il voit que le pointeur est
déréférencé, pour initialiser une référence, il sait qu'il n'est
pas nul, et peut donc supprimer le test. Avec comme résultat que
le pointeur n'est plus nul.

Il y a un autre compilateur dont on me dit qu'il faisait pareil.
J'ai un trou de mémoire en ce qui concerne son nom à l'instant,
mais à l'époque, il était assez connu pour tester tout. (Il se
vendait comme compilateur de « contrôle » ; son but n'était
pas de compiler l'exécutable qu'on livrait, mais de compiler
avec un maximum de vérifications, de façon à ce qu'on détecte un
maximum d'erreurs.)



Ça m'est revenu, le nom du compilateur. C'est Center Line (ou
quelque chose du genre). Et il existe encore (bien que ça fait
des années que je n'en ai plus entendu parler) :
http://www.ics.com/products/centerline/index.html. Parmi les
features : « Locates incorrect pointer values, illegal array
indices, bad function arguments, type mismatches, and
uninitialized variables ».

À l'époque où j'entendais parler (début des années 1990s), on me
disait que les temps d'exécution était deux ou trois fois plus
lent, parfois plus, qu'avec le compilateur Sun. Le marché visé
était uniquement des builds de déboggage. Avec l'amélioration de
l'analyse statique (pour pouvoir supprimer les tests qu'on sait
n'échouera pas) et l'augmentation de la vitesse des machines, ça
ne m'étonnerait pas qu'il puisse servir à la production.

Aussi à l'époque, et d'après ce qu'on m'a dit (je n'ai jamais pu
m'en servir), la taille d'un pointeur était trois fois plus
grand que sous Sun CC ; physiquement, le pointeur C/C++
contenait trois pointeurs hardware : la valeur courante, et les
deux bornes. (La norme C a été soigneusement formulée pour
permettre, voire même encourager une telle implémentation.)

En effet, pour que ce test soit valide pour n'importe quelles
applications, le pointeur doit se trouver sur la pile. Sinon
(le pointeur est lui-même un objet pointé se trouvant sur le
tas), l'ensemble de cette opération(test + déréférencement)
est non atomique (pas de test&set pour les déréférencements)
et le compilateur doit normalement poser un lock, sauf si le
compilateur ne génère que du code monothreadé.


Tu te moques de nous ou quoi ? Si un autre thread peut modifier
le pointeur, l'utilisateur a déjà un lock. Sinon, il a un
comportement indéfini, qui risque réelement de lui causer des
ennuis. (N'oublie pas que la lecture d'un pointeur n'est pas
forcément atomique. Au moins, pas sur un Intel.)


L'utilisateur a déjà un lock ?


Il en a intérêt. Tu ne peux pas modifier un objet en mémoire
dans un thread sans que tous les threads qui y accèdent
protègent leurs accès.

Qui l'y oblige ?


Posix, pour un. La façon que travaille l'hardware moderne, pour
l'autre.

Heureusement que ce n'est pas obligatoire notamment dans le
domaine de développement de certains drivers et applications
real-time.


Certes, il existe d'autres solutions. Mais pas en C++. (Pas
encore, en tout cas -- Microsoft a annoncé qu'il vont définir
la signification de volatile pour qu'il marche. Mais ce n'est
pas le cas dans VC++ 8, au moins d'après les code généré.)

Par contre le compilateur se doit d'avoir un comportement
consistant : le compilateur doit mettre un lock, même s'il est
peu probable qu'une application s'amuse à changer un pointeur
NULL en un pointeur valide dans une autre thread (par contre
changer un pointeur valide en un autre pointeur valide sans
lock est un cas pratique).


Pratique ou non, il ne marche pas. Il risque de donner les
apparences de marcher sur un ancien processeur, avec un
compilateur primitif, mais au plus tard, dès que tu as un
multi-core, ou un bon optimisateur, tu risques d'avoir des
problèmes. Même dans le cas le plus favorable, tu risques
fortement des surprises. Considère :

thread 1 :
p = new MyClass ;

thread 2 :
p->someMember ;

Le compilateur risque de réordonner les écritures de façon à ce
que l'écriture physique de p en thread 1 précède l'écriture des
variables membre dans le constructeur de MyClass ; thread 2
pourrait donc accéder à une variable non initialisée. Tous les
compilateurs que je connais réordonne les écritures dans
certains cas -- je me rappelle l'avoir vu dans MS C, version
1.0. C'est une partie essentielle de l'optimisation. Avec les
compilateurs courants (VC++ pré version 7, et peut-être
pré-version 8, par exemple), il suffisait de mettre le
constructeur dans une source différente que son appel pour
inhiber l'optimisation, mais VC++ 8, comme la plupart des
compilateurs modernes, a des options pour optimiser au delà des
frontières d'une unité de compilation.

Et évidemment, même si le compilateur n'optimise rien, dans un
processeur multi-core, au moins de prendre des précautions
précises (instructions membar sur Sparc, fence sur AMD, et
peut-être Intel, etc.), il n'y a aucune garantie que une
CPU/core voire les écritures dans le même ordre qu'ils ont été
émises par l'autre.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
kanze
Sylvain wrote:
Manuel Zaccaria wrote on 17/10/2006 14:02:


[...]
Ceci dit, je ne vois aucun intérêt à initialiser une référence
avec un pointeur nul. Comme il a déjà été dit et répété, c'est
un comportement indéfini.


si tu savais comme je m'en cogne de ces indéfinitions là !!


Tiens, tiens, tiens. On réjoigne une accusation que j'ai fait
dans la passée. Tu t'en fous que ton code soit correct, ou qu'il
est sûr de marcher. Pourvu qu'il ne crashe pas pendant la démo,
c'est ça.

pour qui écrit un sample jamais publié (surtout pas utilisé)
c'est surement d'une haute importance; quand on est derrière
un vrai compilo pour du vrai code, le comportement est tout à
fait complètement défini.


Avec le compilateur CenterLine, oui. Il est garanti de provoquer
un message d'erreur. Avec les autres compilateurs, je ne sais
pas. Je ne vois rien de garantie dans la documentation de Sun
CC, ni dans celle de g++. (Je n'ai rien vu dans la documentation
de VC++ non plus, mais je ne l'ai pas tout lue.)

et pour résumer les 2 pts: je me cogne de savoir ce qu'un
compilo que je n'utiliserais jamais estime indéfini.


Donc, tu n'utiliseras jamais Sun CC, ni g++. Ni, probablement
VC++, parce que jusqu'à preuve du contraire, il l'estime
indéfini aussi.

[...]
Sinon on utilise un pointeur, pas une référence. Faut pas
mélanger des concepts qui visent des buts différents.


tu peux nous parler un peu plus de ces "buts" ?


C'est bien ce que dit la plupart de la littérature C++ depuis au
moins 15 ans.

pour moi (et pour le compilo quand il l'utilise) une référence
est un pointeur.


Pour toi, peut-être. Pas selon la définition du langage, en
revanche, et pas dans l'esprit des auteurs des compilateurs.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Manuel Zaccaria
Sylvain a écrit:
Manuel Zaccaria wrote on 17/10/2006 14:02:

Est ce que *first a un comportement defini si first vaut NULL ?
quelle importance puisqu'il N'est PAS déréférencé ...





Bien sur que si... petit rappel:


bien sur que non...

class A {
A& a;
A(A another) : a(another) { ... }


ici ce doit être A(A&) (indiqué le 28/09 à 22:11)


Désolé... je voulais écrire A(A const &)


/*virtual*/ void execute() { ... }

void crash() {
a.execute();
}


personne n'a indiqué qu'un "vrai" déréférencement ne plantera pas.


Il n'y a pas de "vrai" ou de "faux" déréférencement. Il y a ou il n'y a
pas déréférencement. La cuisine interne du compilateur ne nous concerne pas.

c'est même une évidence et donc qui s'autorisera à avoir des références
nulles devra les tester, or "tester cette valeur ne conduira pas à un code
très clean mais bon ..." (indiqué le 28/09 à 20:49)


Mais " A() : a(*this) {} " fait l'affaire. Pourquoi NULL ?

Ceci dit, je ne vois aucun intérêt à initialiser une référence
avec un pointeur nul. Comme il a déjà été dit et répété, c'est
un comportement indéfini.


si tu savais comme je m'en cogne de ces indéfinitions là !!


Je m'en moque! Je répondais à Dieu Tout Puissant.

pour qui écrit un sample jamais publié (surtout pas utilisé) c'est
surement d'une haute importance; quand on est derrière un vrai compilo
pour du vrai code, le comportement est tout à fait complètement défini.


Y'a des faux copilos et du faux code maintenant ? Whouaa, j'apprend
quelque chose tous les jours. Merci Sylvain.

et pour résumer les 2 pts: je me cogne de savoir ce qu'un compilo que je
n'utiliserais jamais estime indéfini.


Tu n'utilises aucun compilateur ? Car c'est, à ma connaîssance, un
comportement indéfini dans tous les compilateurs.
Si ça peut t'aider, remplace mentalement "indéfini" par "n'importe quoi".

J'ajoute que ce serait contre-productif
(une pessimisation). Il devient tout-à-coup nécessaire de tester
dans les fonctions membres l'adresse de la référence à coup de :


et pourquoi donc "nécessaire" ??? si (dans l'esprit des réponses données
au PO) il s'agit d'un noeud d'un btree, je sais surement par une autre
donnée membre qui est le noeud principal, je sais qu'il n'a pas de père et
n'aurait aucune "nécessité" à déréferencer ce pointeur (!) que je sais
null.


Tu parles de pointeur... alors utilise un pointeur.
Si c'est juste pour pouvoir écrire "." au lieu de "->", bof.
Utiliser une référence comme un pointeur, c'est juste de l'obfuscation.

AMHA, le but des références est justement de GARANTIR que l'objet
existe (this est toujours != 0).


non le premier but est de fluidifier l'écriture, accessoirement de forcer
l'initialisation avec une valeur non nulle.


A l'envers.
Le premier but est de forcer l'initialisation avec une valeur non nulle,
accessoirement de fluidifier l'écriture.

j'ai dit "forcer", je ne dis pas empêcher; certain évoquent des compilos
qui se rendrait compte de cela, je préférerais pour ma part les compilos
qui n'introduisent pas du soi-disant code de contrôle totalement inutile
et couteux.

quant à "garantir que l'object existe" ... dis moi que "l'erreur" ci-après
est détectée par ton compilo et on parlera de l'argument.

int* p = new int[1];
int& r = *p;
delete [] p;
r;


Ce code invoque un comportement indéfini aka "n'importe quoi".
Après ça, même un if(p!=0) ou if(&r!=0) est indéfini.

Sinon on utilise un pointeur, pas une référence. Faut pas mélanger
des concepts qui visent des buts différents.


tu peux nous parler un peu plus de ces "buts" ?
pour moi (et pour le compilo quand il l'utilise) une référence est un
pointeur.


Un pointeur peut être nul et on devrais le tester avant de déréférencer.
Un pointeur peut changer de valeur pour pointer ailleurs.
Une référence ne peut pas être nulle, on n'a rien à tester. On l'utilise.
Une référence ne change pas d'identité.
Une référence peut allonger la durée de vie d'un temporaire.
etc.

Mais tout ça, tu le sais déjà.

Quand on se bat contre le compilateur, on est toujours perdant!


?!??


Comme James l'a dit et répété, le compilateur a totalement le droit
d'ignorer et optimiser (lire supprimer) le test "if(&ref != NULL)"
car il "sait" à l'avance que c'est "impossible".

Mais quel est l'intérêt de faire ça ? Je n'en vois aucun.


ne pas le voir n'implique pas que cela puisse exister.


int& r = *(int*)0;

Quelle est l'utilité de r ?

Plus sérieusement, dans le cas du noeud de btree, qu'est-ce qui
empêche d'utiliser this plutôt que NULL. C'est permi par la norme
et on obtient le résultat voulu.

Mes 2 centimes suisses,


je croyais la Suisse plus riche ;)


Oh mais je ne suis pas suisse moi ;)

-
Manuel Zaccaria






Avatar
Gabriel Dos Reis
"kanze" writes:

[...]

| > pour qui écrit un sample jamais publié (surtout pas utilisé)
| > c'est surement d'une haute importance; quand on est derrière
| > un vrai compilo pour du vrai code, le comportement est tout à
| > fait complètement défini.
|
| Avec le compilateur CenterLine, oui. Il est garanti de provoquer
| un message d'erreur. Avec les autres compilateurs, je ne sais
| pas. Je ne vois rien de garantie dans la documentation de Sun
| CC, ni dans celle de g++. (Je n'ai rien vu dans la documentation
| de VC++ non plus, mais je ne l'ai pas tout lue.)

Depuis des années, GCC exploite activement ces genres de
fonctionnement indéfini : si tu déréférences un pointeur, GCC en
déduit qu'il n'est pas nul (tu n'aurais pas dû le faire sinon) ; par
conséquent, l'optimizateur va virer des codes et autres choses sur la
base de cette inférence.

-- Gaby
Avatar
Sylvain
Falk Tannhäuser wrote on 18/10/2006 02:07:
Sylvain schrieb:
qui s'autorisera à avoir des références nulles devra les tester


On les teste comment, sachant que le compilateur a le droit d'éliminer
le test ?


ton compilateur modifie tes séquencements en virant des tests ??
change de compilo !!

De plus, rien ne garantit que la référence reste nulle lorsque
l'héritage entre en jeu !


?!? héritage ou pas la référence reste ce qu'elle est tant qu'une
nouvelle valeur ne lui est pas assignée.

Question à 0,02 € : Qu'affiche-t-il le programme suivant:


avec les compilos que j'utilise ?
la console affiche "Null reference"

struct Base {
int i;
};
struct Derived : public Base {
virtual ~Derived() {}
};


btw, cette écriture est incorrecte et peux justement provoquer des
plantes simplement en faisant:

Base* b = new Derived();
delete b;

la virtualité du destructeur doit être définie sur la classe de base,
non sur une classe enfant.

Réponse : Chez moi cela donne
Frtzragngvba snhyg (pber qhzcrq)


punatr qr pbzcvyb ;)

Sylvain.


Avatar
dieu.tout.puissant
kanze wrote:
wrote:

wrote:

S'il y a déréférencement dans ce cas-là, il y aura crash à
l'exécution. Je n'ai encore rencontré aucun compilateur qui
sacrifie la rapidité du code (génère une instruction de plus)
dans le but de provoquer un crash.


Il te manque de l'expérience, alors, parce que la plupart des
compilateurs aujourd'hui ont des options pour insérer du code de
vérification dans certains cas. Et ce n'est pas une nouveauté.


Vérification, oui. Crash intentionnel, non.


C'est l'équivalent d'un assert. Avec g++, j'ai bien un core dump
sous Unix ; avec VC++, j'ai le pop-up d'une erreur sous
Windows, avec le choix d'aborter complétement, ou de continuer
sous le debuggeur.


Crash intentionnel : sous-entendu intentionnel de la part du
compilateur non de l'utilisateur. Bien entendu, le compilateur peut
mettre à disposition une option "effectuer un déréférencement
effectif lors d'une opération d'initilisation d'une référence dans
le but de provoquer un crash lorsque l'opérande ne désigne pas un
objet existant" que l'utilisateur peut activer, mais lors d'une
compilation non spécialement paramétrée, le compilateur ne prendra
pas, seul, la décision de provoquer ce genre de crash. Ce serait comme
si le compilateur se permettait de mettre implicitement et par défaut
des assert dans une version release du code.

Cela dit, il existe des domaines sensibles ou un crash est beaucoup
moins grave qu'une corruption de données, et peut-être qu'il existe,
pour ces domaines, des compilateurs C++ qui, *indépendamment* de sa
paramétrisation, générera l'équivalent d'assert dans le but de
contrôler certains opérations. Mais cela m'étonnerait grandement que
l'on ne puisse pas générer du code épuré de ces vérifications
(c'est pour cela que je veux voir leur génération de code, si de tels
compilateurs existent). La question de départ est quand même : est-ce
que le code suivant est valable dans le sens pratique, c'est-à-dire
passable sur n'importe quel compilateur :
int *p = 0;
int &r = *p;

"n'importe quel compilateur" sérieux... bien sûr, on pourrait écrire
un compilateur maison qui rejette spécifiquement ce code, en
générant un déréférencement effectif. Mais il n'y a aucun avantage
pratique à provoquer un crash et être plus lent en général pour ces
opérations, plutôt que de laisser passer ce code (qui crashera de
toute façon si on essaie d'utiliser r tel qu'il est défini).


Et moi, je peux te dire que je me suis déjà servi d'un
compilateur où il ne marchait pas -- le compilateur
généré systèmatiquement des tests pour un pointeur null
chaque fois qu'on se servait de l'opérateur * unaire.


Si tu peux te souvenir du nom de ce compilateur,
j'aimerais vraiment étudier la manière dont il génère ce
genre de test.


Le compilateur, c'était le compilateur de Green Hills. Je ne
sais pas ce qui lui en est devenu aujourd'hui.


Merci. Je vais effecteur quelques recherches sur ce
compilateur.


Il se vendait encore il y a peu. Je ne sais pas si l'option pour
générer les vérifications y est toujours, mais j'imagine que si.
En revanche, dans le mesure qu'il n'ont plus leur propre
front-end pour le C++, et que le C aujourd'hui permet
explicitement l'expression &*p, où p est un pointeur nul, c'est
peu probable que la vérification se fait encore lors de
l'initialisation d'une référence (qui doit bien apparaître au
back-end comme un &*p).


J'ai effectué quelques recherches, et bien que ce compilateur soit
bourré d'options et fonctionnalités, je ne vois pas le comportement
qui nous interesse (mais je n'ai pas eu accès à toute la spec). Au
fait, es-tu sûr que le compilateur ait généré un véritable code de
vérification. Apparemment, le domaine de ce compilateur est surtout
celui des systèmes embarqués, et la vérification et prise en charge
d'un déréférencement d'un pointeur null pourrait très bien se
situer à un autre endroit (système d'exploitation, machine virtuelle,
...).



Avec la plupart des compilateurs, tu risque d'avoir des
problèmes si tu fais des choses du genre :

Derived* pd = NULL ;
Base& r = *pd ;
Base* pb = &r ;
if ( pb == NULL ) ...

La conversion d'un Derived* en Base* peut exiger en fait une
changement de l'adresse. Qui ne doit pas avoir lieu si le
pointeur est null. Normalement, donc, le compilateur génère du
code pour tester ce cas. S'il voit que le pointeur est
déréférencé, pour initialiser une référence, il sait qu'il n' est
pas nul, et peut donc supprimer le test. Avec comme résultat que
le pointeur n'est plus nul.


Oui, ce genre de pointeurs traités ne sont pas considérés des
pointeurs bruts, tout comme un pointeur vers une donnée ou fonction
membre d'une classe n'est pas un vrai pointeur (plutôt un offset).
D'où un traitement spécial.



Il y a un autre compilateur dont on me dit qu'il faisait pareil.
J'ai un trou de mémoire en ce qui concerne son nom à l'instant,
mais à l'époque, il était assez connu pour tester tout. (Il se
vendait comme compilateur de « contrôle » ; son but n'était
pas de compiler l'exécutable qu'on livrait, mais de compiler
avec un maximum de vérifications, de façon à ce qu'on détecte un
maximum d'erreurs.)



Ça m'est revenu, le nom du compilateur. C'est Center Line (ou
quelque chose du genre). Et il existe encore (bien que ça fait
des années que je n'en ai plus entendu parler) :
http://www.ics.com/products/centerline/index.html. Parmi les
features : « Locates incorrect pointer values, illegal array
indices, bad function arguments, type mismatches, and
uninitialized variables ».


Je ne connais pas non plus, je vais donc effectuer de nouveau quelques
recherches.


À l'époque où j'entendais parler (début des années 1990s), on me
disait que les temps d'exécution était deux ou trois fois plus
lent, parfois plus, qu'avec le compilateur Sun. Le marché visé
était uniquement des builds de déboggage. Avec l'amélioration de
l'analyse statique (pour pouvoir supprimer les tests qu'on sait
n'échouera pas) et l'augmentation de la vitesse des machines, ça
ne m'étonnerait pas qu'il puisse servir à la production.

Aussi à l'époque, et d'après ce qu'on m'a dit (je n'ai jamais pu
m'en servir), la taille d'un pointeur était trois fois plus
grand que sous Sun CC ; physiquement, le pointeur C/C++
contenait trois pointeurs hardware : la valeur courante, et les
deux bornes. (La norme C a été soigneusement formulée pour
permettre, voire même encourager une telle implémentation.)

En effet, pour que ce test soit valide pour n'importe quelles
applications, le pointeur doit se trouver sur la pile. Sinon
(le pointeur est lui-même un objet pointé se trouvant sur le
tas), l'ensemble de cette opération(test + déréférencement)
est non atomique (pas de test&set pour les déréférencements)
et le compilateur doit normalement poser un lock, sauf si le
compilateur ne génère que du code monothreadé.


Tu te moques de nous ou quoi ? Si un autre thread peut modifier
le pointeur, l'utilisateur a déjà un lock. Sinon, il a un
comportement indéfini, qui risque réelement de lui causer des
ennuis. (N'oublie pas que la lecture d'un pointeur n'est pas
forcément atomique. Au moins, pas sur un Intel.)


L'utilisateur a déjà un lock ?


Il en a intérêt. Tu ne peux pas modifier un objet en mémoire
dans un thread sans que tous les threads qui y accèdent
protègent leurs accès.


Si, tu peux le modifier, mais il vaut mieux que l'objet ne représente
qu'un mot mémoire pour que l'opération soit atomique. Il est bien
évidemment important que tout type d'opération atomique puisse être
utilisée sans lock, car certaines sont nécessaires à la
conception-même d'un lock (ex:test&set).


Qui l'y oblige ?


Posix, pour un. La façon que travaille l'hardware moderne, pour
l'autre.


Il me semble que ni l'un ni l'autre ne l'y *oblige*.
Mais, bien sûr, l'utilisateur doit vraiment savoir ce qu'il fait.


Heureusement que ce n'est pas obligatoire notamment dans le
domaine de développement de certains drivers et applications
real-time.


Certes, il existe d'autres solutions. Mais pas en C++. (Pas
encore, en tout cas -- Microsoft a annoncé qu'il vont définir
la signification de volatile pour qu'il marche. Mais ce n'est
pas le cas dans VC++ 8, au moins d'après les code généré.)


Si.
D'après le code généré (et même optimisé) par VC++ 8 :

#include <iostream>
void main() {
int x = 0;
for (int i=0; i<9; i++) x++;
std::cout << x; // pour une génèration de code dans tous les cas
}

Code généré:

00401000 mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401006 push 9 ; optimisation !
00401008 call dword ptr
[__imp_std::basic_ostream<char,std::char_traits<char>
::operator<< (402038h)]



-------------------------------------
On ajoute simplement volatile à la déclaration de x:

#include <iostream>
void main() {
volatile int x = 0;
for (int i=0; i<9; i++) x++;
std::cout << x; // pour une génèration de code dans tous les cas
}

Code généré:

00401008 mov eax,9
0040100D mov ecx,1
for (int i=0; i<9; i++) x++;
00401012 add dword ptr [esp],ecx ; écriture de x à chaque
tour de boucle
00401015 sub eax,ecx
00401017 jne main+12h (401012h)
std::cout << x;
00401019 mov eax,dword ptr [esp]
0040101C mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401022 push eax
00401023 call dword ptr
[__imp_std::basic_ostream<char,std::char_traits<char> >::operator<<
(402038h)]




Par contre le compilateur se doit d'avoir un comportement
consistant : le compilateur doit mettre un lock, même s'il est
peu probable qu'une application s'amuse à changer un pointeur
NULL en un pointeur valide dans une autre thread (par contre
changer un pointeur valide en un autre pointeur valide sans
lock est un cas pratique).


Pratique ou non, il ne marche pas. Il risque de donner les
apparences de marcher sur un ancien processeur, avec un
compilateur primitif, mais au plus tard, dès que tu as un
multi-core, ou un bon optimisateur, tu risques d'avoir des
problèmes. Même dans le cas le plus favorable, tu risques
fortement des surprises. Considère :

thread 1 :
p = new MyClass ;

thread 2 :
p->someMember ;

Le compilateur risque de réordonner les écritures de façon à ce
que l'écriture physique de p en thread 1 précède l'écriture des
variables membre dans le constructeur de MyClass ; thread 2
pourrait donc accéder à une variable non initialisée. Tous les
compilateurs que je connais réordonne les écritures dans
certains cas -- je me rappelle l'avoir vu dans MS C, version
1.0. C'est une partie essentielle de l'optimisation. Avec les
compilateurs courants (VC++ pré version 7, et peut-être
pré-version 8, par exemple), il suffisait de mettre le
constructeur dans une source différente que son appel pour
inhiber l'optimisation, mais VC++ 8, comme la plupart des
compilateurs modernes, a des options pour optimiser au delà des
frontières d'une unité de compilation.

Et évidemment, même si le compilateur n'optimise rien, dans un
processeur multi-core, au moins de prendre des précautions
précises (instructions membar sur Sparc, fence sur AMD, et
peut-être Intel, etc.), il n'y a aucune garantie que une
CPU/core voire les écritures dans le même ordre qu'ils ont été
émises par l'autre.


Oui, de toute façon, l'ordre des opérations dans un contexte
multithreadé n'est jamais garanti sans mécanisme de synchronisation.
Mais bien sûr il n'est pas indispensable pour certaines applications.
Exemple :

- Une thread principale qui récupère très fréquemment la valeur
d'une variable nommée "eval" (ne représentant qu'un mot mémoire).
- n unités de calculs (threads) qui donnent régulièrement le
résultat de leurs évaluations(algo différent) du même problème
dans cette variable eval. Ces threads communiquent également entre
elles.
- Les évaluations données tenderont vers un meilleur résultat dans
le temps., mais cette amélioration n'est pas forcément totalement
croissante ou linéaire. C'est une tendance.
- L'important est que la thread principale puisse récupérer
immédiatement le résultat d'une évaluation (ce résultat est ensuite
utilisé dans d'autres calculs). D'ailleurs, statistiquement, la
thread principale récupère fréquemment la même valeur
d'évaluation.

L'ordre ici n'est pas important, et l'accès intensif de la variable
eval prohibe l'utilisation d'un lock, qui n'est de toute facon pas
nécessaire (puisque lecture/écriture d'un seul mot mémoire).





Avatar
Sylvain
Manuel Zaccaria wrote on 18/10/2006 15:22:

personne n'a indiqué qu'un "vrai" déréférencement ne plantera pas.


Il n'y a pas de "vrai" ou de "faux" déréférencement. Il y a ou il n'y a
pas déréférencement. La cuisine interne du compilateur ne nous concerne pas.


tout à fait! il y a des copies de pointeurs et des déréférencements;
j'utilisais "faux" pour ne pas froisser votre ardeur à voir le second
quand il ne s'agit que du premier.

c'est même une évidence et donc qui s'autorisera à avoir des références
nulles devra les tester, or "tester cette valeur ne conduira pas à un code
très clean mais bon ..." (indiqué le 28/09 à 20:49)


Mais " A() : a(*this) {} " fait l'affaire. Pourquoi NULL ?


pourquoi pas ?

pour qui écrit un sample jamais publié (surtout pas utilisé) c'est
surement d'une haute importance; quand on est derrière un vrai compilo
pour du vrai code, le comportement est tout à fait complètement défini.


Y'a des faux copilos et du faux code maintenant ? Whouaa, j'apprend
quelque chose tous les jours. Merci Sylvain.


tu as mal lu / compris -- il y a l'approche théorique cadré et décrit
par la norme et il y a l'expérience (la vraie vie).

et pour résumer les 2 pts: je me cogne de savoir ce qu'un compilo que je
n'utiliserais jamais estime indéfini.


Tu n'utilises aucun compilateur ? Car c'est, à ma connaîssance, un
comportement indéfini dans tous les compilateurs.


ta connaissance n'est donc pas universelle.

Utiliser une référence comme un pointeur, c'est juste de l'obfuscation.


alors nous sommes tous des Jourdain.

AMHA, le but des références est justement de GARANTIR que l'objet
existe (this est toujours != 0).
non le premier but est de fluidifier l'écriture, accessoirement de forcer

l'initialisation avec une valeur non nulle.


A l'envers.
Le premier but est de forcer l'initialisation avec une valeur non nulle,
accessoirement de fluidifier l'écriture.


non, je maintiens. car tu sembles oublier que pour affecter une valeur.

préférer déclarer une fonction T& foo() plutôt que T* foo() satisfait
l'écriture - recevoir un T*, oblige à tester, et oblige à gérer l'erreur
(si c'en est une) ptr nul; avec un T& la méthode a géré les anomalies
(rectifier le statut de la donnée ou thrower ou whatever pertinent dans
son contexte) et l'appelant utilise bille-en-tête la réf.

Après ça, même un if(p!=0) ou if(&r!=0) est indéfini.


il n'y a pas d'après, le code a planté sur la 4ième ligne.

Une référence ne change pas d'identité.


pas spontanément mais on peut lui faire changer.

Comme James l'a dit et répété, le compilateur a totalement le droit
d'ignorer et optimiser (lire supprimer) le test "if(&ref != NULL)"
car il "sait" à l'avance que c'est "impossible".


3ième répet.: "tester cette valeur ne conduira pas à un code très clean
mais bon ..."

int& r = *(int*)0;
Quelle est l'utilité de r ?


là ici ? j'en ai aucune idée.

Plus sérieusement, dans le cas du noeud de btree, qu'est-ce qui
empêche d'utiliser this plutôt que NULL. C'est permi par la norme
et on obtient le résultat voulu.


perso, j'utiliserais (pour un noeud) des ptr et non des refs...

Sylvain.



3 4 5 6 7