15 April 2023

C++ 核心指南目录

“Keep use of pointers simple and straightforward”

理由

复杂的指针操作是导致故障的根源。

注意

请使用gsl::span。指针应该只指向一个对象。指针的数值计算很容易出错误,是大量严重 bug 和安全隐患的根源。用 span就能以边界检测、类型安全的方式访问数组数据。如果我们通过常量访问已知边界长度的数组,编译器能够帮助我们验证数组下标是否合法。

坏例子

void f(int* p, int count)
{
    if (count < 2) return;

    int* q = p + 1;    // BAD

    ptrdiff_t d;
    int n;
    d = (p - &n);      // OK
    d = (q - p);       // OK

    int n = *p++;      // BAD

    if (count < 6) return;

    p[4] = 1;          // BAD

    p[count - 1] = 2;  // BAD

    use(&p[0], 3);     // BAD
}

好例子

void f(span<int> a) // BETTER: use span in the function declaration
{
    if (a.size() < 2) return;

    int n = a[0];      // OK

    span<int> q = a.subspan(1); // OK

    if (a.size() < 6) return;

    a[4] = 1;          // OK

    a[a.size() - 1] = 2;  // OK

    use(a.data(), 3);  // OK
}

注意

对于用变量作为下标访问数组的操作,不管是工具还是程序员都很难验证其正确性与安全性。 span 在运行时会被边界检测,就能以类型安全的方式访问数组数据。at()是另一个解决方案,可以确保单次访问是被边界检测的。如果需要迭代器访问数组数据,请使用从数组的 span 生成的迭代器。

坏例子

void f(array<int, 10> a, int pos)
{
    a[pos / 2] = 1; // BAD
    a[pos - 1] = 2; // BAD
    a[-1] = 3;    // BAD (but easily caught by tools) -- no
                  // replacement, just don't do this
    a[10] = 4;    // BAD (but easily caught by tools) -- no
                  // replacement, just don't do this
}

好例子

使用 span

void f1(span<int, 10> a, int pos) // A1: Change parameter type to use span
{
    a[pos / 2] = 1; // OK
    a[pos - 1] = 2; // OK
}

void f2(array<int, 10> arr, int pos) // A2: Add local span and use that
{
    span<int> a = {arr.data(), pos};
    a[pos / 2] = 1; // OK
    a[pos - 1] = 2; // OK
}

使用 at()

void f3(array<int, 10> a, int pos) // ALTERNATIVE B: Use at() for access
{
    at(a, pos / 2) = 1; // OK
    at(a, pos - 1) = 2; // OK
}

坏例子

void f()
{
    int arr[COUNT];
    for (int i = 0; i < COUNT; ++i)
        arr[i] = i; // BAD, cannot use non-constant indexer
}

好例子

使用 span

#include <gsl/gsl>
using namespace gsl;
constexpr int COUNT = 10;
void f1()
{
    int arr[COUNT];
    span<int> av = arr;
    for (int i = 0; i < COUNT; ++i) {
        av[i] = i;
        cout << av[i] << ", ";
    }
}
int main()
{
    f1();
    return EXIT_SUCCESS;
}
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 

使用 span 和带范围的 for

#include <gsl/gsl>
using namespace gsl;
constexpr int COUNT = 10;
void f1a()
{
     int arr[COUNT];
     span<int, COUNT> av = arr;
     int i = 0;
     for (auto& e : av) {         
         e = i++;
         cout << e << ", ";
     }
}
int main()
{
    f1a();
    return EXIT_SUCCESS;
}
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 

使用at()访问

#include <gsl/gsl>
using namespace gsl;
constexpr int COUNT = 10;
void f2()
{
    int arr[COUNT];
    for (int i = 0; i < COUNT; ++i) {
        at(arr, i) = i;
        cout << at(arr, i) << ", ";
    }
}
int main()
{
    f2();
    return EXIT_SUCCESS;
}
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 

使用带范围的 for

constexpr int COUNT = 10;
void f3()
{
    int arr[COUNT];
    int i = 0;
    for (auto& e : arr) {
         e = i++;
         cout << e << ", ";
    }
}
int main()
{
    f3();
    return EXIT_SUCCESS;
}
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 

注意

工具可以用at()替换掉用动态索引访问数组的代码:

#include <gsl/gsl>
using namespace gsl;
static int a[10];

void f(int i, int j)
{
    a[i + j] = 12;      // BAD, could be rewritten as ...
    at(a, i + j) = 12;  // OK -- bounds-checked
    cout << at(a, i + j);
}
int main()
{
    f(2, 3);
    return EXIT_SUCCESS;
}
12

例子

如果用指针传递数组,则没有机会进行边界检测了。所以尽量避免。

void g(int* p);

void f()
{
    int a[5];
    g(a);        // BAD: are we trying to pass an array?
    g(&a[0]);    // OK: passing one object
}

如果你要传递一个数组,可以这样写:

void g(int* p, size_t length);  // old (dangerous) code

void g1(span<int> av); // BETTER: get g() changed.

void f2()
{
    int a[5];
    span<int> av = a;

    g(av.data(), av.size());   // OK, if you have no choice
    g1(a);                     // OK -- no decay here, instead use
                               // implicit span ctor
}

强化

  • 标记计算结果为指针类型,针对指针的算术操作。
  • 标记索引下标不是有效范围内的常量,且针对表达式或数组类变量的下标操作。
  • 标记隐式地将数组类型转换成指针的操作。

此规则是边界安全规则集的一部分。