modernize-loop-convert

此检查将 for(...; ...; ...) 循环转换为使用 C++11 中新的基于范围的循环。

三种类型的循环可以转换

MinConfidence 选项

risky

在容器表达式比仅对已声明表达式(变量、函数、枚举等)的引用更复杂,并且其一部分出现在循环中的其他位置的循环中,我们降低了对转换的信心,因为语义变化的风险增加。这些循环的转换被标记为 risky,因此只有在将最低所需置信度级别设置为 risky 时才会进行转换。

int arr[10][20];
int l = 5;

for (int j = 0; j < 20; ++j)
  int k = arr[l][j] + l; // using l outside arr[l] is considered risky

for (int i = 0; i < obj.getVector().size(); ++i)
  obj.foo(10); // using 'obj' is considered risky

请参阅 基于范围的循环仅评估 end() 一次,了解在将最低所需置信度级别设置为 risky 时不正确转换的示例。

reasonable(默认)

如果循环在每次迭代后调用 .end().size(),则该循环的转换将被标记为 reasonable,因此如果将所需置信度级别设置为 reasonable(默认)或更低,则该循环将被转换。

// using size() is considered reasonable
for (int i = 0; i < container.size(); ++i)
  cout << container[i];

safe

任何其他不符合上述标准被标记为 riskyreasonable 的循环都将被标记为 safe,因此如果将所需置信度级别设置为 safe 或更低,则该循环将被转换。

int arr[] = {1,2,3};

for (int i = 0; i < 3; ++i)
  cout << arr[i];

示例

原始

const int N = 5;
int arr[] = {1,2,3,4,5};
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

// safe conversion
for (int i = 0; i < N; ++i)
  cout << arr[i];

// reasonable conversion
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
  cout << *it;

// reasonable conversion
for (vector<int>::iterator it = begin(v); it != end(v); ++it)
  cout << *it;

// reasonable conversion
for (vector<int>::iterator it = std::begin(v); it != std::end(v); ++it)
  cout << *it;

// reasonable conversion
for (int i = 0; i < v.size(); ++i)
  cout << v[i];

// reasonable conversion
for (int i = 0; i < size(v); ++i)
  cout << v[i];

在将最小置信度级别设置为 reasonable(默认)的情况下应用检查后

const int N = 5;
int arr[] = {1,2,3,4,5};
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

// safe conversion
for (auto & elem : arr)
  cout << elem;

// reasonable conversion
for (auto & elem : v)
  cout << elem;

// reasonable conversion
for (auto & elem : v)
  cout << elem;

反向迭代器支持

转换器还可以转换使用 rbeginrend 反向遍历容器的迭代器循环。开箱即用,这将在 C++20 模式下使用 ranges 库自动发生,但是可以配置检查以在没有 C++20 的情况下工作,方法是指定一个函数来反转范围,并可选地指定该函数所在的标头文件。

UseCxx20ReverseRanges

当设置为 true 时,在 C++20 或更高版本模式下使用 std::ranges::reverse_view 转换循环。默认值为 true

MakeReverseRangeFunction

指定用于反转迭代器对的函数,该函数应接受一个具有 rbeginrend 方法的类,并返回一个具有 beginend 方法的类,这些方法分别调用 rbeginrend 方法。常见示例是 ranges::reverse_viewllvm::reverse。默认值为一个空字符串。

MakeReverseRangeHeader

指定 MakeReverseRangeFunction 声明所在的头文件。对于前面的示例,此选项将设置为 range/v3/view/reverse.hppllvm/ADT/STLExtras.h。如果这是一个空字符串,并且 MakeReverseRangeFunction 被设置,则检查将继续进行,假设该函数已在翻译单元中可用。这可以放在尖括号中,以表示将包含添加为系统包含。默认值为一个空字符串。

IncludeStyle

一个字符串,指定使用的包含样式,llvmgoogle。默认为 llvm

限制

