Por que (apenas) alguns compiladores usam o mesmo endereço para literais de string idênticos?

92

https://godbolt.org/z/cyBiWY

Posso ver dois 'some'literais no código assembler gerado pelo MSVC, mas apenas um com clang e gcc. Isso leva a resultados totalmente diferentes de execução de código.

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

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

Alguém pode explicar a diferença e semelhanças entre essas saídas de compilação? Por que o clang / gcc otimiza algo mesmo quando nenhuma otimização é solicitada? É algum tipo de comportamento indefinido?

Também noto que, se eu alterar as declarações para as mostradas abaixo, clang / gcc / msvc não deixará nenhuma "some"no código do assembler. Por que o comportamento é diferente?

static const char A[] = "some";
static const char B[] = "some";
Eugene Kosov
fonte
4
stackoverflow.com/a/52424271/1133179 Algumas boas respostas relevantes para uma pergunta intimamente relacionada, com aspas padrão.
luk32,
6
Para MSVC, a opção do compilador / GF controla esse comportamento. Consulte docs.microsoft.com/en-us/cpp/build/reference/…
Sjoerd
1
Para sua informação, isso pode acontecer para funções também.
user541686

Respostas:

109

Este não é um comportamento indefinido, mas um comportamento não especificado. Para literais de string ,

O compilador tem permissão, mas não é obrigatório, para combinar armazenamento para literais de string iguais ou sobrepostos. Isso significa que literais de string idênticos podem ou não ser comparados iguais quando comparados por ponteiro.

Isso significa que o resultado de A == Bpode ser trueou false, do qual você não deve depender.

Do padrão, [lex.string] / 16 :

Se todos os literais de string são distintos (ou seja, são armazenados em objetos não sobrepostos) e se as avaliações sucessivas de um literal de string produzem o mesmo objeto ou um objeto diferente não é especificado.

Songyuanyao
fonte
36

As outras respostas explicaram porque você não pode esperar que os endereços do ponteiro sejam diferentes. No entanto, você pode facilmente reescrever isso de uma forma que garanta Ae Bnão compare igual:

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

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

A diferença é que Ae Bagora são matrizes de personagens. Isso significa que eles não são ponteiros e seus endereços precisam ser distintos, assim como os de duas variáveis ​​inteiras deveriam ser. C ++ confunde isso porque faz ponteiros e arrays parecerem intercambiáveis ​​( operator*e operator[]parecem se comportar da mesma forma), mas eles são realmente diferentes. Por exemplo, algo como const char *A = "foo"; A++;é perfeitamente legal, mas const char A[] = "bar"; A++;não é.

Uma maneira de pensar sobre a diferença é char A[] = "..."dizer "dê-me um bloco de memória e preencha-o com os caracteres ...seguidos de \0", enquanto char *A= "..."diz "dê-me um endereço no qual eu possa encontrar os caracteres ...seguidos de \0".

tobi_s
fonte
8
Essa seria uma resposta ainda melhor se você pudesse explicar por que é diferente.
Mark Ransom
Observe que *pe p[0]não apenas "parecem se comportar da mesma forma", mas por definição são idênticos (desde que p+0 == pseja uma relação de identidade, pois 0é o elemento neutro na adição de ponteiro-inteiro). Afinal, p[i]é definido como *(p+i). A resposta é bastante válida.
Peter - Reintegrar Monica em
typeof(*p)e typeof(p[0])são ambos, charentão não há realmente muito que possa ser diferente. Eu concordo que 'parecem se comportar da mesma forma' não é a melhor formulação, porque a semântica é muito diferente. O envio da mensagem me lembrou a melhor maneira de elementos de acesso de matrizes C ++: 0[p], 1[p], 2[p]etc. Esta é a forma como os profissionais fazem isso, pelo menos quando querem confundir as pessoas que nasceram após a linguagem de programação C.
tobi_s
Isso é interessante, e fiquei tentado a adicionar um link para o FAQ C, mas percebi que há muitas questões relacionadas, mas nenhuma parece ir direto ao ponto desta questão aqui.
tobi_s
23

Se um compilador escolhe ou não usar o mesmo local de string para Ae isso Bdepende da implementação. Formalmente, você pode dizer que o comportamento do seu código não é especificado .

Ambas as opções implementam o padrão C ++ corretamente.

Bate-Seba
fonte
O comportamento do código é lançar uma exceção ou não fazer nada, escolhido, antes da primeira vez que o código é executado, de maneira não especificada . Isso não significa que o comportamento como um todo não seja especificado - apenas que o compilador pode selecionar qualquer um dos comportamentos da maneira que achar adequada antes da primeira observação do comportamento.
supercat
3

É uma otimização para economizar espaço, geralmente chamada de "string pooling". Aqui estão os documentos para MSVC:

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

Portanto, se você adicionar / GF à linha de comando, deverá ver o mesmo comportamento com MSVC.

A propósito, você provavelmente não deve comparar strings por meio de ponteiros como esse, qualquer ferramenta de análise estática decente sinalizará esse código como defeituoso. Você precisa comparar o que eles apontam, não os valores reais do ponteiro.

Paulm
fonte