将函数分配给函数指针,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! */
}

根据我的理解,p2p3 赋值没问题,因为函数参数类型匹配且常量性正确。但是为什么 p1p4 赋值不会失败呢?将 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会影响它们在函数内部的使用(以防意外),但在签名上基本忽略了。这是因为按值传递的对象的 constness 对调用站点的原始 copied-from 对象没有任何影响。

这就是您所看到的。

(我个人认为这个设计决定是一个错误;它令人困惑且没有必要!但事实就是如此。请注意,它来自同一段落,将 void foo(T arg[5]); 默默地更改为 void foo(T* arg);, 所以有很多胡说八道!我们必须处理的已经有很多了!)

不过请记住,这不只是擦除此类参数类型中的 any const。在 int* const 中,指针是 const,但在 int const*(或 const int*)中,指针不是 const,而是指向 const 的东西.只有第一个示例与指针本身的 constness 有关,将被删除。


[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 的赋值,但警告您使用 fp2fp3 是搬起石头砸自己的脚。在 C++ 中,如果没有 reinterpret_castfp2fp3 行根本无法编译。添加显式强制转换会使编译器假设您知道自己在做什么并消除警告,但由于其未定义的行为,程序仍然失败。

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 函数的示例,但它通常已被弃用。