Chiamata di funzione con puntatore a non const e puntatore a argomenti const dello stesso indirizzo


14

Voglio scrivere una funzione che input una matrice di dati e output un'altra matrice di dati usando i puntatori.

Mi chiedo quale sia il risultato se entrambi srce dstindicato lo stesso indirizzo perché so che il compilatore può ottimizzare per const. È un comportamento indefinito? (Ho taggato sia C che C ++ perché non sono sicuro che la risposta possa differire tra loro e voglio sapere di entrambi.)

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

Oltre alla domanda precedente, è ben definito se elimino il constcodice originale?

Risposte:


17

Se è vero che il comportamento è ben definito - è non è vero che i compilatori possono "Ottimizza per const", nel senso che vuoi dire.

Cioè, un compilatore non è ammesso che solo perché un parametro è a const T* ptr, la memoria indicata da ptrnon verrà modificata tramite un altro puntatore. I puntatori non devono nemmeno essere uguali. Il constè un obbligo, non una garanzia - un obbligo da voi (= la funzione) di non apportare modifiche attraverso tale puntatore.

Per avere effettivamente quella garanzia, è necessario contrassegnare il puntatore con la restrictparola chiave. Pertanto, se si compilano queste due funzioni:

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

la foo()funzione deve leggere due volte da x, mentre bar()deve leggerlo solo una volta:

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

Guarda questo dal vivo GodBolt.

restrictè solo una parola chiave in C (dal C99); sfortunatamente, finora non è stato introdotto in C ++ (per la scarsa ragione che è più complicato introdurlo in C ++). Molti compilatori lo supportano, comunque, come __restrict.

In conclusione: il compilatore deve supportare il caso d'uso "esoterico" durante la compilazione f()e non avrà alcun problema con esso.


Vedi questo post relativo ai casi d'uso per restrict.


constnon è "un obbligo da parte tua (= la funzione) di non apportare modifiche tramite quel puntatore". Lo standard C consente di rimuovere la funzione consttramite un cast e quindi modificare l'oggetto attraverso il risultato. In sostanza, constè solo una consulenza e una comodità per il programmatore per aiutare a evitare di modificare inavvertitamente un oggetto.
Eric Postpischil,

@EricPostpischil: è un obbligo da cui puoi uscire.
einpoklum,

Un obbligo da cui puoi uscire non è un obbligo.
Eric Postpischil,

2
@EricPostpischil: 1. Stai dividendo i capelli qui. 2. Non è vero.
einpoklum,

1
Questo è il motivo memcpye strcpysono dichiarati con restrictargomenti, mentre memmovenon lo è - solo quest'ultimo consente la sovrapposizione tra i blocchi di memoria.
Barmar il

5

Questo è ben definito (in C ++, non più sicuro in C), con e senza il constqualificatore.

La prima cosa da cercare è la rigida regola di aliasing 1 . Se srce dstpunta allo stesso oggetto:

  • in C, devono essere di tipo compatibile ; char*e char const*non sono compatibili.
  • in C ++, devono essere di tipo simile ; char*e char const*sono simili.

Per quanto riguarda il constqualificatore, potresti obiettare che da quando la dst == srctua funzione modifica efficacemente ciò a cui srcpunta, srcnon dovrebbe essere qualificato come const. Non constfunziona così . Due casi devono essere considerati:

  1. Quando si definisce un oggetto const, come in char const data[42];, modificarlo (direttamente o indirettamente) porta a un comportamento indefinito.
  2. Quando constviene definito un riferimento o un puntatore a un oggetto, come in char const* pdata = data;, si può modificare l'oggetto sottostante a condizione che non sia stato definito come const2 (vedere 1.). Quindi quanto segue è ben definito:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Qual è la regola di aliasing rigorosa?
2) è const_castsicuro?


Forse il PO significa possibile riordino degli incarichi?
Igor R.

char*e char const*non sono compatibili. _Generic((char *) 0, const char *: 1, default: 0))valuta a zero.
Eric Postpischil,

La frase "Quando constviene definito un riferimento o un puntatore a un oggetto" non è corretta. Intendi che quando viene definito un riferimento o un puntatore a un tipoconst -qualificato , ciò non significa che l'oggetto a cui è impostato puntare non possa essere modificato (in vari modi). (Se il puntatore punta a un oggetto, significa che l'oggetto è effettivamente per definizione, quindi il comportamento del tentativo di modificarlo non è definito.)constconst
Eric Postpischil

@Eric, sono così specifico quando la domanda riguarda Standard o taggata language-lawyer. L'esattezza è un valore che apprezzo molto, ma sono anche consapevole che viene fornito con più complessità. Qui, ho deciso di optare per frasi semplici e di facile comprensione, perché credo che questo sia voluto da OP. Se pensi diversamente, rispondi, sarò tra i primi a votarlo. Comunque, grazie per il tuo commento.
YSC

3

Questo è ben definito in C. Le regole di aliasing rigorose non si applicano con il chartipo, né con due puntatori dello stesso tipo.

Non sono sicuro di cosa intendi per "ottimizzare per const". Il mio compilatore (GCC 8.3.0 x86-64) genera lo stesso codice esatto per entrambi i casi. Se aggiungi l' restrictidentificatore ai puntatori, il codice generato è leggermente migliore, ma non funzionerà per il tuo caso, i puntatori saranno gli stessi.

(C11 §6.5 7)

Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi:
- un tipo compatibile con il tipo effettivo dell'oggetto,
- una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,
- un tipo che è il tipo con segno o senza segno corrispondente al tipo effettivo dell'oggetto,
- un tipo che è il tipo con segno o non firmato corrispondente a una versione qualificata del tipo effettivo dell'oggetto,
- un tipo aggregato o unione che include uno dei suddetti tipi tra i suoi membri (incluso, ricorsivamente, un membro di un'unione sottaggregata o contenuta), oppure
- un tipo di carattere.

In questo caso (senza restrict), otterrai sempre 121come risultato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.