为什么不能在C++中重新定义类中的类型名称?

Why can't redefine type names in class in C++?

本文关键字:类型 定义 不能 C++ 新定义 为什么      更新时间:2023-10-16

根据本书C++入门部分,7.4.1 类型名称是特殊的:

通常,内部作用域

可以从外部作用域重新定义名称,即使该名称已在内部作用域中使用。但是,在类中,如果成员使用外部作用域中的名称,并且该名称是类型,则该类随后可能不会重新定义该名称。

因此,例如:

typedef double Money;
class Account {
public:
Money balance() { return bal; }
private:
typedef double Money;
Money bal;
};
int main() {
typedef double Money;
Money asset;
typedef double Money;
return 0;
}

当你编译上面的例子时,它会抱怨:

a.cc:6:24: error: declaration of ‘typedef double Account::Money’ [-fpermissive]
typedef double Money;
^
a.cc:1:16: error: changes meaning of ‘Money’ from ‘typedef double Money’ [-fpermissive]
typedef double Money;

那么为什么我们不能在类中重新定义类型名称,而我们可以在内部作用域中重新定义类型名称呢?


我的编译器版本是g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609.
该部分中还有一个注释:

尽管重新定义类型名称是一个错误,但编译器不需要诊断此错误。一些编译器会悄悄地接受这样的代码,即使程序出错了。

这不是类型所独有的。[基本.class.范围]/2:

N中使用的名称S应在其 上下文,并在已完成的S范围内重新评估时。不 违反此规则需要诊断。

原因是类范围中的名称查找有点特殊。考虑:

using Foo = int;
struct X {
Foo a;    // ::Foo, i.e., int
void meow() { 
Foo b = a; // X::Foo; error: no conversion from int to char*
}
using Foo = char*;
};

成员函数体中的名称查找会考虑所有类成员,无论是在成员函数之前还是之后声明(否则,类定义中定义的成员函数将无法使用类中稍后声明的数据成员)。结果是你会得到两个具有不同含义的Foo,即使它们在词法上都位于类成员Foo声明之前。这很容易导致代码非常混乱和脆弱,因此标准禁止它。

当编译器读取该行时

Money balance() { return bal; }

在类定义中,它已经使用了类外Money的定义。这使得这条线

typedef double Money;

类内有问题。但是,在类中使用之前,可以在类中使用重定义Money。以下就可以了。

typedef double Money;
class Account {
public:
typedef double Money;
Money balance() { return bal; }
private:
Money bal;
};

引文中的关键点是:

因此,该类随后可能不会重新定义该名称。

我想尝试回答您评论中的一些问题。

评论 1

"但是在函数 main 中,我们可以重新定义 typedef double Money,即使它是在语句 Money asset 之后定义的">

所以你在问,为什么typedef标识符可以在非成员函数(在非类范围内)中多次定义?

答案在这里:重复的typedefs - 在C中无效,但在C++中有效?

评论2

因此,在这个例子中,两个具有不同含义的 Foos 在词法上都位于语句 Foo b = a in 函数 meow 之前。然后编译器无法确定 b 的类型。正确与否?

编译器可以确定该代码块中 b 的类型。B的类型是遗忘char*而a的类型是int

虽然两个具有不同含义的Foo在词法上都位于语句之前,Foo b = a在函数meow中,Foo定义为int先于Foo定义为char*。书中说名称查找过程不同:

• 首先,编制成员声明。
• 函数体只有在看到整个类之后才会编译。

所以在第一步,在编译成员声明的同时,Foo ausing Foo = char*按顺序编译。而第一Foo使用了Foo的外部定义,即int。然后,创建一个内部作用域Foo,类型为char*。之后,编译器开始编译函数体。对于函数meowFoo b使用内部作用域Foo,这是char*,而对于在第一步中已经编译的a,是一个整数Foo。这就是转换错误发生的方式。

评论3

我想知道的是为什么"类随后可能不会重新定义该名称"。但是"内部作用域可以从外部作用域重新定义名称,即使该名称已在内部作用域中使用。

R Sahu的观点(我认为这也是这本书想说的)是,如果你真的想重新定义一个typedef标识符,你只能在课程的一开始就这样做。因此,上下文中不会有任何关于该标识符的"歧义"。

总结

允许此操作:

(这不能在g++中编译(因为标准禁止这样做),但可以在Visual Studio中编译,因为从逻辑上讲,这里没有冲突。

typedef double Money;
class Account {
public:
Money balance() { return bal; }
private:
typedef double Money;
Money bal;
};

很容易造成这样的事情:

(不能同时在 g++ 和 Visual Studio 中编译,因为逻辑上这里存在冲突。

using Foo = int;
struct X {
Foo a;    // ::Foo, i.e., int
void meow() { 
Foo b = a; // X::Foo; error: no conversion from int to char*
}
using Foo = char*;
};

因此,如果您真的想在类中重新定义typedef标识符。仅执行以下操作:

(可以在 g++ 和 Visual Studio 中编译,因为从逻辑上讲,这里没有冲突,标准只允许这样做。

typedef double Money;
class Account {
public:
typedef double Money;  
//put the redefine statement in the very beginning
Money balance() { return bal; }
private:
Money bal;
};

附注:

解释代码

typedef double Money;
class Account {
public:
Money balance() { return bal; }
private:
typedef double Money;
Money bal;
};

这些代码在逻辑上是正确的,但对于标准代码,它是被禁止的。与上面提到的编译步骤相同,首先编译函数balance的声明,所以这里Money是外部Money。然后遵守typedef double Money我们得到一个内部范围Money并且bal的类型Account::Money而不是外部范围。

所以实际上你可以用Visual Studio编译器来做到这一点,但不能用g++

typedef double Money;
class Account {
public:
Money shit = 12.34; //outside money, type is double
private:
typedef string Money;  
Money bal;   //bal is string not double
};

感谢其他两个答案的启发。我的帖子中有一些预测,这是我个人的推论。如果有任何错误,请随时更正。