【问题标题】:Why are redundant class name qualifiers allowed?为什么允许冗余类名限定符?
【发布时间】:2023-04-07 11:01:01
【问题描述】:

我遇到了一些这样的代码:

struct A {
    A() {}
    A(int) {}
};

struct B : A {
    void init(int i);
};

void B::init(int i) {
    A::A(i); // what is this?
}

int main() {
    B b;
    b.init(2);
}

这使用 VC11 beta 编译和运行,没有 /W4 的错误或警告。

明显的意图是调用 B::init 来重新初始化 B 的 A 基础子对象。我相信它实际上解析为一个名为i 类型为A 的新变量的变量声明。使用 clang 编译会产生诊断:

ConsoleApplication1.cpp:11:14: warning: declaration shadows a local variable
        A::A(i);
             ^
ConsoleApplication1.cpp:10:22: note: previous declaration is here
    void B::init(int i) {
                     ^
ConsoleApplication1.cpp:11:14: error: redefinition of 'i' with a different type
        A::A(i);
             ^
ConsoleApplication1.cpp:10:22: note: previous definition is here
    void B::init(int i) {
                     ^

这个类型可以用多余的类限定来引用,这似乎很奇怪。

另外,A::A(i) 似乎被 VS11 和 clang/gcc 解析不同。如果我做A::A(b) clang 和 gcc 使用默认构造函数创建一个类型为A 的变量b。 VS11 错误指出 b 是未知标识符。 VS11 似乎将A::A(i) 解析为使用构造函数A::A(int)i 作为参数创建临时A。当多余的限定符被消除时,VS 像 clang 和 gcc 一样将源解析为变量声明,并产生关于隐藏变量 i 的类似错误。

这种解析上的差异解释了为什么 VS11 会阻塞多个额外的限定符; A::A::A::A(i),以及为什么鉴于 clang 和 gcc 可以接受一个额外的限定符,任何多于一个的数字与一个额外的结果相同。

这是另一个在不同上下文中使用冗余限定符的示例。所有编译器似乎都将其解析为临时构造:

class Foo {};

void bar(Foo const &) {}

int main() {
    bar(Foo::Foo());
}
  1. 为什么完全允许冗余限定符?
  2. 在某些上下文中可以引用构造函数,例如继承构造函数的语法 (class D : B { using B::B; };),但 VS 似乎允许它在任何地方使用。 VS 错了吗,clang 和 gcc 在冗余限定符的解析方式上是否正确?
  3. 我知道 VS 在标准合规性方面仍然落后了一些,但我确实发现现代、积极开发的编译器可能如此不同,在这种情况下将冗余限定符解析为构造函数的名称,这有点令人惊讶(即使构造函数没有名称)与简单地将冗余限定符解析为类型,导致 VS 在其他人声明变量的地方构造一个临时变量。如果 B b(A::A(i)); 被 clang 和 gcc 解析为最令人头疼的解析,情况会变得更糟,但 VS 将其视为使用初始化程序声明 B 类型的变量 b。这么严重的差异还有很多吗?
  4. 显然,在可移植代码中应避免冗余限定符。有什么好的方法可以防止这种结构被使用吗?

【问题讨论】:

  • FWIW,Comeau 同意 MSVC:A::A(j) 是一个构造函数调用。 IIRC 这是基于名称 A 被注入到 A 的类命名空间中,并且引用了 A 类型(不是构造函数)。但是TypeIdentifier(j) 只是编写 C 风格转换(TypeIdentifier)j 的另一种方式,因此它实际上调用了构造函数。此外,Comeau 报告“构造函数或析构函数可能没有获取其地址”,我不明白。
  • @SteveJessop 但是如果A::A 指的是注入到类命名空间A 中的类型名称A 不是A::A 指的是类型吗?然后A::A i;必须是变量声明,A::A(i);也必须是变量声明?
  • 啊,因为如果有疑问,这是一个声明。好吧,我当然很困惑。如果/Comeau 弄错了,我会感到惊讶。

标签: c++ language-lawyer


【解决方案1】:

虽然这种现象可能归因于 类名注入,正如 ehemient 的回答中所指出的,但对于这个特定的示例,它在很久以前就被 C++ 语言禁止了。

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#147

A::A 的组合需要引用类构造函数,而不是类注入名称。 A::A(i) 应该被兼容的编译器解释为涉及构造函数名称的非法(因此无意义)表达式。例如,Comeau 编译器会因为这个原因拒绝编译您的代码。

显然 VC11 继续将A::A 视为对注入类名的引用。有趣的是,我在 VS2005 中没有观察到这个问题。

A::A 被解释为引用注入名称的那一天,人们可以将A 对象声明为

A::A::A::A::A::A a;

以此类推,任意数量的As。但现在不是了。令人惊讶的是,ideone 使用的 GCC(4.3.4?)版本仍然存在这个问题

http://ideone.com/OkR0F

你可以用你的 VC11 版本试试这个,看看它是否允许。

【讨论】:

  • §3.4.3.1/2 将其限定为“在构造函数是可接受的查找结果的查找中... [ 注意: 例如,构造函数不是详细说明类型说明符中的可接受查找结果,因此不会使用构造函数代替注入的类名。]”,我认为这违背了这一点?
  • @ephemient 是否在任何地方定义了“可接受的查找结果”?例如,A::A 的构造函数查找结果是否可能导致代码不被解析为 elaborated-type-specifier,而是导致它被解析为对构造函数?如果发生这种情况,那会使构造函数成为“可接受的查找结果”吗?好像那里有点循环......
  • @ephemient:不是真的,不知道在哪个上下文中它是“可接受的查找结果”。标准的那部分中的示例清楚地表明A::A a 是错误的,因为A::A 指的是构造函数。请参阅:它会导致错误,但它被认为是“可接受的查找结果”。如果使用复杂的类型说明符是不可接受的,但目前我还不清楚还有哪些其他情况。
  • 你仍然可以跳 A::A::A::...,只要最后的 A 也使用忽略函数的查找形式(就像查找 :: 之前的名称一样)。例如struct A::A::A::A a; 可以对结构A 有效。
【解决方案2】:

来自 ISO/IEC 14882:2011 最终草案 §9,

² class-name 在看到 class-name 之后立即插入到声明它的范围内。 class-name 也被插入到类本身的范围内,这被称为 injected-class-name

当你写作时

A::A(i);

和声明一样

A i;

因为多余的括号是多余的(您可以添加任意数量),而A::A 指的是A


从 §14.6.1,

¹ 与普通(非模板)类一样,类模板具有注入类名称(第 9 条)。注入的类名可以用作 template-nametype-name。当它与 template-argument-list 一起使用时,作为模板 template-parametertemplate-argument,或作为最终标识符在友元类模板声明的elaborated-type-specifier中,它指的是类模板本身。否则,它相当于 template-name 后跟 <> 中包含的类模板的 template-parameters

注入的类名似乎是为了方便在类中将A<...> 简称为A

【讨论】:

  • 注入的类名在外部可见,这对我来说似乎很有害。此外,它使继承的构造函数的语法有点不协调。
  • 这个答案很好,但我现在不接受,因为我不想阻止其他人提供可能最终更彻底的答案。
  • 虽然这个答案是有道理的,但 5.1.1/8 说:Where class-name :: class-name is used, and the two class-names refer to the same class, this notation names the constructor 这似乎在说一些不同的东西。
猜你喜欢
  • 1970-01-01
  • 2010-09-11
  • 2021-11-25
  • 1970-01-01
  • 1970-01-01
  • 2017-05-23
  • 1970-01-01
  • 2012-08-21
  • 2013-05-08
相关资源
最近更新 更多