Pourquoi certains compilateurs utilisent-ils (seulement) la même adresse pour des chaînes littérales identiques?

92

https://godbolt.org/z/cyBiWY

Je peux voir deux 'some'littéraux dans le code assembleur généré par MSVC, mais un seul avec clang et gcc. Cela conduit à des résultats totalement différents de l'exécution du code.

static const char *A = "some";
static const char *B = "some";

void f() {
    if (A == B) {
        throw "Hello, string merging!";
    }
}

Quelqu'un peut-il expliquer la différence et les similitudes entre ces sorties de compilation? Pourquoi clang / gcc optimise-t-il quelque chose même si aucune optimisation n'est demandée? Est-ce une sorte de comportement indéfini?

Je remarque également que si je change les déclarations pour celles présentées ci-dessous, clang / gcc / msvc n'en laisse aucune "some"dans le code assembleur. Pourquoi le comportement est-il différent?

static const char A[] = "some";
static const char B[] = "some";
Eugène Kosov
la source
4
stackoverflow.com/a/52424271/1133179 Une belle réponse pertinente à une question étroitement liée, avec des guillemets standard.
luk32
1
@ luk32 Je discute des drapeaux du compilateur qui
Shafik Yaghmour
6
Pour MSVC, l'option du compilateur / GF contrôle ce comportement. Voir docs.microsoft.com/en-us/cpp/build/reference/…
Sjoerd
1
Pour info, cela peut également arriver pour les fonctions.
user541686

Réponses:

109

Ce n'est pas un comportement indéfini, mais un comportement non spécifié. Pour les littéraux de chaîne ,

Le compilateur est autorisé, mais pas obligatoire, à combiner le stockage pour des littéraux de chaîne égaux ou superposés. Cela signifie que des littéraux de chaîne identiques peuvent ou non être comparables lorsqu'ils sont comparés par un pointeur.

Cela signifie que le résultat de A == Bpourrait être trueou false, sur lequel vous ne devriez pas dépendre.

D'après la norme, [lex.string] / 16 :

Le fait de savoir si tous les littéraux de chaîne sont distincts (c'est-à-dire qu'ils sont stockés dans des objets sans chevauchement) et si les évaluations successives d'un littéral de chaîne produisent le même objet ou un objet différent n'est pas spécifié.

songyuanyao
la source
36

Les autres réponses expliquent pourquoi vous ne pouvez pas vous attendre à ce que les adresses des pointeurs soient différentes. Pourtant, vous pouvez facilement réécrire ceci d'une manière qui garantit cela Aet Bne pas comparer égal:

static const char A[] = "same";
static const char B[] = "same";// but different

void f() {
    if (A == B) {
        throw "Hello, string merging!";
    }
}

La différence étant que Aet Bsont maintenant des tableaux de caractères. Cela signifie qu'ils ne sont pas des pointeurs et que leurs adresses doivent être distinctes, tout comme celles de deux variables entières devraient l'être. C ++ confond cela car il rend les pointeurs et les tableaux interchangeables ( operator*et operator[]semblent se comporter de la même manière), mais ils sont vraiment différents. Par exemple, quelque chose comme const char *A = "foo"; A++;est parfaitement légal, maisconst char A[] = "bar"; A++; ne l'est pas.

Une façon de penser à la différence est que char A[] = "..."dit "donnez-moi un bloc de mémoire et remplissez-le avec les caractères ...suivis de \0", alors que char *A= "..."dit "donnez-moi une adresse à laquelle je peux trouver les caractères ...suivis de \0".

tobi_s
la source
8
Ce serait une réponse encore meilleure si vous pouviez expliquer pourquoi c'est différent.
Mark Ransom
Notez que *pet p[0]non seulement "semblent se comporter de la même manière", mais par définition sont identiques (à condition que ce p+0 == psoit une relation d'identité parce que 0c'est l'élément neutre dans l'addition pointeur-entier). Après tout, p[i]est défini comme *(p+i). La réponse fait cependant un bon point.
Peter - Réintègre Monica
typeof(*p)et typeof(p[0])sont les deux, chardonc il ne reste vraiment pas grand-chose qui pourrait être différent. Je suis d'accord que «semblent se comporter de la même manière» n'est pas la meilleure formulation, car la sémantique est si différente. Votre message m'a rappelé la meilleure façon d'éléments d'accès des réseaux de C: 0[p], 1[p], 2[p]etc. Voici comment les pros le faire, au moins quand ils veulent confondre les gens qui sont nés après le langage de programmation C.
tobi_s
C'est intéressant, et j'ai été tenté d'ajouter un lien vers la FAQ C, mais je me suis rendu compte qu'il y avait beaucoup de questions connexes, mais aucune ne semble aller droit au but de cette question ici.
tobi_s
23

Le fait qu'un compilateur choisisse ou non d'utiliser le même emplacement de chaîne pour Aet Bdépend de l'implémentation. Officiellement, vous pouvez dire que le comportement de votre code n'est pas spécifié .

Les deux choix implémentent correctement le standard C ++.

Bathsheba
la source
Le comportement du code est soit de lever une exception, soit de ne rien faire, choisi, avant la première exécution du code, d'une manière non spécifiée . Cela ne signifie pas que le comportement dans son ensemble n'est pas spécifié - simplement que le compilateur peut sélectionner l'un ou l'autre comportement de la manière qu'il juge appropriée avant la première fois que le comportement est observé.
supercat
3

Il s'agit d'une optimisation pour économiser de l'espace, souvent appelée "regroupement de chaînes". Voici la documentation pour MSVC:

https://msdn.microsoft.com/en-us/library/s0s0asdt.aspx

Par conséquent, si vous ajoutez / GF à la ligne de commande, vous devriez voir le même comportement avec MSVC.

Au fait, vous ne devriez probablement pas comparer des chaînes via des pointeurs comme celui-ci, tout outil d'analyse statique décent signalera ce code comme défectueux. Vous devez comparer ce qu'ils pointent, pas les valeurs réelles du pointeur.

paulm
la source