【问题标题】:Is this use of c_str with exception undefined behavior?c_str 的这种使用是否具有异常未定义的行为?
【发布时间】:2017-07-20 16:11:12
【问题描述】:

我见过几个类似的 sn-ps 代码,看起来像这样:

struct MyExcept : std::exception {
    explicit MyExcept(const char* m) noexcept : message{m} {}

    const char* what() const noexcept override {
        return message;
    }

    const char* message;
};

void foo() {
    std::string error;

    error += "Some";
    error += " Error";

    throw MyExcept{error.c_str()};
}

int main() {
    try {
        foo();
    } catch (const MyExcept& e) {
        // Is this okay?
        std::cout << e.message << std::endl;
    }
}

在注释Is this okay? 之后的行中,我们读取了使用std::stringfoo 函数中分配的c 样式字符串。由于字符串被堆栈展开破坏,这是未定义的行为吗?


如果它确实是未定义的行为,如果我们用这个替换 main 函数呢?

int main() {
    foo();
}

由于没有catch,编译器不会强制展开堆栈,而是在控制台中输出what()的结果并中止程序。那么它仍然是未定义的行为吗?

【问题讨论】:

    标签: c++ exception c++14 undefined-behavior stack-unwinding


    【解决方案1】:

    是的,这是未定义的行为。您正在使用悬空指针。

    void foo() {
        std::string error;
    
        error += "Some";
        error += " Error";
    
        throw MyExcept{error.c_str()};
    } // <<  error goes out of scope here and so does the pointer returned
      //     from c_str()
    

    由于没有catch,编译器不会强制解开堆栈,而是在控制台中输出what()的结果并中止程序。那么它仍然是未定义的行为吗?

    由于默认实现将使用std::terminate,然后调用std::abort(),这可能仍然是未定义的行为,因为大多数标准处理程序实现将尝试取消引用what()

    您可以安装自己的处理程序来避免这种情况。

    【讨论】:

    • 你说的是哪一部分?第一个还是第二个?您是否阅读了整个问题?
    • @GuillaumeRacicot 您展示的两个版本都会出现这种情况。
    • 没有。 MyExcept 可以包含一个悬空指针而不会导致任何未定义的行为,除非您取消引用它。
    • @GuillaumeRacicot 至少标准的terminate 调用会尝试在您的第二个示例中取消引用它。
    • @GuillaumeRacicot 只是不要这样做。
    【解决方案2】:

    您的第一个 sn-p 具有未定义的行为。 [exception.ctor]/1:

    当控制从抛出异常的点传递到处理程序时,析构函数被本节中指定的进程调用,称为堆栈展开。

    在这里,析构函数或error被调用,导致c_str()成为一个悬空指针。稍后取消引用它,例如当您使用 std::cout 时,是未定义的行为。

    您的第二个 sn-p 非常好。没有理由说它是未定义的行为。你从来没有真正打电话给what,或者做任何其他可能导致未定义行为的事情。唯一没有被标准定义的是堆栈展开是否发生,[except.terminate]/2

    在没有找到匹配处理程序的情况下,在调用std​::​terminate()之前是否解开堆栈是由实现定义的。

    【讨论】:

    • 感谢 Rakete1111,所以如果第二个 sn-p 中存在未定义的行为,则取决于 GCC 或任何其他实现?
    • “没有理由它会是未定义的行为。” 假设这是定义的实现。它可能是也可能不是未定义的行为,具体取决于实现的功能。
    • @GuillaumeRacicot 不,没有未定义的行为。从编译器到编译器的唯一行为是堆栈展开是否发生,这是实现定义的。你从来没有真正使用过有问题的指针,所以没关系。
    • @user0042 很好,但在哪里? default handler 只是调用std::abort,仅此而已。但是好吧,也许我错过了一个报价,我会试着找到它。没有什么说这是实现定义的。
    • @Rakete1111 好吧,我不得不承认,我根据经验告诉我,也没有参考报价:-/ ...
    【解决方案3】:

    正如其他人所说,代码具有未定义的行为,因为分配给 message 的指针悬空。

    std::runtime_error 已经为此问题提供了解决方案。调用以std::string 作为输入的构造函数,并且根本不覆盖what()

    struct MyExcept : std::runtime_error {
        explicit MyExcept(const std::string & m) noexcept : std::runtime_error(m) {}
    };
    
    void foo() {
        std::string error;
    
        error += "Some";
        error += " Error";
    
        throw MyExcept(error);
    }
    
    int main() {
        try {
            foo();
        }
        catch (const MyExcept& e) {
            std::cout << e.what() << std::endl;
        }
    }
    

    std::runtime_error 有一个内部std::string,其数据what() 默认返回,从而避免了悬空问题。

    【讨论】:

    • 确实这是设置异常错误消息的正确方法。有趣的是,std::exception::what() 方法是virtual,但似乎没有简单的方法可以覆盖它,因为它必须返回一个const char *,在大多数情况下会导致悬空指针问题。也许有一天签名会改成返回std::string...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-14
    • 1970-01-01
    • 2016-11-12
    • 1970-01-01
    • 1970-01-01
    • 2019-09-26
    • 1970-01-01
    相关资源
    最近更新 更多