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

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";

Ответ 1

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

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

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

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

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

Ответ 2

В других ответах объясняется, почему вы не можете ожидать, что адреса указателей будут разными. Однако вы можете легко переписать это так, чтобы гарантировать, что 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 = "foo"; A++; совершенно законна, но const char A[] = "bar"; A++; const char A[] = "bar"; A++; нет.

Один из способов подумать о различии заключается в том, что char A[] = "..." говорит: "Дайте мне блок памяти и заполните его символами ... за которым следует \0 ", тогда как char *A= "..." говорит" дайте мне адрес, в котором я могу найти символы ... за которым следует \0 ".

Ответ 3

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

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

Ответ 4

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

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

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

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