【问题标题】:Does the C++ standard mandate that C-linkage functions are `noexcept`?C++ 标准是否要求 C 链接函数是“noexcept”?
【发布时间】:2014-08-13 07:09:09
【问题描述】:

我在标准中找不到任何强制使用extern "C" 声明的函数为noexcept 的内容,无论是隐式还是显式。

然而,应该清楚的是,C 调用约定不能支持异常......或者是吗?

标准是否在我错过的地方提到了这一点?如果不是,为什么不呢?它只是作为某种实现细节留下来吗?

【问题讨论】:

  • 无法想象 C++ 语言演变中出现如此大规模的兼容性中断,我们可以吗?
  • 它是否会破坏兼容性是非常值得怀疑的。从 C 函数泄漏异常的程序可能总是有未定义的行为。
  • @ta.speot.is 实际上可能是个骗子-.-
  • MSVC++ 编译器似乎认为它未指定,/EHs 与 /EHsc。

标签: c++ c c++11 language-lawyer noexcept


【解决方案1】:

据我所知,不能保证使用“C”链接定义的函数不会引发异常。该标准允许 C++ 程序调用具有“C”语言链接的外部函数,并定义用 C++ 编写的具有“C”语言链接的函数。因此,没有什么可以阻止 C++ 程序调用具有“C”语言链接的函数,该函数实际上是用 C++ 编写的(可能在另一个编译单元中,尽管这不是必需的)。这将是一件奇怪的事情,但很难排除。此外,我没有看到标准中的哪个地方说这样做会导致未定义的行为(事实上,由于标准无法定义未用 C++ 编写的函数的行为,这将是 only 用法没有正式未定义的行为)。

因此,我认为假设“C”链接意味着noexcept 是错误的。

【讨论】:

  • 很好的答案,你当然知道如何用正确的方式表达。
  • 一个不太奇怪的情况是一个 C 函数接受(并调用)一个函数指针,该指针可能指向一个抛出的 C++ 函数。
【解决方案2】:

嗯,我假设 extern "C" 只是使用 C 链接,而不是 C 函数。它会阻止编译器执行C++ name mangling

更直接 - 假设这段代码。

// foo.cpp
extern "C" void foo()
{
    throw 1;
}

// bar.cpp
extern "C" void foo();
void bar()
{
    try
    {
        foo();
    }
    catch (int)
    {
        // yeah!
    }
}

【讨论】:

  • 实际上,它不仅仅是防止名称篡改。 extern "C++" void Foo();extern "C" void Foo(); 不同。它们是不同的类型,具有不同的属性,链接只是其中之一。
  • 我认为它也会影响调用约定,并且异常需要特定的调用约定才能使堆栈展开工作。
【解决方案3】:

Marc van Leeuwen's answer 是正确的:查看当前的working draft,似乎没有什么要求声明extern "C" 的函数隐含noexcept。有趣的是,标准 C++ 禁止 C++ 标准库中的 C 标准库函数抛出异常。这些函数本身通常指定为extern "C"(但这是定义的实现,请参见16.4.3.3-2)。查看子句16.4.6.13 [限制异常处理] 和随附的脚注174175

C 标准库中的函数不应引发异常 [脚注 174],除非此类函数调用程序提供的引发异常的函数。[脚注 175]

脚注 174:

  1. 也就是说,C 库函数都可以被视为标记为 noexcept。 这允许实现基于运行时不存在异常来进行性能优化。

脚注 175:

函数 qsort() 和 bsearch() ([alg.c.library]) 满足这个条件。

话虽如此,遵循与标准库相同的策略通常是一个很好的设计指南,出于Marc van Leeuwen's answer 中提到的原因,我认为用户定义的extern "C" 函数也可以用noexcept 指定是个好主意, 除非它被传递一个指向 C++ 函数的指针作为回调参数,如 qsort 等。我用clang10、gcc10做了一个小实验,代码如下:

#include <cstring>
#include <cstdlib>
#include <iostream>

extern "C" int cmp(const void* lhs, const void* rhs) noexcept;
extern "C" int non_throwing();

int main()
{
    constexpr int src[] = {10, 9, 8, 7, 6, 5};
    constexpr auto sz = sizeof *src;
    constexpr auto count = sizeof src / sz;

    int dest[count];
    int key = 7;

    std::cout << std::boolalpha
    // noexcept is unevaluated so no worries about UB here
        << "non_throwing: " << noexcept(non_throwing()) << '\n'
        << "memcpy: " << noexcept(std::memcpy(dest, src, sizeof dest)) << '\n'
        << "malloc: "<< noexcept(std::malloc(16u)) << '\n'
        << "free: " << noexcept(std::free(dest)) << '\n'
        << "exit: " << noexcept(std::exit(0)) << '\n'
        << "atexit: " << noexcept(std::atexit(nullptr)) << '\n'
        << "qsort: " << noexcept(std::qsort(dest, count, sz, cmp)) << '\n' // should fail
        << "bsearch: " << noexcept(std::bsearch(&key, dest, count, sz, cmp)) << '\n'; // should fail
}

