在变量初始化之前使用Goto语句会导致编译器错误。

10

Consider this code (VS2008):

void WordManager::formatWords(std::string const& document)
{
    document_ = document;
    unsigned int currentLineNo = 1;

    size_t oldEndOfLine = 0;
    size_t endOfLine    = document_.find('\n');
    while(endOfLine != std::string::npos)
    {
        std::string line = document_.substr(oldEndOfLine, (endOfLine - oldEndOfLine));
        if(line.size() < 2)
        {
            oldEndOfLine = endOfLine + 1;
            endOfLine    = document_.find('\n', oldEndOfLine);
            continue;
        }

        std::vector<std::string> words = Utility::split(line);
        for(unsigned int i(0); i < words.size(); ++i)
        {
            if(words[i].size() < 2)
                continue;
            Utility::trim(words[i], WordManager::delims);
            Utility::normalize(words[i], WordManager::replace, WordManager::replaceWith);

            if(ruleOne(words[i]) && ruleTwo(words[i]))
            {
                std::set<Word>::iterator sWIter(words_.find(Word(words[i])));

                if(sWIter == words_.end())
                    words_.insert(Word(words[i])).first->addLineNo(currentLineNo);
                else
                    sWIter->addLineNo(currentLineNo);
            }
        }
        ++currentLineNo;

        oldEndOfLine = endOfLine + 1;
        endOfLine    = document_.find('\n', oldEndOfLine);
    }
}

如果重要的话:这是一个用于过滤和修改文档中单词的作业代码。`document`保存着文档(之前从文件中读取)。
我希望引入一个恶意的goto,因为在这种情况下它实际上更加简洁,如下所示:
void WordManager::formatWords(std::string const& document)
{
    document_ = document;
    unsigned int currentLineNo = 1;

    size_t oldEndOfLine = 0;
    size_t endOfLine    = document_.find('\n');
    while(endOfLine != std::string::npos)
    {
        std::string line = document_.substr(oldEndOfLine, (endOfLine - oldEndOfLine));
        // HERE!!!!!!
        if(line.size() < 2)
            goto SkipAndRestart;

        std::vector<std::string> words = Utility::split(line);
        for(unsigned int i(0); i < words.size(); ++i)
        {
            if(words[i].size() < 2)
                continue;
            Utility::trim(words[i], WordManager::delims);
            Utility::normalize(words[i], WordManager::replace, WordManager::replaceWith);

            if(ruleOne(words[i]) && ruleTwo(words[i]))
            {
                std::set<Word>::iterator sWIter(words_.find(Word(words[i])));

                if(sWIter == words_.end())
                    words_.insert(Word(words[i])).first->addLineNo(currentLineNo);
                else
                    sWIter->addLineNo(currentLineNo);
            }
        }

SkipAndRestart:
        ++currentLineNo;

        oldEndOfLine = endOfLine + 1;
        endOfLine    = document_.find('\n', oldEndOfLine);
    }
}

无论这是否是一个好的设计选择,在此时都与此无关。编译器报错:error C2362: initialization of 'words' is skipped by 'goto SkipAndRestart'
我不理解这个错误。为什么跳过words初始化很重要,并且是一个错误?这正是我想要发生的,我不希望它做更多的工作,只是重新启动循环。继续宏是否不会做更多或更少相同的事情?

我本以为这只是一个警告,而不是一个错误。如果你只使用 break 而不是 goto,会发生什么? - Paul R
6
大多数人可能不同意使用goto版本更加"简洁"的说法! - Oliver Charlesworth
@Oli:我知道,这就是为什么我说事情的实际设计无关紧要;我不想引发争论 :P @Paul:编译。 - IAE
如果你认为goto更加简洁,为什么要将其描述为“恶意”的呢? - user568493
@bstn:因为它是恶意的。这就是为什么我通常同意Oli的说法,即它通常不会使代码更清晰。我不知道为什么在这种情况下我更喜欢它。也许是因为语法糖?生活在危险的野外? - IAE
4个回答

22
你忽略了words数组的构建:
    if(line.size() < 2)
        goto SkipAndRestart;
    std::vector<std::string> words = Utility::split(line);
    // ...
SkipAndRestart:

如果您在SkipAndRestart:标签后使用了words,这可能会导致问题。虽然在您的情况下没有使用它,但是words变量直到其所在的作用域结束之前都不会被销毁,因此在编译器看来,在标签点时该变量仍处于使用状态。

words放入其自己的作用域中可以避免这种情况:

    if(line.size() < 2)
        goto SkipAndRestart;
    {
        std::vector<std::string> words = Utility::split(line);
        // ...
    }