在某些情况下,该工具可能会错误地执行删除信息并更改语义的转换。该工具的用户应注意以下情况概述的检查的行为和限制。

循环头内的注释

原始循环头内的注释将被忽略并在转换时删除。

for (int i = 0; i < N; /* This will be deleted */ ++i) { }

基于范围的循环仅评估 end() 一次

C++11 基于范围的 for 循环仅在循环初始化期间调用 .end() 一次。如果在原始循环中,.end() 在每次迭代后被调用,则转换后循环的语义可能不同。

// The following is semantically equivalent to the C++11 range-based for loop,
// therefore the semantics of the header will not change.
for (iterator it = container.begin(), e = container.end(); it != e; ++it) { }

// Instead of calling .end() after each iteration, this loop will be
// transformed to call .end() only once during the initialization of the loop,
// which may affect semantics.
for (iterator it = container.begin(); it != container.end(); ++it) { }

如上所述,在循环体中调用容器的成员函数被认为是 risky。如果调用的成员函数修改了容器,则由于 .end() 仅被调用一次,因此转换后的循环的语义将不同。

bool flag = false;
for (vector<T>::iterator it = vec.begin(); it != vec.end(); ++it) {
  // Add a copy of the first element to the end of the vector.
  if (!flag) {
    // This line makes this transformation 'risky'.
    vec.push_back(*it);
    flag = true;
  }
  cout << *it;
}

上面的原始代码打印出容器的内容,包括新添加的元素,而下面的转换后的循环仅打印原始内容,不包括新添加的元素。

bool flag = false;
for (auto & elem : vec) {
  // Add a copy of the first element to the end of the vector.
  if (!flag) {
    // This line makes this transformation 'risky'
    vec.push_back(elem);
    flag = true;
  }
  cout << elem;
}

如果 .end() 有副作用,语义也会受到影响。例如,如果在每次迭代后调用 .end() 会被记录,那么如果 .end() 最初是在每次迭代后被调用的,则转换后的循环中的语义将发生变化。

iterator end() {
  num_of_end_calls++;
  return container.end();
}

重载的 operator->() 具有副作用

同样,如果 operator->() 被重载为具有副作用,例如记录,则语义会发生变化。如果迭代器的 operator->() 用于原始循环,它将被替换为 <container element>.<member>,因为基于范围的 for 循环的隐式解引用。因此,重载的 operator->() 的任何副作用都将不再执行。

for (iterator it = c.begin(); it != c.end(); ++it) {
  it->func(); // Using operator->()
}
// Will be transformed to:
for (auto & elem : c) {
  elem.func(); // No longer using operator->()
}

容器的指针和引用

虽然检查的大部分风险分析都致力于确定迭代器或容器是否在循环内被修改,但可以通过指针或引用访问和修改容器来规避分析。

如果使用容器而不是使用指针或引用,则以下转换将仅在 risky 级别应用,因为调用容器的成员函数被认为是 risky。检查无法识别与容器关联的表达式,这些表达式与循环头中使用的表达式不同,因此下面的转换最终在 safe 级别执行。

vector<int> vec;

vector<int> *ptr = &vec;
vector<int> &ref = vec;

for (vector<int>::iterator it = vec.begin(), e = vec.end(); it != e; ++it) {
  if (!flag) {
    // Accessing and modifying the container is considered risky, but the risk
    // level is not raised here.
    ptr->push_back(*it);
    ref.push_back(*it);
    flag = true;
  }
}

OpenMP

由于基于范围的 for 循环仅从 OpenMP 5 开始可用,因此不应在对 OpenMP 版本 5 之前的兼容性有要求的代码上使用此检查。此检查不尝试排除 OpenMP 5 之前的 for 循环的错误诊断,这是故意的

为了防止此检查应用(并破坏)OpenMP for 循环,但仍然应用于非 OpenMP for 循环,建议在特定 for 循环上使用 NOLINT(请参阅 抑制不需要的诊断)。