LLVM 项目博客

LLVM 项目新闻和来自战壕的细节

Google Summer of Code: C++ Modernizer 改善

Google Summer of Code (GSoC) 为学生提供奖学金,让他们在夏季参与开源项目。今年,我被选中参与 Clang C++ Modernizer 项目,该项目以前称为 C++11 Migrator,由英特尔团队驱动。该工具的目标是通过使用新 C++ 标准的新功能来现代化 C++ 代码,从而提高可维护性、可读性和编译时间和运行时性能。该项目曾在 4 月份的博客文章 “C++11 Migrator 的现状” 中被介绍,并且一直在不断发展,包括架构和功能方面。

本文介绍了该工具在过去几个月中的改进,包括我今年夏天为 GSoC 做的贡献。有关该工具的完整概述以及如何安装它,请访问文档:https://clang.llvm.net.cn/extra/clang-modernize.html#getting-started。要演示该工具,您可以查看 Chandler Carruth 在 Going Native 2013 上的演讲:The Care and Feeding of C++'s Dragons。clang-modernize 的展示从大约 33 分钟开始。

转换构成翻译单元的所有文件

与上一个版本相比,一个主要改进是能够转换构成翻译单元的所有文件,而不仅仅是主源文件。这意味着如果需要,头文件也会被转换,这使得现代化器更加有用。

为了避免更改不应该更改的文件,例如系统头文件或第三方库的头文件,有一些选项可以控制哪些文件应该被转换。
  • -include 接受一个逗号分隔的路径列表,这些路径允许被转换。整个目录树中每个给定路径下的所有文件都被标记为可修改的。出于安全考虑,默认行为是不转换任何额外文件。
  • -exclude 接受一个逗号分隔的路径列表,这些路径禁止被转换。可以用来从包含的目录树中修剪子树。
  • -include-from-exclude-from 分别等效于 -include 和 -exclude,但以文件名作为参数而不是逗号分隔的路径列表。该文件应包含每行一个路径。
例如,假设目录层次结构为
  • src/foo.cpp
  • include/foo.h
  • lib/third-party.h
要转换 foo.cppfoo.h 但保持 third-party.h 原样,您可以使用以下命令之一
clang-modernize -include=include/ src/foo.cpp -- -std=c++11 -I include/ -I lib/
clang-modernize -include=. -exclude=lib/ src/foo.cpp -- -std=c++11 -I include/ -I lib/

转换

现在总共有 6 个转换,其中 2 个是新的
  1. Add-Override 转换
    为覆盖的成员函数添加 'override' 说明符。
  2. 循环转换
    使用基于范围的 for 循环。
  3. 按值传递转换 [新增]
    替换 const-ref 参数,这些参数将从使用按值传递习语中受益。
  4. 替换 Auto-Ptr 转换 [新增]
    替换对已弃用的std::auto_ptr的用法std::unique_ptr.
  5. 使用 Auto 转换
    在变量声明中使用 auto 类型说明符。
  6. 使用 Nullptr 转换
    在适用时用 nullptr 替换空文字和宏。

Add-Override 的改进

自 4 月份的上一篇文章以来,Add-Override 转换已得到改进,可以处理用户定义的宏。某些项目(如 LLVM)使用一个宏扩展为 'override' 说明符,以与不符合 C++11 的编译器保持向后兼容性。clang-modernize 可以检测这些宏并使用它们,而不是使用 'override' 标识符。

启用此功能的命令行开关是 -override-macros

示例

clang-modernize -override-macros foo.cpp

之前
之后
#define LLVM_OVERRIDE override

struct A {
 
virtual void foo();
};


struct B : A {
 
virtual void foo();
};
#define LLVM_OVERRIDE override


struct A {
 
virtual void foo();
};


struct B : A {
 
virtual void foo() LLVM_OVERRIDE;
};

使用 Nullptr 的改进

此转换也已得到改进,可以处理类似于 NULL 的用户定义的宏。用户可以使用命令行开关 -user-null-macros=<string> 指定哪些宏可以被 nullptr 替换。

示例

clang-modernize -user-null-macros=MY_NULL bar.cpp

之前
之后
#define MY_NULL 0

void bar() {
 
int *p = MY_NULL;
}

#define MY_NULL 0

void bar() {
 
int *p = nullptr;
}

新增转换:替换 Auto-Ptr

此转换是 GSoC 工作的结果。该转换替换了对std::auto_ptr的用法std::unique_ptr的用法。它还会在需要时插入对std::move()的调用。

之前
之后
#include <memory>

void steal(std::auto_ptr<int> x);


void foo(int i) {
 std
::auto_ptr<int> p(new int(i));

 steal(p);
}
#include <memory>

void steal(std::unique_ptr<int> x);


