13

例如,有两种不同的方法来访问私有数组的元素,重载数组下标运算符或定义at

T& operator[](size_t i) { return v[i]; }
T const& operator[](size_t i) const { return v[i]; }

T& at(size_t i)
{
     if (i >= length)
         throw out_of_range("You shall not pass!");

     return v[i];
}

T const& at(size_t i) const
{
     if (i >= length)
         throw out_of_range("You shall not pass!");

     return v[i];
}

at版本可以抛出异常,但数组下标运算符不能。

我的问题是,尽管operator[]不会引发异常,是否可以将其标记为noexcept即使知道它可以引发 SIGSEGV 信号,还是只是一种不好的做法?

我想指出一个信号(如 SIGSEGV)也不例外。作为字面意思的解释noexcept,一个noexcept函数是一个声称它不会抛出异常的函数。它没有说明其他任何事情(包括信号)。

但是,noexcept函数的意义远不止于此,至少对于我的代码的客户而言。noexcept还隐含地表示该函数是安全的,它将在没有计算错误的情况下完成其执行。

那么,标记noexcept一个不安全的功能是不合适的吗?

4

3 回答 3

5

这是一个棘手的问题,并提出了noexcept 中涵盖的一些关键问题——为了什么?来自 Andrzej 的 C++ 博客,其中说,我将尝试在此处引用最小值(强调我的):

在这篇文章中,我想分享我对使用 noexcept 真正增加价值的地方的观察。它比人们预期的要少,并且与抛出或不抛出异常没有太大关系。这个结论让我有点吃惊,我犹豫是否要提出它,因为这与我从我认为有关该主题的权威人士那里听到的建议背道而驰。

和:

鉴于对 noexcept 的这种消极态度,它是否可以被认为是有用的?是的。noexcept功能很晚才引入 C++11,以解决移动语义的一个特定问题。Douglas Gregor 和 David Abrahams 在这里描述了它。

然后他继续给出了一个不寻常的移动分配定义,并认为我们真正想要传达的不是它不会抛出异常而是它不会失败,但这是一个非常困难的问题,但它是真正的意图:

[...]这是因为 noexcept 真正要传达的信息是该功能永远不会失败;并不是说它从不抛出!我们可以在上面看到一个函数可能会失败但仍然不会抛出,但它仍然符合 noexcept(false) 的条件。也许关键字应该被称为 nofail。编译器无法检查永不失败的保证(就像任何其他故障安全保证一样),因此我们唯一能做的就是声明它。

这是一个更普遍的观察的一部分,我们感兴趣的是程序组件中的故障安全而不是异常安全。无论你使用异常、错误返回值、errno 还是什么

无论如何,关于基本(无泄漏,保持不变),强(提交或回滚)和永不失败保证的推理仍然应该成立。

因此,如果我们采取类似的立场,那么答案似乎是否定的,如果不适合使用noexcept,那似乎就是你所倾向的地方。我不认为这是一个明确的答案。

他还注意到提案 N3248:noexcept Prevents Library Validation。这反过来又是N3279 的基础:在库中保守使用 noexcept。这篇论文定义了窄合约和宽合约,就像 N3248 一样:

宽合约

函数或操作的广泛契约不指定任何未定义的行为。这样的契约没有先决条件:具有广泛契约的函数对其参数、任何对象状态或任何外部全局状态都没有额外的运行时约束。具有广泛合同的函数的示例是 vector::begin() 和 vector::at(size_type) 。没有广泛合同的函数的例子是 vector::front() 和 vector::operator[](size_type) 。

窄合约

狭义合同是不广泛的合同。当以违反记录的合同的方式调用时,函数或操作的狭窄合同会导致未定义的行为。这样的契约至少规定了一个涉及其参数、对象状态或一些外部全局状态的先决条件,例如静态对象的初始化。具有窄合约的标准函数的好例子是 vector::front() 和 vector::operator[](size_type) 。

并建议:

LWG 同意不能抛出的具有广泛合同的每个库函数都应标记为无条件 noexcept。

并暗示不应具有狭窄合同的功能noexcept,这得到LWG 问题 2337的支持,其中说:

[...]这些设计考虑超越了我们反对 noexcept 窄合约功能的一般政策。[...]

因此,如果我们想保守一点并遵循标准库实践,那么似乎因为operator[]没有广泛的合同,它不应该被标记noexcept

于 2015-05-13T21:00:06.993 回答
1

事实:无论您是否使用noexcept,该函数仍然可以抛出SIGSEGV

现在,我认为答案是主观的,取决于个人的观点。你对noexcept函数有什么期望?你会期望它永远不会失败(即使noexcept不提供这样的保证)?与“安全”函数相比,您将如何处理非安全函数?项目会有很大的不同吗?如果您的回答是肯定的,那么您也许不应该使用noexcept

另一方面,不使用它(当你确定它不会抛出异常时)会让另一个程序员担心在调用这个函数时提供一个 try 子句。

来自Bjarne StroustroupThe C++ Programming Language ”,第 4 版:

对于推理程序的程序员和优化程序的编译器来说,声明一个函数 noexcept 可能是最有价值的。程序员无需担心提供 try 子句(用于处理 noexcept 函数中的故障),优化器无需担心来自异常处理的控制路径。

那么,将noexcept标记为不安全的函数是否不合适?

我个人的观点是不,使用它不是坏习惯或“不道德”,更重要的是它可以带来一些好处。

于 2015-05-13T21:42:33.743 回答
0

一种选择是添加边界检查并导致超出范围的值做一些有意义的事情,例如 return last ,如下所示。

T& operator[](size_t i)
{
    static T default;
    if (i >= length && length > 0)
    {
        return v[length - 1];
    }
    else if (i >= length && length == 0)
    {
        return default;
    }
    return v[i];
}

这是非标准的,但在我看来,如果您清楚地记录行为,这是可以接受的。这允许无例外和无信号保证。

于 2015-05-13T19:18:55.993 回答