【问题标题】:C complains about passing char** value to function taking char const*const*const but C++ doesn'tC 抱怨将 char** 值传递给采用 char const*const*const 的函数,但 C++ 没有
【发布时间】:2013-12-19 23:20:11
【问题描述】:

在解释和创建函数参数的类型时,我很难理解为什么 C++ 比 C 表现得更“宽松”。

C 做了世界上最简单的事情,它坚持你所写的,仅此而已,另一方面,C++ 以一种我无法真正理解的扭曲方式运行。

例如,流行的argvchar* [],当传递给函数时变成char**,我真的不明白为什么,我期望和“想要”的是char * const *,但我得到了这种行为而是。

您也可以阅读this article in PDF 讨论C 和C++ 之间的这种差异,文章也以这句话结尾:

尽管 C++ 忽略了参数中的顶级 cv 限定符 确定函数签名时的声明,它不 完全忽略那些 cv 限定符。

由于我在网上找不到这个问题(嵌入式系统编程 - 2000 年 2 月,而且这个旧问题是免费的),我想知道这句话可能意味着什么。

有人可以解释为什么这种行为在 C++ 中是这样的吗?

编辑:

我的一个例子是

#include <stdio.h>

void foo(int argc, const char *const *const argv) {
  printf("%d %s\n", argc, argv[0]);
}

int main(int argc, char *argv[]) {
  foo(argc, argv);
  return (0);
}

如果你用 gcc 4.8.1 编译它,你会得到预期的错误

gcc cv_1.c 
cv_1.c: In function ‘main’:
cv_1.c:8:3: warning: passing argument 2 of ‘foo’ from incompatible pointer type [enabled by default]
   foo(argc, argv);
   ^