void foo(int i) {
 std
::unique_ptr<int> p(new int(i));

 steal(
std::move(p));
}

新转换:按值传递

这也是 GSoC 的产物,该转换利用了 C++11 中添加的移动语义,以避免对接受具有移动构造函数类型的函数进行复制。通过更改为按值传递语义,如果提供了右值参数,则可以避免复制。对于左值参数,复制次数保持不变。

该转换目前仅限于复制到类字段的构造函数参数。

示例

clang-modernize pass-by-value.cpp

之前之后
#include <string>

class A {
public:
 A(
const std::string &Copied,

const std::string &ReadOnly)
   
: Copied(Copied),

ReadOnly(ReadOnly) {}

private:
 std
::string Copied;
 
const std::string &ReadOnly;
};
#include <string>
#include <utility>

class A {

public:
 A(
std::string Copied,

const std::string &ReadOnly)
   
: Copied(std::move(Copied)),

ReadOnly(ReadOnly) {}

private:
 std
::string Copied;
 
const std::string &ReadOnly;
};

std::move()是 <utility> 中声明的库函数<utility>。如果需要,clang-modernize 会添加此头文件。

这种转换还有很大的改进空间。可能存在其他可以安全转换的情况。热烈欢迎对此方面的贡献!

可用性改进

我们还努力提高现代化程序的整体可用性。调用现代化程序现在需要更少的参数,因为大多数情况下可以推断出参数。
  • 如果没有提供编译数据库或标志,则假定 -std=c++11。
  • 默认情况下启用所有转换。
  • 如果提供了编译数据库,则不需要显式列出文件。现代化程序将从编译数据库中获取文件。使用 -include 选择要使用的文件。
还添加了两个新功能。
  1. 使用 LibFormat 自动重新格式化受转换影响的代码。
  2. 一个新的命令行开关,用于根据编译器支持选择要应用的转换。

重新格式化转换后的代码

LibFormatclang-format 背后的库,clang-format 是一个用于格式化 C、C++ 和 Obj-C 代码的工具。clang-modernize 也使用此库来重新格式化转换后的代码。当使用 -format 启用时,默认样式为 LLVM。-style 选项可以以与 clang-format 相同的方式控制样式。

示例

format.cpp
#include <iostream>
#include <vector>

void f(const std::vector<int> &my_container) {
 
for (std::vector<int>::const_iterator I = my_container.begin(),
                                       E
= my_container.end();
      I
!= E; ++I) {
   std
::cout << *I << std::endl;
 }
}

不重新格式化

$ clang-modernize -use-auto format.cpp
#include <iostream>
#include <vector>

void f(const std::vector<int> &my_container) {
 
for (auto I = my_container.begin(),
                                       E
= my_container.end();
      I
!= E; ++I) {
   std
::cout << *I << std::endl;
 }
}

重新格式化

$ clang-modernize -format -style=LLVM -use-auto format.cpp
#include <iostream>
#include <vector>

void f(const std::vector<int> &my_container) {
 
for (auto I = my_container.begin(), E = my_container.end(); I != E; ++I) {
   std
::cout << *I << std::endl;
 }
}

有关此选项的更多信息,请查看文档:格式化命令行选项.

根据编译器支持选择转换

另一个有用的命令行开关是:-for-compilers。此选项启用给定编译器支持的所有转换。

例如,假设您的项目放弃了对编译器“旧”版本的依赖。您可以自动将代码现代化为要支持的编译器的最新最小版本。

要支持 Clang >= 3.1、GCC >= 4.6 和 MSVC 11

clang-modernize -format -for-compilers=clang-3.1,gcc-4.6,msvc-11 foo.cpp

有关此选项的更多信息,以及查看每个编译器可以使用哪些转换,请阅读文档.

下一步是什么?

并行转换多个翻译单元的能力将很快到来。想想像 make 和 ninja 一样的clang-modernize -j。结果,大型代码库的现代化将变得更快。

更多转换正在发布,以及对现有转换的改进,例如传值转换。

我们将继续修复错误并添加新功能。我们的积压是公开可用的:https://cpp11-migrate.atlassian.net/secure/RapidBoard.jspa?rapidView=1&view=planning

参与进来!

对该工具感兴趣?发现错误?有可以对其他人有用的转换的想法?该项目是开源的,非常欢迎贡献!

现代化器有自己的错误和项目跟踪器。如果你想提交或修复错误,请访问:https://cpp11-migrate.atlassian.net

其他一些需要牢记的地址

最后的话

最后,我要感谢我的导师 Edwin Vane 及其在英特尔的团队、Tareq Siraj 和 Ariel Bernal 为我提供的巨大支持。还要感谢 LLVM 社区和 Google 暑期代码计划团队,让我有机会在这个夏天参与 C++ Modernizer 的工作。

-- Guillaume Papin