Appel de fonction avec pointeur sur non-const et argument pointeur sur const de la même adresse

14

Je veux écrire une fonction qui entre un tableau de données et génère un autre tableau de données à l'aide de pointeurs.

Je me demande quel est le résultat si les deux srcet dstpointe à la même adresse parce que je sais compilateur peut optimiser pour const. S'agit-il d'un comportement indéfini? (J'ai marqué à la fois C et C ++ parce que je ne sais pas si la réponse peut différer entre eux, et je veux en savoir plus sur les deux.)

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

En plus de la question ci-dessus, est-ce bien défini si je supprime le constcode d'origine?

Willy
la source

Réponses:

17

S'il est vrai que le comportement est bien défini - il n'est pas vrai que les compilateurs peuvent "optimiser pour const" dans le sens que vous voulez dire.

Autrement dit, un compilateur n'est pas autorisé à supposer que, simplement parce qu'un paramètre est un const T* ptr, la mémoire pointée par ptrne sera pas modifiée via un autre pointeur. Les pointeurs n'ont même pas besoin d'être égaux. C'est constune obligation, pas une garantie - une obligation de votre part (= la fonction) de ne pas apporter de modifications via ce pointeur.

Afin d'avoir réellement cette garantie, vous devez marquer le pointeur avec le restrictmot - clé. Ainsi, si vous compilez ces deux fonctions:

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()fonction doit lire deux fois x, alors qu'elle bar()n'a besoin de la lire qu'une seule fois:

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

Regardez ça en direct GodBolt.

restrictn'est qu'un mot-clé en C (depuis C99); malheureusement, il n'a pas encore été introduit en C ++ (pour la mauvaise raison qu'il est plus compliqué de l'introduire en C ++). Cependant, de nombreux compilateurs le supportent un peu comme __restrict.

Conclusion: le compilateur doit prendre en charge votre cas d'utilisation "ésotérique" lors de la compilation f()et n'aura aucun problème avec celui-ci.


Voir cet article concernant les cas d'utilisation de restrict.

einpoklum
la source
constn'est pas «une obligation de votre part (= la fonction) de ne pas apporter de modifications via ce pointeur». La norme C permet à la fonction de supprimer constvia un cast puis de modifier l'objet à travers le résultat. Essentiellement, ce constn'est qu'un conseil et une commodité pour le programmeur pour éviter de modifier un objet par inadvertance.
Eric Postpischil
@EricPostpischil: C'est une obligation dont vous pouvez vous débarrasser.
einpoklum
Une obligation dont vous pouvez vous sortir n'est pas une obligation.
Eric Postpischil
2
@EricPostpischil: 1. Vous divisez les cheveux ici. 2. Ce n'est pas vrai.
einpoklum
1
C'est pourquoi memcpyet strcpysont déclarés avec des restrictarguments, alors que ce memmoven'est pas le cas - seul ce dernier permet le chevauchement entre les blocs de mémoire.
Barmar
5

C'est bien défini (en C ++, plus sûr en C), avec et sans le constqualificatif.

La première chose à rechercher est la règle stricte d'alias 1 . Si srcet dstpointe vers le même objet:

  • en C, ils doivent être de types compatibles ; char*et char const*ne sont pas compatibles.
  • en C ++, ils doivent être de types similaires ; char*et char const*sont similaires.

En ce qui concerne le constqualificatif, vous pourriez faire valoir que, depuis que dst == srcvotre fonction modifie effectivement ce qui srcpointe vers, srcne doit pas être qualifiée de const. Ce n'est pas comme ça que ça constmarche. Deux cas doivent être considérés:

  1. Lorsqu'un objet est défini comme const, comme dans char const data[42];, le modifier (directement ou indirectement) conduit à un comportement indéfini.
  2. Lorsqu'une référence ou un pointeur vers un constobjet est défini, comme dans char const* pdata = data;, on peut modifier l'objet sous-jacent à condition qu'il n'ait pas été défini comme const2 (voir 1.). Donc, ce qui suit est bien défini:
int main()
{
    int result = 42;
    int const* presult = &result;
    *const_cast<int*>(presult) = 0;
    return *presult; // 0
}

1) Quelle est la règle stricte d'alias?
2) Est-ce const_castsûr?

YSC
la source
Peut-être que le PO signifie une réorganisation possible des affectations?
Igor R.
char*et char const*ne sont pas compatibles. _Generic((char *) 0, const char *: 1, default: 0))évalue à zéro.
Eric Postpischil
La formulation «Lorsqu'une référence ou un pointeur vers un constobjet est défini» est incorrecte. Vous voulez dire que lorsqu'une référence ou un pointeur vers un typeconst qualifié est défini, cela ne signifie pas que l'objet sur lequel il doit pointer ne peut pas être modifié (par divers moyens). (Si le pointeur pointe vers un objet, cela signifie que l'objet est en effet par définition, donc le comportement d'essayer de le modifier n'est pas défini.)constconst
Eric Postpischil
@Eric, je ne suis précis que lorsque la question concerne Standard ou balisée language-lawyer. L'exactitude est une valeur que je chéris, mais je suis également consciente qu'elle s'accompagne de plus de complexité. Ici, j'ai décidé d'opter pour la simplicité et les phrases faciles à comprendre, car je crois que c'est ce que OP souhaitait. Si vous pensez le contraire, veuillez répondre, je serai parmi les premiers à le voter. Quoi qu'il en soit, merci pour ton commentaire.
YSC
3

Ceci est bien défini dans C. Les règles d'alias strictes ne s'appliquent pas avec le chartype, ni avec deux pointeurs du même type.

Je ne sais pas trop ce que vous entendez par "optimiser pour const". Mon compilateur (GCC 8.3.0 x86-64) génère exactement le même code pour les deux cas. Si vous ajoutez le restrictspécificateur aux pointeurs, le code généré est légèrement meilleur, mais cela ne fonctionnera pas pour votre cas, les pointeurs étant les mêmes.

(C11 §6.5 7)

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants:
- un type compatible avec le type effectif de l'objet,
- une version qualifiée d'un type compatible avec le type effectif de l'objet,
- un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
- un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
- un type d'agrégation ou d'union qui en inclut un parmi les types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue), ou
- un type de caractère.

Dans ce cas (sans restrict), vous obtiendrez toujours 121le résultat.

SS Anne
la source