使用c_str是否有未定义的异常行为

Is this use of c_str with exception undefined behavior?

本文关键字:异常 未定义 str 是否 使用      更新时间:2023-10-16

我看到了几个类似的代码片段,看起来像这样:

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()的结果并中止程序。那么,它仍然是未定义的行为吗?

是的,这是未定义的行为。你正在使用一个悬空的指针。
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()

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

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

不过,为了避免这种情况,您可以安装自己的处理程序。

您的第一个代码段具有未定义的行为。[exception.ctor]/1:

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

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

你的第二个片段非常好。没有理由认为这是未定义的行为。您从未实际调用过what,也从未执行过任何可能导致未定义行为的其他操作。标准中唯一没有定义的是堆栈是否展开,[except.terminate]/2:

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

正如其他人所说,由于分配给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()默认返回,从而避免了悬空问题。