C++标准是否规定C链接函数是`noexcept`的?

29

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

然而,显然,C 调用约定无法支持异常...或者说不行吗?

标准是否提及了这一点,是否有我错过的地方?如果没有,为什么呢?它仅仅被留作某种实现细节吗?


2
它是否会破坏兼容性还是存在疑问的。从C函数泄漏异常的程序可能一直存在未定义的行为。 - Puppy
4
关联的链接:https://dev59.com/vGUo5IYBdhLWcg3wrhHj#15845731。 - ta.speot.is
@ta.speot.is 可能是一个重复的问题。-.- - Lightness Races in Orbit
1
MSVC++编译器似乎认为它是未指定的,/EHs与/EHsc。 - Hans Passant
我删除了我的回答,因为我现在认为你的意思是:“C++标准是否应该允许程序通过将其标记为noexcept来正式假定给定的C函数在处理C++异常方面表现正确?”,就像我们期望汇编手工编写的导入函数遵循C调用约定一样。 - didierc
显示剩余5条评论
5个回答

22
据我所知,使用“C”链接定义的函数不保证不会抛出异常。标准允许C++程序调用带有“C”语言链接的外部函数,并定义使用C++编写并具有“C”语言链接的函数。因此,没有任何防止C++程序调用使用“C”语言链接实际上是用C++编写的函数(可能是在另一个编译单元中,尽管这甚至不是必要的)的方法。这将是一件奇怪的事情,但很难排除。此外,我没有看到标准在哪里说这样做会导致未定义的行为(实际上,由于标准无法定义非使用C++编写的函数的行为,这将是唯一没有正式未定义行为的用法)。
因此,我认为假设“C”链接意味着noexcept是错误的。

1
好的回答,你肯定知道如何用恰当的措辞表达。 - didierc
4
一种不那么奇怪的情况是一个C函数接受(并调用)一个函数指针,该指针可能指向一个抛出异常的C++函数。 - Tavian Barnes

9

我猜测extern "C"只是使用C语言风格,而不是C函数。它可以防止编译器进行C++名称重整

更直接的说 - 假设这段代码。

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

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

1
实际上,它不仅仅是防止名称混淆。extern "C++" void Foo();extern "C" void Foo();是不同的类型,具有不同的属性,其中链接性只是其中之一。 - Captain Obvlious
1
我认为这也会影响调用约定,并且异常需要特定的调用约定才能使堆栈展开正常工作。 - Lightness Races in Orbit

3

Marc van Leeuwen的回答是正确的:查找当前工作草案,没有强制要求声明为extern "C"的函数隐式为noexcept。有趣的是,标准C ++禁止在C ++标准库中抛出C标准库函数。这些函数通常被指定为extern "C"(但这是实现定义,请参见16.4.3.3-2)。请查看条款16.4.6.13 [对异常处理的限制]以及附带的脚注174175

C标准库的函数不应该抛出异常[脚注174],除非这样的函数调用了一个会抛出异常的程序提供的函数。[脚注175]
脚注174: 也就是说,可以将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,则所有输出都为false,这使得在严格符合性方面需要/EHsc中的'c'。


0

没有任何地方说明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实现为可以被catch(...)捕获的抛出异常,即使编译C代码时也是如此。如果您在任何地方执行未定义行为,则整个程序在代码路径无法撤销地致力于达到未定义行为时就变成了未定义行为,因此这可能会导致C标准库函数抛出异常。

如果传递给 qsortbsearch 的比较函数抛出异常,行为是否已经定义?一些平台的异常处理机制只有当 throwcatch 之间的每个堆栈帧具有特定布局时才能工作,但在不需要堆栈展开时,其他布局可能更有效率。对于这样的平台,C编译器通常没有理由不使用这样的布局。 - supercat
@supercat:我见过的每个具有该属性的架构都会盲目解开任何中间堆栈帧,因为解开会一直进行到它识别到下一个帧。这可能会对fopen()造成麻烦,但我无法想象对于qsort()会有什么问题。此外,我期望在一个不支持此功能的架构上,从C++中包含qsort()的函数指针将被声明为noexcep。他们已经不得不使用#ifdef来使链接工作,所以这不是太麻烦。 - Joshua
如果函数只需要使用其参数一次,则可以将其翻译为“然后以简单的'ret'结束”。或者,它可以跟踪ESP的当前值和起始值之间的关系,并在堆栈比函数开始时深n个字节时使用“[ESP +(n + 8)]”寻址模式。通过这样做的函数的堆栈展开可能是不可能的。 - supercat
@supercat:这就是catch最初的工作方式,只不过它是一个每个线程变量(就像errno一样)。 - Joshua
许多独立实现用于为“裸机”平台生成代码,这些平台在加载的程序之外没有“操作系统”。如果程序包括一个小的机器码片段(通常是十几条指令或更少)进行上下文切换,许多这样的平台可以支持线程,但是任何试图使用静态存储的编译器函数都会在调用超过一个线程时出错。因此,专为此类目的设计的高质量独立实现将避免在除了库函数(例如rand())之外使用静态存储,其语义自然需要它。 - supercat
显示剩余4条评论

0

一个 C 函数 foo 可以调用一个在 C++ 中声明为 extern "C" 的函数 bar,该函数可能(间接地)抛出一些 C++ 异常。

一个 C 函数 foo(可能被某个 C++ 函数调用)可以调用 longjmp,其运行时行为类似于异常抛出。

据我所知,最早的 C++ 编译器(Cfront)使用 longjmp 生成 C 代码来翻译 throw(并使用 setjmp 来翻译 catch)。当然,C++ 析构函数会使事情变得复杂。


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