gcc10 和 clang10 的输出是:

non_throwing: false
memcpy: true
malloc: true
free: true
exit: true
atexit: true
qsort: false
bsearch: false

对于 msvc142,如果使用 /EHsc 编译,那么所有输出显然都是 true。而使用 /EHs,所有输出都是错误的,这使得 /EHsc 中的“c”对于严格遵守是必要的。

【讨论】:

    【解决方案4】:

    没有任何地方说extern "C" 函数是noexcept。另一方面,几乎所有的 C 标准库函数都是noexcept,除非你做了一些奇怪的事情。通常,这归结为调用未定义的行为,但还有一些其他情况。这些应该都是:

    • qsort() 的函数指针参数可以抛出;因此qsort() 可以抛出。
    • bsearch() 也是如此。
    • 您可以替换malloc()realloc()free()。如果你这样做,这些可能会抛出。
    • 在前一种情况下,calloc()fopen()fclose()freopen()system()strdup() 也可能抛出。 (strdup() 已定义但不保证存在。)
    • setjmp()catch(...) 不要混用。至少有一个平台将longjmp() 实现为throw jmp_buf 的逻辑等价物,导致catch(...) 捕获它。
    • 可能会抛出未定义的行为。一些系统实际上确实将 *NULL 实现为抛出异常,即使在编译 C 代码时也可以被catch(...) 捕获。如果您在任何地方执行未定义的行为,一旦代码路径不可撤销地致力于达到未定义的行为,整个程序就会未定义,因此这可能会导致 C 标准库函数抛出异常。

    【讨论】:

    • 如果传递给qsortbsearch 的比较函数抛出异常,是否定义了行为?某些平台的异常处理机制只有在 throwcatch 之间的每个堆栈帧都具有特定布局时才有效,但是当不需要堆栈展开时,其他布局可能更有效,并且此类平台的 C 编译器会通常没有理由不使用这样的布局。
    • @supercat:我看到的每个带有该属性的拱门都会盲目地展开任何中间堆栈帧,因为展开会向下进行,直到它识别出下一帧。这可能会给fopen() 带来麻烦,但我无法想象它会给qsort() 带来麻烦。此外,我希望在一个不起作用的体系结构上,当从 C++ 中包含时,指向qsort() 的函数指针将被声明为noexcep。他们已经不得不使用#ifdef 来使链接工作,所以这并没有更多的麻烦。
    • @supercat:另一方面,如果你的断言是真的,那么传递给自定义 C 函数的任何函数指针都必须是 noexcept 从而使所有 C 函数 noexcept 这与所有答案相反这个问题。
    • 要求函数不是noexcept,而是在直接从外部语言函数调用的任何情况下,它们都不会抛出/泄漏异常。在 x86 上,函数访问其参数的最简单方法是从 push ebp / mov ebp,esp 开始,然后退出 mov esp,ebp / pop ebp。如果所有函数都这样做,它将创建堆栈帧的链接列表但不要求所有函数都这样做。接收一个参数的函数可以以pop edx / pop eax / push eax / push edx...
    • 然后以简单的ret 结束,如果它只需要使用它的参数一次。或者,它可以跟踪 ESP 的当前值和起始值之间的关系,如果堆栈比函数启动时深n 字节,则使用[ESP+(n+8)] 寻址模式。通过执行此类操作的函数展开堆栈可能是不可能的。
    【解决方案5】:

    C 函数 foo 可以调用用 C++ 编码、声明为 extern "C" 的函数 bar,因此 bar 是(可能间接地)throw - 导致一些 C++ 异常。

    一个 C 函数 foo(可能被某个 C++ 函数调用)可以调用运行时行为接近异常抛出的 longjmp

    IIRC,第一个 C++ 编译器 (Cfront) 使用 longjmp 生成 C 代码以翻译 throw(和 setjmp 翻译 catch)。当然,C++ 析构函数会使事情复杂化。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-17
      • 2021-10-27
      • 2022-01-02
      • 2016-12-10
      • 2016-11-18
      • 1970-01-01
      • 2021-06-15
      相关资源
      最近更新 更多