在下列情况下可以使用goto吗?

时间:2012-01-31 06:58:08

标签: c++

好的,世纪的问题:)

在你说或想什么之前,让我告诉你,我已经读过几个关于这个话题的类似问题,但我没有找到解决问题的明确方法。我的案例是具体的,对于我认为的系统程序员而言是典型的。

我经常遇到这种情况。我讨厌搞砸了,不知道为什么,可能是因为每个人都在大喊大叫它是坏事。但到目前为止,我没有找到更好的解决方案来解决我的具体情况,而且我目前的做法可能比使用goto更加丑陋。

以下是我的案例:我使用C ++(Visual C ++)进行Windows应用程序开发,而且我常常在我的例程中使用大量的API。假设以下情况:

int MyMemberFunction()
{
    // Some code... //

    if (!SomeApi())
    {
        // Cleanup code... //

        return -1;
    }

    // Some code... //

    if (!SomeOtherApi())
    {
        // Cleanup code... //

        return -2;
    }

    // Some more code... //

    if (!AnotherApi())
    {
        // Cleanup code... //

        return -3;
    }

    // More code here... //

    return 0; // Success
}

所以在每次Api之后我必须检查它是否成功,并且如果没有则中止我的功能。为此,我使用了大量// Cleanup code... //,通常几乎重复,然后是return语句。该功能执行10个任务(例如使用10个Apis),如果任务#6失败,我必须清理以前任务创建的资源。请注意,清理应该由函数本身完成,因此不能使用异常处理。另外,在这种情况下,我无法看到有多少谈论RAII可以帮助我。

我想到的唯一方法是使用goto从所有这些失败案例跳转到一个清理标签,放在函数的末尾。

有没有更好的方法呢?在这种情况下使用goto会被认为是不好的做法吗?该怎么办?这种情况对我来说非常典型(对于像我这样的系统程序员,我相信)。

P.S。:需要清理的资源属于不同类型。可能存在内存分配,需要关闭的各种系统对象句柄等。

更新:

我认为人们仍然没有得到我想要的东西(可能我的解释很糟糕)。我认为伪代码应该足够了,但这是一个实际的例子:

  1. 我用CreateFile打开两个文件。如果此步骤失败:我必须清除已打开的文件句柄(如果有)。我稍后会阅读一个文件的一部分并写入另一个文件。

  2. 我使用SetFilePointer将读指针放在第一个文件中。如果此步骤失败:我必须关闭上一步打开的句柄。

  3. 我使用GetFileSize来获取目标文件大小。如果api失败,或文件大小异常,我必须进行清理:与上一步相同。

  4. 我从第一个文件中分配指定大小的缓冲区。如果内存分配失败,我必须再次关闭文件句柄。

  5. 我必须使用ReadFile从第一个文件中读取。如果失败,我必须:释放缓冲区内存,并关闭文件句柄。

  6. 我使用SetFilePointer将写指针放在第二个文件中。如果失败,则必须进行相同的清理。

  7. 我必须使用WriteFile写入第二个文件。如果失败了,bla-bla-bla ......

  8. 另外,假设我使用关键部分保护此函数,并且在函数开头调用EnterCriticalSection之后,我必须在每个LeaveCriticalSection语句之前调用return

    现在请注意,这是一个非常简化的示例。可能会有更多的资源和更多的清理工作 - 大多数相同,但有时会有所不同,这取决于哪个步骤失败了。但是让我们在这个例子中谈谈:我如何在这里使用RAII?

3 个答案:

答案 0 :(得分:7)

不需要使用goto,它容易出错,导致冗余且相当不安全的代码。

使用 RAII ,您不必使用goto。 RAII到 smart pointers 非常适合您的方案。

您确保范围内的所有资源都是RAII管理的(使用智能指针或您自己的资源管理类),每当出现错误情况时,您只需要返回,RAII将神奇地释放您的资源。

答案 1 :(得分:4)

只要以前任务创建的资源以自我清理后的类/对象的形式维护,RAII就可以为此工作。你提到了内存和系统对象句柄,所以让我们把它们作为一个起点。

// non RAII code:
int MyMemberFunction() { 
    FILE *a = fopen("Something", "r");

    if (!task1()) {
       fclose(a);
       return -1;
    }

    char *x = new char[128];

    if (!task2()) {
        delete [] x;
        fclose(a);
        return -2;
    }
}

基于RAII的代码:

int MyMemberFunction() { 
    std::ifstream a("Something");
    if (!task1())
        return -1; // a closed automatically when it goes out of scope

    std::vector<char> x(128);

    if (!task2())
        return -2; // a closed, x released when they go out of scope
    return 0; // again, a closed, x released when they go out of scope
}

另请注意,如果您通常希望工作正常,您可以编写代码来更贴切地描绘:

int MyMemberFunction() {
    bool one, two;

    if ((one=task1()) && (two=task2()))
        return 0;

    // If there was an error, figure out/return the correct error code.
    if (!one)
        return -1;
    return -2;
}

编辑:虽然这很不寻常,但如果你真的需要使用c风格的I / O,你仍然可以把它包装成一个类似于iostream的C ++类:

class stream { 
    FILE *file;
public:
    stream(char const &filename) : file(fopen(filename, "r")) {}
    ~stream() { fclose(file); }
};

这显然已经简化了(很多),但总体思路完美无缺。有一个不那么明显,但通常更优秀的方法:iostream实际上使用缓冲类,使用underflow进行读取,使用overflow进行写入(再次,简化,但这次没有那么多)。编写一个使用FILE *来处理读/写的缓冲类并不是非常困难。所涉及的大部分代码实际上只是一个相当薄的转换层,可以为函数提供正确的名称,将参数重新排列为正确的类型和顺序,等等。

在记忆的情况下,您有两种不同的方法。一个是这样的,编写一个类似于矢量的类,它纯粹作为你需要使用的内存管理的包装器(new / deletemalloc / {{1等等。)

另一种方法是观察free有一个Allocator参数,所以它实际上只是一个包装器,你可以指定它如何获取/释放内存。例如,如果您确实需要其后端为std::vectormalloc,那么编写使用它们的free类会相当容易。这样,大多数代码都遵循正常的C ++约定,使用Allocator就像其他任何东西一样(并且仍然支持RAII)。同时,您可以完全控制内存管理的实现,因此您可以直接使用std::vector / newdelete / malloc或其他内容。操作系统(如果需要)(例如,Windows上的free / LocalAlloc)。

答案 2 :(得分:1)

使用提升: