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 deT
" o del tipo de funciónT
se ajusta para que sea "puntero aT
". 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)*)
eint(*)(int, const int*)
son tipos idénticos. - nota final]