将函数分配给函数指针,const 参数是否正确?
Assigning function to function pointer, const argument correctness?
我现在在大学里学习 C++ 和 OOP 的基础知识。我不是 100% 确定函数指针在为它们分配函数时如何工作。我遇到了以下代码:
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! */
}
根据我的理解,p2
和 p3
赋值没问题,因为函数参数类型匹配且常量性正确。但是为什么 p1
和 p4
赋值不会失败呢?将 const double/int 与非常量 double/int 匹配不应该是非法的吗?
根据 C++ 标准(C++ 17、16.1 可重载声明)
(3.4) — Parameter declarations that differ only in the presence or
absence of const and/or volatile are equivalent. That is, the const
and volatile type-specifiers for each parameter type are ignored when
determining which function is being declared, defined, or called.
因此在确定函数类型的过程中,下面函数声明的第二个参数的限定符 const 被丢弃。
void mystery7(int a, const double b);
函数类型为void( int, double )
。
还要考虑下面的函数声明
void f( const int * const p );
相当于下面的声明
void f( const int * p );
使参数成为常量的是第二个const(即将指针本身声明为常量对象,不能在函数内部重新赋值)。第一个 const 定义指针的类型。它没有被丢弃。
注意,尽管在 C++ 标准中使用了术语 "const reference",但引用本身不能与指针相反。也就是下面的声明
int & const x = initializer;
不正确。
虽然这个声明
int * const x = initializer;
正确并声明了一个常量指针。
按值传递的函数参数有一条特殊规则。
虽然对它们const
会影响它们在函数内部的使用(以防意外),但在签名上基本忽略了。这是因为按值传递的对象的 const
ness 对调用站点的原始 copied-from 对象没有任何影响。
这就是您所看到的。
(我个人认为这个设计决定是一个错误;它令人困惑且没有必要!但事实就是如此。请注意,它来自同一段落,将 void foo(T arg[5]);
默默地更改为 void foo(T* arg);
, 所以有很多胡说八道!我们必须处理的已经有很多了!)
不过请记住,这不只是擦除此类参数类型中的 any const
。在 int* const
中,指针是 const
,但在 int const*
(或 const int*
)中,指针不是 const
,而是指向 const
的东西.只有第一个示例与指针本身的 const
ness 有关,将被删除。
[dcl.fct]/5
The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator. After determining the type of each parameter, any parameter of type “array of T
” or of function type T
is adjusted to be “pointer to T
”. After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function's parameter-type-list. [ Note: This transformation does not affect the types of the parameters. For example, int(*)(const int p, decltype(p)*)
and int(*)(int, const int*)
are identical types. — end note ]
在某些情况下,向函数参数添加或删除 const
限定符是一个严重的错误。当你通过指针传递参数.
时它会出现
这是一个可能出错的简单示例。此代码在 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;
}
这里的问题是 fp3
。这是一个指向接受两个 const char*
参数的函数的指针。但是,它指向标准库调用 strncpy()
¹,其第一个参数是它 修改 的缓冲区。也就是说,fp3( dest, src, length )
的类型承诺不修改 dest
指向的数据,但随后它将参数传递给 strncpy()
,后者修改了该数据!这是可能的,因为我们更改了函数的类型签名。
尝试修改字符串常量是未定义的行为——我们有效地告诉程序调用 strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )
——在我测试过的几个不同的编译器上,它会在运行时默默地失败。
任何现代 C 编译器都应该允许对 fp1
的赋值,但警告您使用 fp2
或 fp3
是搬起石头砸自己的脚。在 C++ 中,如果没有 reinterpret_cast
,fp2
和 fp3
行根本无法编译。添加显式强制转换会使编译器假设您知道自己在做什么并消除警告,但由于其未定义的行为,程序仍然失败。
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);
按值传递的参数不会出现这种情况,因为编译器会复制这些参数。标记按值传递的参数 const
仅表示该函数不需要修改其临时副本。例如,如果标准库内部声明了 char* strncpy( char* const dest, const char* const src, const size_t n )
,它就不能使用 K&R 习语 *dest++ = *src++;
。这修改了我们声明的函数参数的临时副本 const
。由于这不会影响程序的其余部分,因此 C 不会介意您是否像在函数原型或函数指针中那样添加或删除 const
限定符。通常,您不会将它们作为头文件中 public 接口的一部分,因为它们是实现细节。
¹ 虽然我使用 strncpy()
作为具有正确签名的 well-known 函数的示例,但它通常已被弃用。
我现在在大学里学习 C++ 和 OOP 的基础知识。我不是 100% 确定函数指针在为它们分配函数时如何工作。我遇到了以下代码:
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! */
}
根据我的理解,p2
和 p3
赋值没问题,因为函数参数类型匹配且常量性正确。但是为什么 p1
和 p4
赋值不会失败呢?将 const double/int 与非常量 double/int 匹配不应该是非法的吗?
根据 C++ 标准(C++ 17、16.1 可重载声明)
(3.4) — Parameter declarations that differ only in the presence or absence of const and/or volatile are equivalent. That is, the const and volatile type-specifiers for each parameter type are ignored when determining which function is being declared, defined, or called.
因此在确定函数类型的过程中,下面函数声明的第二个参数的限定符 const 被丢弃。
void mystery7(int a, const double b);
函数类型为void( int, double )
。
还要考虑下面的函数声明
void f( const int * const p );
相当于下面的声明
void f( const int * p );
使参数成为常量的是第二个const(即将指针本身声明为常量对象,不能在函数内部重新赋值)。第一个 const 定义指针的类型。它没有被丢弃。
注意,尽管在 C++ 标准中使用了术语 "const reference",但引用本身不能与指针相反。也就是下面的声明
int & const x = initializer;
不正确。
虽然这个声明
int * const x = initializer;
正确并声明了一个常量指针。
按值传递的函数参数有一条特殊规则。
虽然对它们const
会影响它们在函数内部的使用(以防意外),但在签名上基本忽略了。这是因为按值传递的对象的 const
ness 对调用站点的原始 copied-from 对象没有任何影响。
这就是您所看到的。
(我个人认为这个设计决定是一个错误;它令人困惑且没有必要!但事实就是如此。请注意,它来自同一段落,将 void foo(T arg[5]);
默默地更改为 void foo(T* arg);
, 所以有很多胡说八道!我们必须处理的已经有很多了!)
不过请记住,这不只是擦除此类参数类型中的 any const
。在 int* const
中,指针是 const
,但在 int const*
(或 const int*
)中,指针不是 const
,而是指向 const
的东西.只有第一个示例与指针本身的 const
ness 有关,将被删除。
[dcl.fct]/5
The type of a function is determined using the following rules. The type of each parameter (including function parameter packs) is determined from its own decl-specifier-seq and declarator. After determining the type of each parameter, any parameter of type “array ofT
” or of function typeT
is adjusted to be “pointer toT
”. After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type. The resulting list of transformed parameter types and the presence or absence of the ellipsis or a function parameter pack is the function's parameter-type-list. [ Note: This transformation does not affect the types of the parameters. For example,int(*)(const int p, decltype(p)*)
andint(*)(int, const int*)
are identical types. — end note ]
在某些情况下,向函数参数添加或删除 const
限定符是一个严重的错误。当你通过指针传递参数.
这是一个可能出错的简单示例。此代码在 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;
}
这里的问题是 fp3
。这是一个指向接受两个 const char*
参数的函数的指针。但是,它指向标准库调用 strncpy()
¹,其第一个参数是它 修改 的缓冲区。也就是说,fp3( dest, src, length )
的类型承诺不修改 dest
指向的数据,但随后它将参数传递给 strncpy()
,后者修改了该数据!这是可能的,因为我们更改了函数的类型签名。
尝试修改字符串常量是未定义的行为——我们有效地告诉程序调用 strncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )
——在我测试过的几个不同的编译器上,它会在运行时默默地失败。
任何现代 C 编译器都应该允许对 fp1
的赋值,但警告您使用 fp2
或 fp3
是搬起石头砸自己的脚。在 C++ 中,如果没有 reinterpret_cast
,fp2
和 fp3
行根本无法编译。添加显式强制转换会使编译器假设您知道自己在做什么并消除警告,但由于其未定义的行为,程序仍然失败。
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);
按值传递的参数不会出现这种情况,因为编译器会复制这些参数。标记按值传递的参数 const
仅表示该函数不需要修改其临时副本。例如,如果标准库内部声明了 char* strncpy( char* const dest, const char* const src, const size_t n )
,它就不能使用 K&R 习语 *dest++ = *src++;
。这修改了我们声明的函数参数的临时副本 const
。由于这不会影响程序的其余部分,因此 C 不会介意您是否像在函数原型或函数指针中那样添加或删除 const
限定符。通常,您不会将它们作为头文件中 public 接口的一部分,因为它们是实现细节。
¹ 虽然我使用 strncpy()
作为具有正确签名的 well-known 函数的示例,但它通常已被弃用。