cv_1.c:3:6: note: expected ‘const char * const* const’ but argument is of type ‘char **’
 void foo(int argc, const char *const *const argv) {
      ^

这个输出隐含了argv被解释为char**的事实

【问题讨论】:

  • 您的 argv 示例实际上是 C++ 从 C 继承而来的。您的 PDF 讨论了 签名,它们仅对重载决议很重要。与简历检查无关。
  • @HansPassant 对于我所关心的,如果它存在于顶层,这个机制仍然会丢弃我的const,我想知道这种奇怪行为背后的基本原理是什么。 argv 在这个例子中只是一个玩具。
  • 请用一个具体的例子来幽默我们。
  • @user2485710:在这方面,C 和 C++ 之间绝对没有区别。在这两种语言中,顶级 cv 限定符对函数签名没有影响。在这两种语言中,char *[] 在函数参数声明中变为 char **。你是从哪里得到有任何想法的?
  • @user2485710:哦,我明白了,链接的 PDF 文章中说...那篇文章绝对不正确。

标签: c++ c constants volatile qualifiers


【解决方案1】:

函数参数可以通过值或引用传递。在引用的情况下,没有顶级限定符,因此我们可以忽略这种情况。

在按值参数的情况下,顶级限定符仅影响副本,并且完全独立于用于复制构造该参数的原始限定符。如果没有从签名中删除顶级限定符,则以下两个函数将是有效的并且不同的重载:

void f(int       i);
void f(int const i);

现在的问题是,调用f(1) 应该选择两个重载中的哪一个?这里的问题是参数是否为 const 不会影响它可以从什么构造,因此编译器将永远无法解决哪个是正确的重载。解决方案很简单:在签名中删除顶级限定符,两者是相同的功能。

【讨论】:

  • 我不明白,对我来说关键步骤是,一个命名变量可能有一个 cv 限定符,从你所说的看起来它只是为了保存临时值制造问题,看起来需要权衡取舍;但是您可以参考 C++ 标准中的某些特定段落吗?
  • @user2485710:不,问题不在于临时对象,而在于重载解决方案。如果需要,请将1 替换为上面的命名变量。顶级限定符适用于函数的参数。 const 或非 const 对象的创建方式完全相同,构造函数(或转换函数)的参数集相同。顶级const 与调用者无关,编译器不能使用它来区分一个调用与另一个调用。选择一个函数,添加/删除顶级限定符,考虑哪些调用对任一签名有效:相同。
【解决方案2】:

您链接的 PDF 文章包含许多关于 C 和 C++ 在处理顶级 cv 限定符方面差异的错误陈述。这些差异要么不存在,要么与文章中暗示的内容不同。

实际上,在确定函数签名和函数类型时,C 和 C++ 都有效地忽略函数参数声明中的顶级 cv 限定符。 C 和 C++ 语言标准(以及底层机制)中的措辞在概念上可能不同,但两种语言的最终结果是相同的。

C++ 在确定函数类型时确实直接忽略了参数上的顶级 cv 限定符,如 8.3.5/5 中所述:“在生成参数类型列表后,任何顶级 cv 限定符修改在形成函数类型时,参数类型被删除。”

C 依赖于 C 特有的 兼容类型 概念,而不是直接忽略这些限定符。它表示仅在参数上的顶级 cv 限定符不同的函数类型是兼容的,这意味着它们是相同的。 6.7.5.3/15 中函数类型兼容性的定义说:“在确定类型兼容性和复合类型时,[...] 每个用限定类型声明的参数都被视为具有 其声明类型的不合格版本。”

链接的 PDF 文章指出,在 C 中,以下声明序列是非法的

void foo(int i);
void foo(const int i);

实际上它在 C 中是完全合法的。C 只要求同一范围内同一实体的所有声明都使用兼容类型 (6.7/4)。上面的两个声明是兼容的,这意味着它们只是合法地重新声明了相同的函数。 (在 C++ 中,上述声明也是合法的,它们也重新声明了相同的功能。)

对于其他示例,在 C 和 C++ 中,以下初始化都是有效的

void foo(const int i);
void bar(int i);

void (*pfoo)(int) = foo;       // OK
void (*pbar)(const int) = bar; // OK

同时,在确定函数参数的局部变量类型时,C 和 C++同样考虑顶级 cv 限定符。例如,在 C 和 C++ 中,以下代码都是格式错误的

void foo(const int i) {
  i = 5; // ERROR!
}

在 C 和 C++ 中,在其参数上使用一个顶级 cv 限定声明的函数稍后可以使用其参数的完全不同的 cv 限定来定义。顶级 cv 限定的任何差异都不构成 C++ 中的函数重载。


此外,您反复提到 char *[] 被解释为 char ** 是相关的。我没有看到相关性。在函数参数列表中,T [] 声明始终等同于 T * 声明。但这与顶级 cv-qualifiers 完全无关。

同时,由于与顶级 cv 限定符无关的原因,您编辑中的代码示例也无法编译。它无法编译,因为在 C 语言中没有从 char **const char *const * 的隐式转换。请注意,此转换不涉及也不关心任何顶级 cv 限定符。影响此转换的const 限定符仅出现在第一和第二级间接寻址上。

这确实涉及 C 和 C++ 之间的差异。在 C 和 C++ 中,从 char **const char ** 的隐式转换都是不允许的(例如,请参见 here)。但是,C++ 允许从 char **const char *const * 的隐式转换,而 C 仍然不允许。你可以阅读更多关于它的信息here。但请再次注意,在所有这些情况下,顶级 cv 限定符是完全不相关的。他们根本没有任何作用。

【讨论】:

  • 标准中描述这一点的段落是什么?模板也是如此?
  • 我敢打赌。至于文章,它已经很老了。在 2000 年之前的几年里,我周围的人仍然乐于使用在 DOS 环境下工作的 IIRC “Turbo C++ 32” 编译器,但在涉及到所有重要部分时都会遇到很多问题。也许文章的作者只是使用了类似的东西..
【解决方案3】:

这是一个猜测,但原因是带有限定符的函数参数只是参数的副本。考虑:

void foo(int * const a, volatile int b) { … }

这些限定符的意思是函数定义中的代码不会修改 a(因为它是 const),并且 b 的值可以以 C++ 实现未知的方式访问。 (这很奇怪;易失性对象通常是硬件寄存器之类的东西,或者可能是进程之间共享的数据。但是假设我们正在调试一个问题,所以我们暂时标记了b volatile 以确保我们可以在调试器。)

C++ 实现在编译和执行定义foo 的代码时必须遵守ab 上的这些限定符,因此它不能忽略这些限定符。

但是,考虑调用者对foo 的看法。 fooa 视为 const 或 b 视为 volatile 的事实与调用者无关。它指定的任何参数都被复制(例如,寄存器或堆栈)以传递给foo。它所做的只是传递值。如果foo 的声明没有限定符:

void foo(int *a, int b) { … }

那么调用者的行为不会改变:无论哪种方式,它只是传递参数的值并调用foo。因此,foo 的这两个声明在调用者看来是相同的,因此它们具有相同的签名。

【讨论】:

  • 所以,简而言之,您是说可观察行为没有变化(对于 foo 之外的内容),这足以重新考虑这个问题的严重性并说也许它的方式就足够了?
【解决方案4】:
void foo( char const * const * const) {}
void bar( char *x[]) {
  foo(x); // warning in C, nothing in C++
}

将此示例编译为 C 会产生警告但 C++ 不会产生任何诊断的原因不是因为 C 和 C++ 将 char *[] 视为不同的类型,或者因为它们在不同的地方丢弃或插入 const ,但仅仅是因为 C 和 C++ 定义“兼容指针类型”的方式不同; C++ 放宽了规则,因为 C 的严格规则并不能防止真正的错误。

考虑一下:对于 char const * const * const 不合法的 char **,您究竟能做什么?由于无法进行任何修改,因此不可能引入任何错误,因此这样的限制没有什么价值。

但这并不是说插入consts 不允许可能产生错误的代码。例如:

void foo(char const **c) { *c = "hello"; }

void bar(char **c) {
  foo(c);
  **c = 'J';
}

如果允许,上面的代码会写入一个字符串常量,这是非法的。

C++ 仔细定义了不兼容的指针类型,因此上述情况是不允许的,同时仍然放宽了 C 的规则,以允许比 C 更安全的程序。

C 规则的一个优点是它们非常简单。基本上:

为了使两种指针类型兼容,两者都应具有相同的限定,并且都应 是指向兼容类型的指针。

对于任何限定符 q,指向非 q 限定类型的指针可以转换为指向 该类型的 q 限定版本;存储在原始指针和转换指针中的值 应该比较相等。

另一方面,C++ 的规则适用于多个段落并使用复杂的定义来准确指定允许的指针转换。兔子洞从 C++11 4.4 [conv.qual] 第 4 段开始。


我想知道这句话可能意味着什么。

他很可能指的是,如果在定义函数时将参数声明为 const,那么编译器将不允许函数定义对参数执行非常量操作。

void foo(int x);
void bar(int x);

void foo(int const x) {
  ++x; // error, parameter is const
}

void bar(int x) {
  ++x; // okay, parameter is modifiable.
}

【讨论】:

  • 我仍然不清楚为什么argv 被编译器解释为char**(请参阅我对第一篇文章的编辑示例),因为它是char* [],这几乎是我开始尝试这个的地方。
  • @user2485710 你关心C中char *[]char**参数有什么区别?
  • 其中aint a[],我看到aint * const a,所以char**argv,这真的不是我期望看到的。
  • void foo(int a[]) { ++a; } 在 C AFAICT 中是合法的,而 void foo(int * const a) { ++a; } 是不合法的。您能否编写一个完整的程序,通过在编译为 C++ 与 C 时产生不同的输出来显示您所关注的差异?如果不同的输出是一种语言或另一种语言的编译器错误,那也没关系。确保将int [] 参数替换为显式int * constint * 时程序产生相同的输出。
  • +1 以获得良好的答案,并在答案顶部将 OP 代码呈现为非常简化的 foo() bar()。有助于专注于问题。
【解决方案5】:

意见太大,无法发表评论。

只有 char const * const * const x 中的第一个 const 会引发 C 警告。
C++ (Visual) 抱怨 8 个中的 2 个。不知道为什么?

恕我直言:从调用函数的角度来看,这两种语言都没有区别 const

void fooccc( char const * const * const x) { if(x) return; }
void foocc_( char const * const *       x) { if(x) return; }
void fooc_c( char const *       * const x) { if(x) return; }
void fooc__( char const *       *       x) { if(x) return; }
void foo_cc( char       * const * const x) { if(x) return; }
void foo_c_( char       * const *       x) { if(x) return; }
void foo__c( char       *       * const x) { if(x) return; }
void foo___( char       *       *       x) { if(x) return; }

int g(char *x[]) {
  fooccc(x); // warning in C passing argument 1 of 'fooccc' from incompatible pointer type
  foocc_(x); // warning in C "
  fooc_c(x); // warning in C "   error in C++ cannot convert parameter 1 from 'char *[]' to 'const char **const ' Conversion loses qualifiers
  fooc__(x); // warning in C "   error in C++ cannot convert parameter 1 from 'char *[]' to 'const char **'       Conversion loses qualifiers
  foo_cc(x); // no problem in C  no problem in C++
  foo_c_(x); // no problem in C  no problem in C++
  foo__c(x); // no problem in C  no problem in C++
  foo___(x); // no problem in C  no problem in C++
  return 0;
  }

注意事项:Eclipse,gcc -std=c99 -O0 -g3 -Wall
C++ Visual Studio 10.0

【讨论】:

  • 我相信 C++ 规则归结为:在任何级别插入 const 都需要在所有更高级别插入 const,除了无关紧要的顶层。
  • @bames53 如果您可以访问另一个 C++ 编译器,我们将不胜感激,请报告您的发现和编译器信息。
  • isocpp.org 网站上列出了几个在线编译器,但是我没有广泛地测试实现,我只是在阅读 C++ 规范。您在这里的发现与我的阅读和一些快速测试(clang 和 vc++)相匹配
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-20
  • 1970-01-01
  • 1970-01-01
  • 2013-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多