博客 阅读需 7 分钟

现代化您的 C++ 代码的简单步骤

分享本文
Easy steps to modernize your C++ code.

最近,C++ 社区一直在倡导使用新标准来现代化现有的代码库。然而,早在 C++11 标准发布之前,Andrei Alexandrescu、Scott Meyers 和 Herb Sutter 等知名 C++ 专家就已经在以“现代 C++ 设计”的名义推广泛型编程了。下面是 Andrei Alexandrescu 对现代 C++ 设计的论述:

现代 C++ 设计定义并系统化地使用泛型组件——高度灵活的设计产物,可以混搭组合,用一套小巧、正交的代码体获得丰富的行为。

他的论述中有三点格外突出:

  • 现代 C++ 设计定义并系统化地使用泛型组件的调用。
  • 高度灵活的设计。
  • 用一套小巧、正交的代码体获得丰富的行为。

现代化您的 C++ 代码,不仅仅是使用新标准;我们还可以运用一些泛型编程的最佳实践来改进代码库。让我们先了解几个手动现代化代码库的简单步骤;在第二部分中,我们将探讨如何自动完成现代化。

一、手动现代化您的源代码

让我们以一个算法为例,试着把它现代化。算法用于计算、数据处理和自动推理。编写它们并不总是轻而易举,这取决于它们的复杂程度。在 C++ 中,人们为简化算法的实现并使其更强大付出了巨大努力。

让我们试着现代化这个快速排序算法的实现:

// The partition function
int partition(int* input,int p,int r){
        int pivot = input[r];
        while( p < r ){
                 while( input[p]< pivot )
                     p++;
                 while( input[r]> pivot )
                    r--;
                if( input[p]== input[r])
                    p++;
                elseif( p < r ){
                     int tmp = input[p];
                     input[p]= input[r];
                     input[r]= tmp;
                }
        }
         return r;
}
// The quicksort recursive function
void quicksort(int* input,int p,int r){
        if( p < r ){
              int j = partition(input, p, r);        
              quicksort(input, p, j-1);
              quicksort(input, j+1, r);
        }
}

从宏观上看,算法有一些共同特征:

  • 使用某种元素类型的容器,并对其进行遍历。
  • 元素之间的比较。
  • 当然,还有对元素的一些处理。

在我们的实现中,容器是一个 int 的原始数组;我们通过递增和递减来遍历。我们用“<”和“>”进行比较,还有一些像交换数据这样的操作。

让我们试着改进其中的每一个方面:

第一步:用迭代器替换容器

使用非泛型容器会迫使我们使用特定的元素类型。要把同样的算法应用到其他类型上,我们就得复制粘贴代码。泛型容器通过支持任意元素类型解决了这个问题;例如,对于我们的快速排序算法,可以使用 std::vector<T> 作为容器,而不是原始数组。

原始数组或 std::vector 只是表示一组元素的众多方式之一;我们也可以把同样的算法应用到链表、队列或任何其他容器上。为此,迭代器是抽象掉底层容器的最佳选择。

迭代器是一个指向区间中某个元素的对象,它可以使用一组运算符(至少包括递增(++)和解引用(*)运算符)遍历该区间的元素。迭代器根据所实现的功能分为五类:输入(Input)、输出(Output)、前向(Forward)、双向(Bidirectional)和随机访问(Random Access)。

在我们的算法中,必须指定使用哪种迭代器。为此,我们需要检测用到了哪些迭代操作。对于快速排序算法,会用到递增和递减,因此需要双向迭代器。使用迭代器,我们可以这样定义这个方法:

template< typename BidirectionalIterator >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last )

第二步:尽可能让比较器泛型化

对于某些算法,被处理的元素不一定是数字;它们可能是字符串或类对象。在这种情况下,让比较器泛型化可以给我们带来一个复用性更强的算法。

快速排序算法也可以应用于字符串列表;因此,最好使用泛型比较器。

