重新定义/隐藏局部变量有多糟糕

How bad is redefining/shadowing a local variable?

本文关键字:局部变量 隐藏 新定义 定义      更新时间:2023-10-16

在将遗留项目升级到VS2015时,我注意到有很多错误,例如在函数中重新定义了局部变量。

void fun()
{
    int count = applesCount();
    cout << "Apples cost= " << count * 1.25;
    for (int bag=0; bag<5;bag++)
    {
        int count = orangesCount(bag);
        cout << "Oranges cost = " << count * 0.75; 
    }
}

编译器发出的错误/警告消息为:

declaration of 'count' hides previous local declaration 

我知道对变量count使用相同的名称显然不是一个好的做法,但编译器真的会把事情搞砸吗?或者通常他们会很优雅地处理这种情况?

是否值得更改和修复变量名称,或者不太可能造成任何伤害,并且风险很低或没有风险?

我注意到有很多错误,比如在函数内部重新定义了局部变量。

您不是在演示重新定义。您显示了变量阴影的示例。

变量阴影在语法上不是错误。它是有效的,定义良好。然而,如果您的意图是从外部范围使用变量,那么您可以认为这是一个逻辑错误。

但是编译器真的会把搞砸吗

没有。

阴影的问题是程序员很难跟踪。这对编译器来说是微不足道的。你可以在这个网站上找到很多问题,这些问题源于隐藏变量引起的混乱。

在这个小函数中,不难找出哪个表达式使用了哪个变量,但可以想象这个函数是几十行和几个嵌套的连续块。如果函数足够长,以至于您无法一眼看到不同范围中的所有不同定义,那么您很可能会产生误解。

declaration of 'count' hides previous local declaration 

这是一个非常有用的编译器警告。您还没有用完名称,为什么不为函数中的所有局部变量指定一个唯一的名称呢?但是,没有必要将此警告视为错误。这只是一个提高程序可读性的建议。

在这个特定的例子中,在内部作用域打开后,您不需要在外部作用域中使用count,所以您还可以为两个计数重用一个变量。

更改和修复变量名值得吗

这取决于您是否更重视短期工作量而不是长期工作量。现在,将代码更改为使用唯一的、描述性的局部变量名是一项"额外"的工作,但每次有人以后必须理解程序时,不必要的阴影会增加心理挑战。

IMHO,错误的编码实践。难以维护和阅读。

编译器可以区分外部变量和内部变量。

有了好的词汇表(和词库),就不需要使用相同的变量名。

对变量进行阴影处理(这就是它)具有完全定义好的语义,因此编译器不会把它搞砸。它将完全按照指示行事,并产生明确的结果。

问题出在人类身上(通常都是这样)。阅读和修改代码时很容易出错。如果一个人不太小心,那么跟踪引用的是哪个具有给定名称的变量可能会很困难,而且当你认为你在修改一个变量,但实际上你在修改另一个变量时,很容易出错。

所以,编译器很好,程序员才是问题所在。

根据我的经验,编译器通常会非常优雅地处理这个问题。

然而,这无疑是一种糟糕的做法,除非您有真正令人信服的理由这样做,否则您应该重用旧变量(如果这样做在逻辑上有意义的话),或者声明一个[名称不同]的新变量。

可能非常糟糕:

考虑这个

std::vector<int> indices(100);
if (true) {
  std::vector<int> indices(100);
  std::iota(indices.begin(), indices.end(), 0);
}
// Now indices will be an all-0 vector instead of the an arithmetic progression!

在Visual Studio中,使用Warning Level4 /W4进行编译时,即使在前缀中使用隐式this指针来消除歧义,也会输出警告,如this->Count:

警告C4458:"Count"的声明隐藏类成员

尽管编译器很少在值上犯错误,但阴影可能会被滥用,并随着时间的推移而变得混乱。

以下是应该避免的对Count成员变量进行阴影处理的示例:

来自虚幻编码标准文档。

class FSomeClass
{
public:
    void Func(const int32 Count)
    {
        for (int32 Count = 0; Count != 10; ++Count)
        {
            // Use Count
        }
    }
private:
    int32 Count;
}

处理此问题的一个选项是在出现阴影时为传入参数名称添加前缀,如下所示:

class FSomeClass
{
public:
    void Func(const int32 InCount)
    {
        Count = InCount;
    
        for (int32 Counter = 0; Counter != 10; ++Counter)
        {
            // Use Count
        }
    }
private:
    int32 Count;
}

使用RAII资源时,实际上可能会将代码简化为影子变量,而不创建新的变量名。

例如,一个简单的记录器,它在进入和离开块时写入stdout:

class LogScope {
  private:
    std::string functionName;
    uint32_t lineNo;
  public:
    LogScope(std::string _functionName, uint32_t _lineNo) : 
      functionName(_functionName), lineNo(_lineNo) {
        std::cout << "Entering scope in " << functionName << " starting line " << lineNo << std::endl;
    };
    ~LogScope(void) {
        std:: cout << "Exiting scope in " << functionName << " starting line " << lineNo << std::endl;
    };
};

它可以像这样使用:

void someFunction() { // First scope here.
    LogScope logScope(__FUNCTION__, __LINE__);
    // some code...
    // A new block.
    // There is really no need to define a new name for LogScope.
    {
        LogScope logScope(__FUNCTION__, __LINE__);
        // some code...
    }    

}

话虽如此,通常不重用变量名是一种很好的做法。

Zar,

编译器会很好地处理这种情况。在您的示例中,计数变量是在两个不同的作用域"{}"中定义的。由于变量的作用域,汇编语言将引用堆栈上的两个不同地址。第一个"计数"可能是堆栈点SP-8,而内部计数可能是SP-4。一旦转换成地址,名称就无关紧要了。

我一般不会因为风格的原因而更改工作代码。如果代码一团糟,那么你就有破坏它的风险。通常混乱的代码没有任何好的测试,所以很难知道你是否破坏了它。

如果您需要增强代码,那么一定要整理一下。

--Matt