Chamada de função com argumentos ponteiro para não-const e ponteiro para const do mesmo endereço

14

Eu quero escrever uma função que insira uma matriz de dados e produza outra matriz de dados usando ponteiros.

Gostaria de saber qual é o resultado se ambos srce dstapontou para o mesmo endereço, porque eu sei que o compilador pode otimizar para const. É um comportamento indefinido? Marquei C e C ++ porque não tenho certeza se a resposta pode diferir entre eles e quero saber sobre os dois.

void f(const char *src, char *dst) {
    dst[2] = src[0];
    dst[1] = src[1];
    dst[0] = src[2];
}

int main() {
    char s[] = "123";
    f(s,s);
    printf("%s\n", s);
    return 0;
}

Além da pergunta acima, isso está bem definido se eu excluir o constcódigo original?

Willy
fonte

Respostas:

17

Embora seja verdade que o comportamento é bem definido - é não verdade que compiladores pode "otimizar para const", no sentido que você quer dizer.

Ou seja, não é permitido que um compilador assuma que apenas porque um parâmetro é a const T* ptr, a memória apontada por ptrnão será alterada por outro ponteiro. Os ponteiros nem precisam ser iguais. A consté uma obrigação, não uma garantia - uma obrigação por você (= a função) não fazer alterações por meio desse ponteiro.

Para realmente ter essa garantia, você precisa marcar o ponteiro com a restrictpalavra - chave. Portanto, se você compilar essas duas funções:

int foo(const int* x, int* y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

int bar(const int* x, int* restrict y) {
    int result = *x;
    (*y)++;
    return result + *x;
}

a foo()função deve ler duas vezes x, enquanto bar()só precisa ler uma vez:

foo:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, DWORD PTR [rdi]  # second read
        ret
bar:
        mov     eax, DWORD PTR [rdi]
        add     DWORD PTR [rsi], 1
        add     eax, eax              # no second read
        ret

Veja isso ao vivo GodBolt.

restricté apenas uma palavra-chave em C (desde C99); infelizmente, ele não foi introduzido no C ++ até agora (pelo baixo motivo que é mais complicado introduzi-lo no C ++). Muitos compiladores suportam isso, no entanto, como __restrict.

Conclusão: O compilador deve oferecer suporte ao seu caso de uso "esotérico" ao compilar f()e não terá nenhum problema com ele.


Consulte esta postagem sobre casos de uso para restrict.

einpoklum
fonte
constnão é "uma obrigação sua (= a função) de não fazer alterações através desse ponteiro". O padrão C permite que a função seja removida constpor meio de uma conversão e, em seguida, modifique o objeto através do resultado. Essencialmente, consté apenas um aviso e uma conveniência para o programador para ajudar a evitar a modificação inadvertida de um objeto.
Eric Postpischil 13/03
@ EricPostpischil: É uma obrigação da qual você pode se livrar.
einpoklum 13/03
Uma obrigação da qual você pode se livrar não é uma obrigação.
Eric Postpischil 13/03
2
@ EricPostpischil: 1. Você está dividindo cabelos aqui. 2. Isso não é verdade.
einpoklum 13/03
11
É por isso memcpye strcpyé declarado com restrictargumentos, enquanto memmovenão é - apenas o último permite sobreposição entre os blocos de memória.
Barmar 13/03
5

Isso está bem definido (em C ++, não há mais certeza em C), com e sem o constqualificador.

A primeira coisa a procurar é a regra estrita de aliasing 1 . Se srce dstaponta para o mesmo objeto:

  • em C, eles devem ser do tipo compatível ; char*e char const*não são compatíveis.
  • em C ++, eles devem ser de tipos semelhantes ; char*e char const*são semelhantes.

Em relação ao constqualificador, você pode argumentar que, desde que dst == srcsua função efetivamente modifique os srcpontos, srcnão deve ser qualificado como const. Não é assim que constfunciona. Dois casos precisam ser considerados:

  1. Quando um objeto é definido constcomo char const data[42];modificado (direta ou indiretamente), leva a um comportamento indefinido.
  2. Quando uma referência ou ponteiro para um constobjeto é definido, como em char const* pdata = data;, é possível modificar o objeto subjacente, desde que não tenha sido definido como const2 (consulte 1.). Portanto, o seguinte está bem definido:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Qual é a regra estrita de aliasing?
2) É const_castseguro?

YSC
fonte
Talvez o OP signifique possível reordenação das tarefas?
Igor R.
char*e char const*não são compatíveis. _Generic((char *) 0, const char *: 1, default: 0))avalia como zero.
Eric Postpischil 13/03
A frase “Quando uma referência ou um ponteiro para um constobjeto é definida” está incorreta. Você quer dizer que quando uma referência ou ponteiro para um tipoconst qualificado é definido, isso não significa que o objeto para o qual ele está apontado não pode ser modificado (por vários meios). (Se o ponteiro aponta para um objeto, isso significa que ele é realmente por definição, portanto o comportamento de tentar modificá-lo não é definido.)constconst
Eric Postpischil
@ Eric, sou apenas tão específico quando a pergunta é sobre padrão ou marcada language-lawyer. Exatidão é um valor que eu aprecio, mas também sei que ele vem com mais complexidade. Aqui, decidi optar por frases simples e fáceis de entender, porque acredito que isso é o que o OP queria. Se você pensa o contrário, responda, estarei entre os primeiros a votar. De qualquer forma, obrigado pelo seu comentário.
YSC 13/03
3

Isso está bem definido em C. As regras estritas de alias não se aplicam ao chartipo, nem a dois ponteiros do mesmo tipo.

Não sei ao certo o que você quer dizer com "otimizar para const". Meu compilador (GCC 8.3.0 x86-64) gera exatamente o mesmo código para os dois casos. Se você adicionar o restrictespecificador aos ponteiros, o código gerado será um pouco melhor, mas isso não funcionará no seu caso, os ponteiros sendo os mesmos.

(C11 §6.5, 7)

Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos:
- um tipo compatível com o tipo efetivo do objeto,
- uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do objeto,
- um tipo agregado ou de união que inclui um dos tipos acima mencionados entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou
- um tipo de caractere.

Nesse caso (sem restrict), você sempre terá 121como resultado.

SS Anne
fonte