23 December 2021

C++ 核心指南目录

P.7 Catch run-time errors early

理由

尽早发现错误,避免导致诡异的 bug。

例子

以下程序传给 increment1 函数一个数组指针和数组的大小作为参数。如果数组大小不小心写错了,会导致数组越界。程序可能会继续执行,但是随时可能运行奔溃。

void increment1(int* p, int n)    // bad: error-prone
{
    for (int i = 0; i < n; ++i) ++p[i];
}

void use1(int m)
{
    const int n = 10;
    int a[n] = {};
    for (int i = 10; i < m; i++) cout << a[i] << " ";
    cout << "\n";
    increment1(a, m);   // maybe typo, maybe m <= n is supposed
                        // but assume that m == 20
    for (int i = 10; i < m; i++) cout << a[i] << " ";
    cout << endl;
}
int main()
{
    use1(20);
}
-1500637744 32759 2 10 2 15 1805646800 85 -1500637797 32759 
-1500637743 32760 3 11 14 21 1805646801 86 -1500637796 32760 

例子

以下代码用了 span ,但是数组大小还是分开设置了,依然会导致越界访问:

void increment2(span<int> p)
{
    for (int& x : p) ++x;
}

void use2(int m)
{
    const int n = 10;
    int a[n] = {};
    for (int i = 10; i < m; i++) cout << a[i] << " ";
    cout << endl;
    increment2({a, m});    // maybe typo, maybe m <= n is supposed
    for (int i = 10; i < m; i++) cout << a[i] << " ";
    cout << endl;
}
int main()
{
    use2(20);
    return 0;
}
0 0 -2018306080 32758 24 0 0 10 0 19
1 1 21 1 281016609 124 1 11 18 21

例子

直接传递一个带数组长度信息的 spanincrement2 就不会出错了:

void increment2(gsl::span<int> p)
{
    for (int& x : p) ++x;
}

void use2(int m)
{
    const int n = 10;
    int a[n] = {};
    for (int i = 0; i < m; i++) cout << a[i] << " ";
    cout << endl;
    increment2(a); // the number of elements of a need not be repeated
    for (int i = 0; i < m; i++) cout << a[i] << " ";
    cout << endl;
}
int main()
{
    use2(20);
    return 0;
}
0 0 0 0 0 0 0 0 0 0 142747757 32759 1858343536 32760 142743120 32759 2 10 2 19 
1 1 1 1 1 1 1 1 1 1 142747757 32759 10 0 937425632 128 2 10 18 20 

从以上输出结果可见, increment2 只增加了数组的 10 个元素,数组后的两个数据前后没有变化。

例子

以下代码定义了 Date 对象,在 user2 中,获取到了 Date 却又把它转成 string 。之后,又用extract_datestring 转成 Date 。做了一次多余的数据转换操作,实在没有必要。

Date read_date(istream& is);    // read date from istream

Date extract_date(const string& s);    // extract date from string

void user1(const string& date)    // manipulate date
{
    auto d = extract_date(date);
    // ...
}

void user2()
{
    Date d = read_date(cin);
    // ...
    user1(d.to_string());
    // ...
}

例子

过度的代码检测开销过大。很多时候过早的检测也效率低下。不要在复杂度为 O(1) 的接口上增加O(n)的检查。

class Jet {    // Physics says: e * e < x * x + y * y + z * z
    float x;
    float y;
    float z;
    float e;
public:
    Jet(float x, float y, float z, float e)
        :x(x), y(y), z(z), e(e)
    {
        // Should I check here that the values are physically meaningful?
    }

    float m() const
    {
        // Should I handle the degenerate case here?
        return sqrt(x * x + y * y + z * z - e * e);
    }

    //???
};

以上代码定义了喷气式飞机的物理参数。物理规律规定 e * e < x * x + y * y + z * z 。但是这个规律不是一个不变式,比如测量错误会出现违反物理规律的情况。

强化

  • 查看指针和数组:是否尽早进行了范围检测,检测是否重复多多余?
  • 查看类型转换:能否避免窄转换?
  • 查看没有进行检测的输入值
  • 查看结构化数据是否又转成字符串使用?