SkipAndRestart:

请注意,continue语句会跳到循环的末尾,在这个位置上实际上您不能放置标签。 这是在循环内部任何局部变量被销毁之后,但在跳回循环顶部之前的一个点。


我的问题更多地是关于为什么这是一个实际的问题。 - IAE
1
我刚刚添加了一些关于销毁时间的内容,这将有助于解释。 - Greg Hewgill
1
如果你要这样做,最好将条件反转并完全删除 goto。这将仅在 line.size() >= 2 时进入代码块,这是预期的行为。 - David Rodríguez - dribeas
1
就编译器而言,该变量仍在使用。实际上,在标签之后仍会使用该变量,如果允许使用“goto”,则会在作用域结束时调用向量的析构函数,释放未构造的内存。砰。 - Steve Jessop

2

使用goto,您可以跳过该行:

std::vector<std::string> words = Utility::split(line);

这不仅跳过了重新初始化,还跳过了创建。因为该变量在循环内部定义,每次循环迭代时都会新创建。如果跳过该创建,您将无法使用它。
如果要创建一次,请将该行移至循环外部。
我将避免我的第一个倾向,告诉你在同一句中使用“goto”和“cleaner”通常是错误的 - 有些情况下,使用“goto”更好,但我不确定这是否是其中之一。我要告诉你的是,如果这是作业,“goto”是一个坏主意 - 任何教育者都会对使用“goto”表示不满意。

作业已经提交了,我只是在这里进行实验。我的老师确实会皱眉并指责我搞砸了好的C++代码。 - IAE

1

当有人认为使用goto可以使代码更易读时,像使用(inline)函数一样进行重构至少同样好,而且不需要使用goto

// Beware, brain-compiled code ahead!
inline void WordManager::giveThisAMeaningfulName(const std::string& word, std::string__size_type currentLineNo)
{
    Utility::trim(word, WordManager::delims);
    Utility::normalize(word, WordManager::replace, WordManager::replaceWith);
    if(ruleOne(word) && ruleTwo(word))
    {
        std::set<Word>::iterator sWIter(words_.find(Word(word)));
        if(sWIter == words_.end())
            words_.insert(Word(word)).first->addLineNo(currentLineNo);
        else
            sWIter->addLineNo(currentLineNo);
    }
}

void WordManager::formatWords(std::string const& document)
{
    document_ = document;
    std::string__size_type currentLineNo = 1;

    std::string line;
    while(std::getline(line))
        if(line.size() > 1)
        {
          std::vector<std::string> words = Utility::split(line);
          if(word.size() > 1)
              for(unsigned int i(0); i < words.size(); ++i)
                  giveThisAMeaningfulName(words[i], currentLineNo++);
        }
}

我没有费心去理解那个内部循环的作用,所以我把它留给你来起一个有意义的名字。请注意,一旦你给它命名,我就不需要理解它的算法才能知道它的作用,因为名字已经说明了一切。

请注意,我已经用std::getline()替换了你手写的行提取代码,这使得代码更加简洁。


这很有趣。我个人认为我的代码非常易读和易于理解!你所写的是一个有效的选项,但是如果不理解类的语义,你会在函数的某些部分出现问题。虽然这是我的错,但这表明我需要编写更易读的代码 :) - IAE
1
@SoulBeaver:我并没有花太多精力去真正理解这个,所以我知道为什么我在顶部添加了那个免责声明。:) 无论如何,我试图传达的是:goto是一种难以理解、不结构化的跳转方式,而存在着几种易于理解、有结构的替代方法:returnbreakif...每当你想使用goto时,请尝试将代码分解,以便能够使用这些替代方法。使用它们,我可以将代码压缩到SO不再添加垂直滚动条的程度。而且代码越少,就越容易理解。 - sbi
我现在已经使用C++编程将近20年了,从来没有使用过goto。我还没有找到一种情况,它比其他重构选项更清晰地表达我的代码。特别是C++的内联函数不需要付出额外代价,使我能够将几乎任意的代码片段命名为函数(我要放入的函数的名称),这比注释好得多,而且明确的依赖控制也更好(你必须显式地将所需数据传递给这些函数,如果太多,就会让你思考你做错了什么),这是一个非常有价值的工具。 - sbi

1
另一种解决方法是在“if”指令之前声明“words”(如果您确实无法在“if”上方填充向量),然后稍后填充向量。
// HERE!!!!!!
std::vector<std::string> words;
if(line.size() < 2)
     goto SkipAndRestart;
words = Utility::split(line);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接