web-dev-qa-db-fra.com

Affectation d'une fonction à un pointeur de fonction, exactitude de l'argument const?

J'apprends les bases du C++ et OOP dans mon université maintenant. Je ne suis pas sûr à 100% du fonctionnement d'un pointeur de fonction lors de leur affectation de fonctions. J'ai rencontré le code suivant:

void mystery7(int a, const double b) { cout << "mystery7" << endl; }
const int mystery8(int a, double b) { cout << "mystery8" << endl; }

int main() {
    void(*p1)(int, double) = mystery7;            /* No error! */
    void(*p2)(int, const double) = mystery7;
    const int(*p3)(int, double) = mystery8;
    const int(*p4)(const int, double) = mystery8; /* No error! */
}

D'après ma compréhension, le p2 et p3 les affectations sont correctes car les types de paramètres de fonction correspondent et la constance est correcte. Mais pourquoi le p1 et p4 les affectations échouent? Ne devrait-il pas être illégal de faire correspondre const double/int à non-const double/int?

16
edwardlam0328

Selon la norme C++ (C++ 17, 16.1 Déclarations surchargeables)

(3.4) - Les déclarations de paramètres qui ne diffèrent que par la présence ou l'absence de const et/ou volatile sont équivalentes. Autrement dit, les spécificateurs de type const et volatile pour chaque type de paramètre sont ignorés lors de la détermination de la fonction qui est déclarée, définie ou appelée.

Ainsi, dans le processus de détermination du type de fonction, le qualificatif const, par exemple du deuxième paramètre de la déclaration de fonction ci-dessous, est rejeté.

void mystery7(int a, const double b);

et le type de fonction est void( int, double ).

Tenez également compte de la déclaration de fonction suivante

void f( const int * const p );

C'est équivalent à la déclaration suivante

void f( const int * p );

C'est le deuxième const qui rend le paramètre constant (c'est-à-dire qu'il déclare le pointeur lui-même comme un objet constant qui ne peut pas être réaffecté à l'intérieur de la fonction). Le premier const définit le type du pointeur. Il n'est pas jeté.

Faites attention à cela, bien que dans la norme C++, le terme "référence const" soit utilisé, les références elles-mêmes ne peuvent pas être constantes à l'opposé des pointeurs. C'est la déclaration suivante

int & const x = initializer;

est incorrect.

Bien que cette déclaration

int * const x = initializer;

est correct et déclare un pointeur constant.

18
Vlad from Moscow

Il existe une règle spéciale pour les arguments de fonction passés par valeur.

Bien que const sur eux affectera leur utilisation à l'intérieur de la fonction (pour éviter les accidents), il est fondamentalement ignoré sur la signature. En effet, la constness d'un objet passé par valeur n'a aucun effet sur l'objet copié d'origine du site d'appel.

Voilà ce que vous voyez.

(Personnellement, je pense que cette décision de conception était une erreur; c'est déroutant et inutile! Mais c'est ce que c'est. Notez que cela vient du même passage qui change silencieusement void foo(T arg[5]); en void foo(T* arg);, donc il y a déjà plein de conneries hokey! t là-dedans que nous devons gérer!)

Rappelez-vous, cependant, que cela n'efface pas seulement anyconst dans le type d'un tel argument. Dans int* const Le pointeur est const, mais dans int const* (Ou const int*) Le pointeur est non -const mais est vers un const chose. Seul le premier exemple concerne constness du pointeur lui-même et sera supprimé.


[dcl.fct]/5 Le type d'une fonction est déterminé à l'aide des règles suivantes. Le type de chaque paramètre (y compris les packs de paramètres de fonction) est déterminé à partir de ses propres decl-specifier-seq et déclarateur. Après avoir déterminé le type de chaque paramètre, tout paramètre de type "tableau de T" ou de type de fonction T est ajusté pour être "pointeur vers T". Après avoir produit la liste des types de paramètres, tout niveau supérieur cv-qualifiers modifiant un type de paramètre sont supprimés lors de la formation du type de fonction . La liste résultante des types de paramètres transformés et la présence ou l'absence des points de suspension ou d'un pack de paramètres de fonction est la fonction liste-type-paramètre de la fonction. [Remarque: Cette transformation n'affecte pas les types de paramètres. Par exemple, int(*)(const int p, decltype(p)*) et int(*)(int, const int*) sont de types identiques. — note finale] =

Il existe une situation où l'ajout ou la suppression d'un qualificatif const à un argument de fonction est un bogue grave. Cela vient quand vous passez un argument par pointeur.

Voici un exemple simple de ce qui pourrait mal se passer. Ce code est cassé en C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// char * strncpy ( char * destination, const char * source, size_t num );

/* Undeclare the macro required by the C standard, to get a function name that
 * we can assign to a pointer:
 */
#undef strncpy

// The correct declaration:
char* (*const fp1)(char*, const char*, size_t) = strncpy;
// Changing const char* to char* will give a warning:
char* (*const fp2)(char*, char*, size_t) = strncpy;
// Adding a const qualifier is actually dangerous:
char* (*const fp3)(const char*, const char*, size_t) = strncpy;

const char* const unmodifiable = "hello, world!";

int main(void)
{
  // This is undefined behavior:
  fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );

  fputs( unmodifiable, stdout );
  return EXIT_SUCCESS;
}

Le problème ici est avec fp3. Il s'agit d'un pointeur vers une fonction qui accepte deux arguments const char*. Cependant, il pointe vers l'appel de bibliothèque standard strncpy() ¹, dont le premier argument est un tampon qu'il modifie. Autrement dit, fp3( dest, src, length ) a un type qui promet de ne pas modifier les données dest pointe vers, mais il transmet ensuite les arguments à strncpy(), qui modifie ces données! Cela n'est possible que parce que nous avons changé la signature de type de la fonction.

Essayer de modifier une constante de chaîne est un comportement non défini - nous avons effectivement dit au programme d'appeler strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") ) - et sur plusieurs compilateurs différents avec lesquels j'ai testé, il échouera silencieusement au moment de l'exécution.

Tout compilateur C moderne devrait autoriser l'affectation à fp1 Mais vous avertir que vous vous tirez dans le pied avec fp2 Ou fp3. En C++, les lignes fp2 Et fp3 Ne se compileront pas du tout sans un reinterpret_cast. L'ajout de la distribution explicite fait que le compilateur suppose que vous savez ce que vous faites et fait taire les avertissements, mais le programme échoue toujours en raison de son comportement non défini.

const auto fp2 =
  reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
// Adding a const qualifier is actually dangerous:
const auto fp3 =
  reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);

Cela ne se produit pas avec des arguments passés par valeur, car le compilateur en fait des copies. Le marquage d'un paramètre passé par la valeur const signifie simplement que la fonction ne s'attend pas à devoir modifier sa copie temporaire. Par exemple, si la bibliothèque standard déclarait en interne char* strncpy( char* const dest, const char* const src, const size_t n ), elle ne pourrait pas utiliser l'idiome K&R *dest++ = *src++;. Cela modifie les copies temporaires des arguments de la fonction, que nous avons déclarées const. Étant donné que cela n'affecte pas le reste du programme, C ne vous dérange pas si vous ajoutez ou supprimez un qualificatif const comme celui-ci dans un prototype de fonction ou un pointeur de fonction. Normalement, vous ne les intégrez pas à l'interface publique dans le fichier d'en-tête, car il s'agit d'un détail d'implémentation.

¹ Bien que j'utilise strncpy() comme exemple d'une fonction bien connue avec la bonne signature, elle est obsolète en général.

1
Davislor