c++ parameters const declaration function-declaration

c++ - Asignación de función a puntero de función, corrección de argumento const?



parameters declaration (3)

Estoy aprendiendo los conceptos básicos de C ++ y OOP en mi universidad ahora. No estoy 100% seguro de cómo funciona un puntero de función cuando se les asigna funciones. Encontré el siguiente código:

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! */ }

A mi entender, las asignaciones de p2 y p3 están bien ya que los tipos de parámetros de función coinciden y la constancia es correcta. Pero, ¿por qué no fallan las asignaciones p1 y p4 ? ¿No debería ser ilegal emparejar const double / int con non-const double / int?


De acuerdo con el estándar de C ++ (C ++ 17, 16.1 declaraciones sobrecargables)

(3.4) - Las declaraciones de parámetros que difieren solo en presencia o ausencia de const y / o volatile son equivalentes. Es decir, los especificadores de tipo const y volatile para cada tipo de parámetro se ignoran cuando se determina qué función se está declarando, definiendo o llamando.

Por lo tanto, en el proceso de determinación del tipo de función, el calificador const, por ejemplo, del segundo parámetro de la siguiente declaración de función se descarta.

void mystery7(int a, const double b);

y el tipo de función es void( int, double ) .

Considera también la siguiente declaración de función.

void f( const int * const p );

Es equivalente a la siguiente declaración.

void f( const int * p );

Es la segunda constante la que hace que el parámetro sea constante (es decir, declara el puntero en sí mismo como un objeto constante que no se puede reasignar dentro de la función). La primera const define el tipo de puntero. No se desecha.

Preste atención a que, aunque en el Estándar de C ++ se usa el término "referencia constante", las referencias en sí mismas no pueden ser opuestas a los punteros. Esa es la siguiente declaración.

int & const x = initializer;

Es incorrecto.

Mientras esta declaración

int * const x = initializer;

Es correcto y declara un puntero constante.


Existe una situación en la que agregar o eliminar un calificador const a un argumento de función es un error grave. Viene cuando pasas un argumento por puntero .

Aquí hay un ejemplo simple de lo que podría salir mal. Este código está roto 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; }

El problema aquí es con fp3 . Este es un puntero a una función que acepta dos argumentos const char* . Sin embargo, apunta a la biblioteca estándar llamada strncpy() , cuyo primer argumento es un búfer que modifica . Es decir, fp3( dest, src, length ) tiene un tipo que promete no modificar los puntos de dest datos, pero luego pasa los argumentos a strncpy() , ¡lo que modifica esos datos! Esto solo es posible porque cambiamos la firma de tipo de la función.

Intentar modificar una constante de cadena es un comportamiento indefinido: efectivamente le dijimos al programa que llamara strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") ) Y en varios compiladores diferentes que probé con , fallará silenciosamente en tiempo de ejecución.

Cualquier compilador de C moderno debería permitir la asignación a fp1 pero le advierte que se está disparando en el pie con fp2 o fp3 . En C ++, las líneas fp2 y fp3 no se compilarán en absoluto sin un reinterpret_cast . Agregar el reparto explícito hace que el compilador asuma que sabes lo que estás haciendo y silencia las advertencias, pero el programa aún falla debido a su comportamiento indefinido.

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

Esto no surge con argumentos pasados ​​por valor, porque el compilador hace copias de ellos. Marcar un parámetro pasado por valor const significa que la función no espera tener que modificar su copia temporal. Por ejemplo, si la biblioteca estándar declara internamente char* strncpy( char* const dest, const char* const src, const size_t n ) , no podrá utilizar el lenguaje K&R *dest++ = *src++; . Esto modifica las copias temporales de la función de los argumentos, que const . Como esto no afecta al resto del programa, a C no le importa si agrega o elimina un calificador const como el de un prototipo de función o un puntero de función. Normalmente, no los hace parte de la interfaz pública en el archivo de encabezado, ya que son un detalle de implementación.

¹ Aunque utilizo strncpy() como ejemplo de una función conocida con la firma correcta, en general está en desuso.


Hay una regla especial para los argumentos de función pasados ​​por valor.

Aunque la const sobre ellos afectará su uso dentro de la función (para evitar accidentes), básicamente se ignora en la firma. Esto se debe a que la const de un objeto pasado por valor no tiene ningún efecto sobre el objeto original copiado desde el sitio de llamada.

Eso es lo que estás viendo.

(Personalmente, creo que esta decisión de diseño fue un error. ¡Es confuso e innecesario! Pero es lo que es. Tenga en cuenta que proviene del mismo pasaje que cambia silenciosamente void foo(T arg[5]); en void foo(T* arg); por lo tanto, ¡hay un montón de tonterías! ¡Ya tenemos que lidiar con eso!)

Sin embargo, recuerde que esto no solo borra cualquier const en el tipo de argumento. En int* const el puntero es const , pero en int const* (o const int* ) el puntero es non- const pero es una cosa const . Solo el primer ejemplo se refiere a la const del puntero en sí y se eliminará.

[dcl.fct]/5 El tipo de una función se determina utilizando las siguientes reglas. El tipo de cada parámetro (incluidos los paquetes de parámetros de función) se determina a partir de su propio decl-specifier-seq y declarator. Después de determinar el tipo de cada parámetro, cualquier parámetro del tipo "matriz de T " o del tipo de función T se ajusta para que sea "puntero a T ". Después de generar la lista de tipos de parámetros, todos los calificadores cv de nivel superior que modifican un tipo de parámetro se eliminan al formar el tipo de función . La lista resultante de tipos de parámetros transformados y la presencia o ausencia de puntos suspensivos o un paquete de parámetros de funciones es la lista de tipos de parámetros de la función. [Nota: esta transformación no afecta los tipos de los parámetros. Por ejemplo, int(*)(const int p, decltype(p)*) e int(*)(int, const int*) son tipos idénticos. - nota final]