Почему (только) некоторые компиляторы используют один и тот же адрес для одинаковых строковых литералов?

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/...
Сьерд
1
К вашему сведению, это может случиться и с функциями.
user541686
Как компиляторы C ++
объединяют

Ответы:

109

Это не неопределенное поведение, а неопределенное поведение. Для строковых литералов ,

Компилятору разрешено, но не обязательно, объединять хранилище для равных или перекрывающихся строковых литералов. Это означает, что идентичные строковые литералы могут сравниваться, а могут и не совпадать при сравнении по указателю.

Это означает, что результатом A == Bможет быть trueили false, от которого вы не должны зависеть.

Из стандарта [lex.string] / 16 :

Все ли строковые литералы различны (то есть хранятся в неперекрывающихся объектах) и не определено, дают ли последовательные вычисления строкового литерала один и тот же или другой объект.

Songyuanyao
источник
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».

tobi_s
источник
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
Это интересно, и у меня возникло желание добавить ссылку на FAQ по C, но я понял, что есть много связанных вопросов, но ни один из них, похоже, не подходит к сути этого вопроса здесь.
tobi_s
23

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

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

Вирсавия
источник
Поведение кода заключается в том, чтобы либо генерировать исключение, либо ничего не делать, выбранное до первого выполнения кода, неопределенным образом . Это не означает, что поведение в целом не определено - просто компилятор может выбрать любое поведение любым способом, который он сочтет подходящим, до первого наблюдения за поведением.
supercat
3

Это оптимизация для экономии места, часто называемая «объединением строк». Вот документы для MSVC:

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

Поэтому, если вы добавите / GF в командную строку, вы должны увидеть такое же поведение с MSVC.

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

Паульм
источник