Dlaczego (tylko) niektóre kompilatory używają tego samego adresu dla identycznych literałów łańcuchowych?

92

https://godbolt.org/z/cyBiWY

Widzę dwa 'some'literały w kodzie asemblera wygenerowanym przez MSVC, ale tylko jeden z clang i gcc. Prowadzi to do zupełnie innych wyników wykonania kodu.

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

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

Czy ktoś może wyjaśnić różnicę i podobieństwa między tymi wynikami kompilacji? Dlaczego clang / gcc optymalizuje coś, nawet jeśli żadna optymalizacja nie jest wymagana? Czy to jakieś niezdefiniowane zachowanie?

Zauważyłem również, że jeśli zmienię deklaracje na te pokazane poniżej, clang / gcc / msvc w ogóle nie pozostawi żadnych "some"w kodzie asemblera. Dlaczego zachowanie jest inne?

static const char A[] = "some";
static const char B[] = "some";
Eugene Kosov
źródło
4
stackoverflow.com/a/52424271/1133179 Dobra, trafna odpowiedź na blisko powiązane pytanie, ze standardowymi cytatami.
luk32
1
@ luk32 Omawiam tutaj flagi kompilatora, które mają na to wpływ
Shafik Yaghmour
6
W przypadku MSVC opcja kompilatora / GF kontroluje to zachowanie. Zobacz docs.microsoft.com/en-us/cpp/build/reference/…
Sjoerd
1
FYI, może się to zdarzyć również w przypadku funkcji.
user541686

Odpowiedzi:

109

To nie jest niezdefiniowane zachowanie, ale nieokreślone zachowanie. Dla napisowych ,

Kompilator może łączyć pamięć masową dla równych lub nakładających się literałów ciągów, ale nie jest to wymagane. Oznacza to, że identyczne literały łańcuchowe mogą, ale nie muszą, porównywać się równo przy porównywaniu przez wskaźnik.

Oznacza to, że wynik A == Bmoże być truelub false, na którym nie powinieneś polegać.

Ze standardu [lex.string] / 16 :

Nie określono, czy wszystkie literały ciągów są różne (to znaczy są przechowywane w obiektach, które nie nakładają się na siebie) i czy kolejne oceny literału ciągu dają ten sam, czy inny obiekt.

songyuanyao
źródło
36

Inne odpowiedzi wyjaśniały, dlaczego nie można oczekiwać, że adresy wskaźników będą różne. Jednak możesz łatwo przepisać to w sposób, który to gwarantuje Ai Bnie porównuje równych:

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

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

Różnica polega na tym Ai Bsą teraz tablice znaków. Oznacza to, że nie są one wskaźnikami, a ich adresy muszą być różne, tak jak musiałyby być adresy dwóch zmiennych całkowitych. C ++ myli to, ponieważ sprawia, że ​​wskaźniki i tablice wydają się zamienne ( operator*i operator[]zachowują się tak samo), ale są naprawdę różne. Np. Coś takiego const char *A = "foo"; A++;jest całkowicie legalne, ale const char A[] = "bar"; A++;nie jest.

Jednym ze sposobów myślenia o tej różnicy jest char A[] = "..."stwierdzenie: „daj mi blok pamięci i wypełnij go znakami, ...po których następuje \0”, natomiast char *A= "..."mówi „podaj adres, pod którym mogę znaleźć znaki, ...po których następuje \0”.

tobi_s
źródło
8
To byłaby jeszcze lepsza odpowiedź, gdybyś mógł wyjaśnić, dlaczego jest inaczej.
Mark Ransom
Należy zauważyć, że *pi p[0]nie tylko „wydają się zachowywać tak samo”, ale z definicji identyczne (pod warunkiem, że p+0 == pjest to relacja tożsamości, ponieważ 0jest to neutralny element w dodawaniu wskaźnika do liczby całkowitej). W końcu p[i]jest definiowany jako *(p+i). Odpowiedź jest jednak słuszna.
Peter - Przywróć Monikę
typeof(*p)i typeof(p[0])są jednymi i drugimi, charwięc naprawdę niewiele pozostało, co mogłoby być inne. Zgadzam się, że „wydaje się zachowywać tak samo” nie jest najlepszym sformułowaniem, ponieważ semantyka jest tak różna. Twój post przypomniał mi najlepszy sposób dostępu elementów macierzy C ++: 0[p], 1[p], 2[p]itd. To jest jak robią to profesjonaliści, przynajmniej jeśli chcą zmylić ludzi, którzy urodzili się po języku programowania C.
tobi_s
Jest to interesujące i kusiło mnie, aby dodać link do C FAQ, ale zdałem sobie sprawę, że jest wiele powiązanych pytań, ale żadne z nich nie wydaje się przecinać bezpośrednio do sedna tego pytania.
tobi_s
23

To, czy kompilator zdecyduje się użyć tej samej lokalizacji ciągu dla implementacji Ai Bzależy od tego, czy. Formalnie możesz powiedzieć, że zachowanie twojego kodu jest nieokreślone .

Obie opcje poprawnie implementują standard C ++.

Batszeba
źródło
Zachowaniem kodu jest albo zgłoszenie wyjątku, albo nie robienie niczego, co zostało wybrane, przed pierwszym wykonaniem kodu, w nieokreślony sposób . Nie oznacza to, że zachowanie jako całość jest nieokreślone - po prostu kompilator może wybrać dowolne zachowanie w dowolny sposób, jaki uzna za stosowny, przed pierwszym zaobserwowaniem zachowania.
supercat
3

Jest to optymalizacja w celu zaoszczędzenia miejsca, często nazywana „łączeniem ciągów”. Oto dokumentacja dla MSVC:

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

Dlatego jeśli dodasz / GF do wiersza poleceń, powinieneś zobaczyć to samo zachowanie z MSVC.

Nawiasem mówiąc, prawdopodobnie nie powinieneś porównywać ciągów za pomocą takich wskaźników, każde przyzwoite narzędzie do analizy statycznej oznaczy ten kod jako wadliwy. Musisz porównać to, na co wskazują, a nie rzeczywiste wartości wskaźnika.

Paulm
źródło