J'ai cette fonction de test très simple que j'utilise pour comprendre ce qui se passe avec le qualificatif const.
int test(const int* dummy)
{
*dummy = 1;
return 0;
}
Celui-ci me renvoie une erreur avec GCC 4.8.3. Pourtant celui-ci compile:
int test(const int* dummy)
{
*(char*)dummy = 1;
return 0;
}
Il semble donc que le qualificatif const ne fonctionne que si j'utilise l'argument sans transtyper vers un autre type.
Récemment, j'ai vu des codes qui utilisaient
test(const void* vpointer, ...)
Au moins pour moi, quand j'ai utilisé void *, j'ai tendance à le convertir en char * pour l'arithmétique du pointeur dans les piles ou pour le traçage. Comment const void * peut-il empêcher les fonctions de sous-programme de modifier les données sur lesquelles vpointer pointe?
const int *var;
const
est un contrat. En recevant un const int *
paramètre, vous "dites" à l'appelant que vous (la fonction appelée) ne modifierez pas les objets vers lesquels le pointeur pointe.
Votre deuxième exemple explicitement rompt ce contrat en supprimant le qualificatif const puis en modifiant l'objet pointé par le pointeur reçu. Ne fais jamais ça.
Ce "contrat" est appliqué par le compilateur. *dummy = 1
ne compilera pas. Le cast est un moyen de contourner cela, en disant au compilateur que vous savez vraiment ce que vous faites et que vous vous laissez faire. Malheureusement, le "je sais vraiment ce que je fais" n'est généralement pas le cas.
const
peut également être utilisé par le compilateur pour effectuer une optimisation impossible autrement.
Remarque sur le comportement indéfini:
Veuillez noter que, bien que la distribution elle-même soit techniquement légale, la modification d'une valeur déclarée comme const
est un comportement non défini. Donc, techniquement, la fonction d'origine est correcte, tant que le pointeur qui lui est transmis pointe vers des données déclarées mutables. Sinon, c'est un comportement indéfini.
plus à ce sujet à la fin du post
Quant à la motivation et à l'utilisation, prenons les arguments des fonctions strcpy
et memcpy
:
char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );
strcpy
fonctionne sur les chaînes de caractères, memcpy
fonctionne sur les données génériques. Bien que j'utilise strcpy comme exemple, la discussion suivante est exactement la même pour les deux, mais avec char *
et const char *
pour strcpy
et void *
et const void *
pour memcpy
:
dest
est char *
car dans le tampon dest
la fonction mettra la copie. La fonction modifiera le contenu de ce tampon, donc ce n'est pas const.
src
est const char *
car la fonction ne lit que le contenu du tampon src
. Il ne le modifie pas.
Ce n'est qu'en regardant la déclaration de la fonction qu'un appelant peut affirmer tout ce qui précède. Par contrat, strcpy
ne modifiera pas le contenu du deuxième tampon passé en argument.
const
et void
sont orthogonaux. C'est toute la discussion ci-dessus à propos de const
qui s'applique à tout type (int
, char
, void
, ...)
void *
est utilisé en C pour les données "génériques".
Encore plus sur le comportement indéfini:
Cas 1:
int a = 24;
const int *cp_a = &a; // mutabale to const is perfectly legal. This is in effect
// a constant view (reference) into a mutable object
*(int *)cp_a = 10; // Legal, because the object referenced (a)
// is declared as mutable
Cas 2:
const int cb = 42;
const int *cp_cb = &cb;
*(int *)cp_cb = 10; // Undefined Behavior.
// the write into a const object (cb here) is illegal.
J'ai commencé par ces exemples car ils sont plus faciles à comprendre. De là, il n'y a qu'une seule étape pour faire fonctionner les arguments:
void foo(const int *cp) {
*(int *)cp = 10; // Legal in case 1. Undefined Behavior in case 2
}
Cas 1:
int a = 0;
foo(&a); // the write inside foo is legal
Cas 2:
int const b = 0;
foo(&b); // the write inside foo causes Undefined Behavior
Encore une fois, je dois souligner: à moins que vous ne sachiez vraiment ce que vous faites et que toutes les personnes qui travaillent au présent et à l'avenir sur le code soient des experts et le comprennent, et vous avez une bonne motivation, à moins que toutes les conditions ci-dessus ne soient remplies, - ne jetez jamais la constance !!
int test(const int* dummy) { *(char*)dummy = 1; return 0; }
Non, cela ne fonctionne pas. La suppression de la constance (avec des données véritablement const
) est un comportement indéfini et votre programme se bloquera probablement si, par exemple, l'implémentation met const
données dans la ROM. Le fait que "ça marche" ne change pas le fait que votre code est mal formé.
Au moins pour moi, lorsque j'utilisais void *, j'ai tendance à le convertir en char * pour l'arithmétique des pointeurs dans les piles ou pour le traçage. Comment const void * peut-il empêcher les fonctions de sous-programme de modifier les données vers lesquelles pointe vpointer?
UNE const void*
signifie un pointeur vers certaines données qui ne peuvent pas être modifiées. Pour le lire, oui, vous devez le convertir en types concrets tels que char
. Mais j'ai dit lire , pas écrire , qui, encore une fois, est UB .
Ceci est couvert plus en profondeur ici . C vous permet de contourner entièrement la sécurité de type: c'est à vous d'empêcher cela.
Il est possible qu'un compilateur donné sur un système d'exploitation donné puisse placer certaines de ses données const
dans des pages de mémoire morte. Si tel est le cas, toute tentative d'écriture à cet emplacement échouerait au niveau matériel, par exemple en provoquant une défaillance de protection générale.
Le qualificatif const
signifie simplement que l'écriture est comportement indéfini. Cela signifie que la norme de langue permet au programme de se bloquer si vous le faites (ou autre chose). Malgré cela, C vous permet de vous tirer une balle dans le pied si vous pensez savoir ce que vous faites.
Vous ne pouvez pas empêcher un sous-programme de réinterpréter les bits que vous lui donnez comme il le souhaite et d'exécuter n'importe quelle instruction machine sur ceux-ci. La fonction de bibliothèque que vous appelez peut même être écrite en assembleur. Mais faire cela à un pointeur const
est comportement indéfini, et vous ne voulez vraiment pas invoquer comportement indéfini.
Du haut de ma tête, un exemple rare où cela pourrait avoir du sens: supposons que vous ayez une bibliothèque qui passe autour des paramètres de la poignée. Comment les génère-t-il et les utilise-t-il? En interne, ils peuvent être des pointeurs vers des structures de données. C'est donc une application où vous pourriez typedef const void* my_handle;
afin que le compilateur génère une erreur si vos clients essaient de le déréférencer ou de faire de l'arithmétique par erreur, puis le redirigent vers un pointeur sur votre structure de données dans les fonctions de votre bibliothèque. Ce n'est pas l'implémentation la plus sûre, et vous voulez faire attention aux attaquants qui peuvent transmettre des valeurs arbitraires à votre bibliothèque, mais c'est très peu de frais généraux.