使用泛型比较器之后,定义可以这样修改:

template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp )

第三步:用标准操作替换自定义操作

许多算法会用到 min、max 和 swap 等常见操作。对于这些操作,最好不要重复造轮子,而是使用 <algorithm> 头文件中的标准实现。

在我们的例子中,可以使用 STL 的 swap 方法,而不是创建我们自己的特定方法。

std::iter_swap( pivot, left );

下面是经过这三个步骤修改后的结果:

#include <functional>
#include <algorithm>
#include <iterator>
 
template< typename BidirectionalIterator, typename Compare >
void quick_sort( BidirectionalIterator first, BidirectionalIterator last, Compare cmp ) {
    if( first != last ) {
        BidirectionalIterator left  = first;
        BidirectionalIterator right = last;
        BidirectionalIterator pivot = left++;
 
        while( left != right ) {
            if( cmp( *left, *pivot ) ) {
                ++left;
            } else {
                while( (left != right) && cmp( *pivot, *right ) )
                    --right;
                std::iter_swap( left, right );
            }
        }
 
        --left;
        std::iter_swap( pivot, left );
 
        quick_sort( first, left, cmp );
        quick_sort( right, last, cmp );
    }
}
 
template< typename BidirectionalIterator >
    inline void quick_sort( BidirectionalIterator first, BidirectionalIterator last ) {
        quick_sort( first, last,
                std::less_equal< typename std::iterator_traits< BidirectionalIterator >::value_type >()
                );
    }

这个实现有以下优点:

  • 它可以应用于多种元素类型。
  • 容器可以是 vector、set、list 或任何其他具有双向迭代器的容器。
  • 它使用了经过充分优化和测试的标准函数。

二、自动现代化

自动检测出可以使用 C++11/C++14/C++17 特性的地方,并在可能的情况下自动修改代码,是很有用的。针对这样的需求,clang-tidy是一个独立的工具,可在适当的情况下,自动把基于旧标准编写的 C++ 代码转换为使用最新 C++ 标准的特性。

下面是 clang-tidy 可以检测出代码现代化机会的一些情况:

  • Override:检测可以为重写了基类虚函数、但尚未添加 override 说明符的成员函数添加该说明符的地方。
  • 循环转换(Loop Convert):检测 for(…; …; …) 形式的循环,把它们替换为 C++11 中新的基于范围的循环,并给出相应的基于范围的循环表达式。
  • 按值传递(Pass-By-Value):检测哪些 const 引用参数适合改用按值传递的惯用法。
  • auto_ptr:检测对已弃用的 std::auto_ptr 的使用,并将其替换为 std::unique_ptr。
  • auto 说明符:检测变量声明中可以使用 auto 类型说明符的地方。
  • nullptr:检测可替换为 nullptr 的空字面量(如适用)。
  • std::bind:该检查查找std::bind的使用,并把简单的用法替换为 lambda。lambda 会在需要的地方使用值捕获。
  • 已弃用的头文件:C 库中的一些头文件在 C++ 中已被弃用,在 C++ 代码库中不再受欢迎。其中一些在 C++ 中没有作用。更多细节请参阅 C++14 标准 [depr.c.headers] 一节。
  • std::shared_ptr:该检查查找通过显式调用构造函数和std::shared_ptr对象,并结合new表达式创建它的代码,并将其替换为对 C++14 引入的std::make_shared的调用。
  • std::unique_ptr:该检查查找通过显式调用构造函数和std::unique_ptr对象,并结合new表达式创建它的代码,并将其替换为对 C++14 引入的std::make_unique的调用。
  • 原始字符串字面量:该检查有选择地把包含转义字符的字符串字面量替换为原始字符串字面量。

使用 Clang 的开发者可以轻松利用 clang-tidy。Visual C++ 开发者和其他编译器的用户可以使用集成了 clang-tidy 的CppDepend

分享本文