Чому (лише) деякі компілятори використовують одну і ту ж адресу для однакових рядкових літералів?


92

https://godbolt.org/z/cyBiWY

Я бачу два 'some'літерали в коді асемблера, згенерованому MSVC, але лише один із clang та gcc. Це призводить до абсолютно різних результатів виконання коду.

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

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

Хто-небудь може пояснити різницю та подібність між цими результатами компіляції? Чому clang / gcc оптимізує щось, навіть коли оптимізація не вимагається? Це якась невизначена поведінка?

Я також помічаю, що якщо я зміню декларації на наведені нижче, clang / gcc / msvc взагалі не залишають жодного "some"в коді асемблера. Чому поведінка відрізняється?

static const char A[] = "some";
static const char B[] = "some";

4
stackoverflow.com/a/52424271/1133179 Якась приємна відповідна відповідь на тісно пов'язане питання зі стандартними цитатами.
luk32


6
Для MSVC параметр компілятора / GF контролює цю поведінку. Див. Docs.microsoft.com/en-us/cpp/build/reference/…
Sjoerd

1
FYI, це може трапитися і з функціями.
user541686

Відповіді:


109

Це не невизначена поведінка, а невизначена поведінка. Для рядкових літералів ,

Компілятору дозволено, але не потрібно, поєднувати пам'ять для рівних або перекриваються рядкових літералів. Це означає, що однакові рядкові літерали можуть, а можуть і не порівнювати рівні при порівнянні покажчиком.

Це означає, що результат A == Bможе бути trueабо false, від якого ви не повинні залежати.

Зі стандарту, [lex.string] / 16 :

Чи всі рядкові літерали є різними (тобто зберігаються в об’єктах, що не перекриваються), і чи послідовні оцінки рядкового літералу дають той самий чи інший об’єкт, не вказано.


36

Інші відповіді пояснювали, чому ви не можете очікувати, що адреси вказівників будуть різними. Тим не менш, ви можете легко переписати це так, щоб це гарантувало, Aі Bне порівнювати рівне:

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

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

Різниця в тому , що Aі Bтепер масиви символів. Це означає, що вони не є покажчиками, і їх адреси повинні бути різними, як і адреси двох цілих змінних. C ++ плутає це, оскільки робить покажчики та масиви взаємозамінними ( operator*і, operator[]здається, поводиться однаково), але вони насправді різні. Наприклад, щось на зразок const char *A = "foo"; A++;цілком законно, але const char A[] = "bar"; A++;ні.

Один із способів подумати про різницю полягає в тому, що char A[] = "..."сказано "дайте мені блок пам'яті і заповніть його символами, ...за якими слідують \0", тоді як char *A= "..."говорить "дайте мені адресу, за якою я можу знайти символи, ...за якими слідують \0".


8
Це було б ще кращою відповіддю, якби ви могли пояснити, чому це інакше.
Марк Ренсом,

Слід зазначити , що *pі p[0]не тільки «здається, поводяться так само» , але , за визначенням , є ідентичними ( при умови , що p+0 == pє ставлення ідентичності , тому що 0це нейтральний елемент в покажчик целочисленного складання). Зрештою, p[i]визначається як *(p+i). Відповідь робить хороший момент.
Пітер - Відновити Моніку

typeof(*p)і typeof(p[0])обидва, charтож насправді залишилось не так багато того, що могло б відрізнятися. Я згоден з тим, що "здається, поводиться однаково" - не найкраще формулювання, оскільки семантика настільки різна. Ваш пост нагадав мені про кращий спосіб доступу елементів масивів C ++: 0[p], 1[p], і 2[p]т.д. Це, як це роблять професіонали, принаймні , коли вони хочуть , щоб заплутати людей , які народилися після того, як на мові програмування Сі.
tobi_s


Це цікаво, і у мене виник спокуса додати посилання на поширені запитання C, але я зрозумів, що є багато пов’язаних запитань, але, здається, жодне з них тут не вирішує питання.
tobi_s

23

Незалежно від того, вирішить компілятор використовувати одне і те ж розташування рядків для Aі Bзалежить від реалізації. Формально ви можете сказати, що поведінка вашого коду не визначена .

Обидва варіанти правильно впроваджують стандарт C ++.


Поведінка коду полягає у тому, щоб або створити виняток, або нічого не зробити, вибране, до першого запуску коду, неуточненим чином . Це не означає, що поведінка в цілому є невизначеною - просто компілятор може вибрати будь-яку поведінку будь-яким способом, який вважає за потрібне до першого спостереження поведінки.
supercat

3

Це оптимізація для економії місця, яку часто називають «об’єднанням рядків». Ось документи для MSVC:

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

Тому, якщо ви додаєте / GF до командного рядка, ви повинні побачити таку ж поведінку з MSVC.

До речі, ви, мабуть, не повинні порівнювати рядки за допомогою таких покажчиків, будь-який гідний інструмент статичного аналізу позначить цей код як дефектний. Вам потрібно порівняти те, на що вони вказують, а не фактичні значення покажчика.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.