SPrinter:一个基于Clang-Tidy的C++程序智能指针错误检查工具

最近项目中需要对C++智能指针做一些代码合入前检查,特别的,发现我们使用的Cppcheck, PCLint+都只支持对一般指针的检查,不支持智能指针,在网上看到这个SPrinter, 发现它比较全面的支持了常见的智能指针错误检查,基于其论文,在这里对智能指针错误类型及SPrinter工具的使用方法做一个简要介绍。

背景介绍

C++中动态分配的内存需要程序员自己管理,稍有不慎就会造成内存错误(Memory errors: ME): 常见的ME有如下几种:

内存泄漏(Memory Leak: ML)
int main() {
    // OK
    int *p = new int;
    delete p; 

    // Memory leak below
    int *q = new int;
    // no delete for q
}
双重释放(Double Free: DF)
int main() {
    // OK
    int * p = new int;
    delete p; 
	...
	
    // Double free below
    delete p;
}
访问已释放的内存(Use after Free: UaF)
int* n = new int{1};
delete n;
...
// Use after free below
std::cout << *n << std::endl; 

针对这些问题,通过采用RAII技术,C++中引入了智能指针(Smart Pointers: SP)来自动释放不再需要的内存。SP可以减少内存错误(ME),但是也会引入更难以发现的ME,这些错误可以称为智能指针错误(Smart Pointer Error: SPE)。
智能指针对对象的管理也可以称之为对对象所有权的管理。违反RAII原则,可能导致bug出现。常见的违反RAII原则的情形有:

  • 未察觉的所有权转换(Unconscious Ownership Transfer)
  • 所有权丢失(leaked ownership)
  • 所有权分叉(forked ownership)
  • 无效的内存所有权(invalid memory ownership)

违反这些原则,可能导致的内存错误的对应关系如下:
在这里插入图片描述
表格来自SPrinter论文,其中:FMnH:freed memory not on heap,
NDTM:new and delete operator type mismatch


使用样例

下面分别说明于Clang-Tidy的SPrinter工具如何检测上面表中的问题,所有示例均来自SPrinter论文

未察觉的所有权转换(Unconscious Ownership Transfer)

由于std::auto_ptr的拷贝构造函数会转移被拷贝的ptr的所有权, std::unique_ptr没有可被调用的public的拷贝构造函数,因而不能将它们放在STL Containers中, 如果存入,就可能会有Unconscious Ownership Transfer发生,比如下面的例子:
在这里插入图片描述
存在问题的地方有:

  • 第6行:将auto_ptr对象作为类的成员变量,且该类没有提供拷贝构造函数(意味着使用默认拷贝构造),那么auto_ptr成员变量在进行默认拷贝构造时将会转移被拷贝的对象的成员的ptr的所有权,SPrinter报错如下:

    warning: Class declares a field of type `auto_ptr' with default copy constructor and/or copy assignment operator. [smartpointersafety-private-autoptr-in-class]
        std::auto_ptr<Group1> self;
        ^
    
    
  • 第10行:auto_ptr放入STL container中(如果vector扩容,就会发生意想不到的结果):

     warning: Put `auto_ptr' in STL containers may cause unexpected behaviour under some circumstances. [smartpointersafety-autoptr-in-container]
        std::vector<std::auto_ptr<Group1>> obj(2);
        ^
    
    
  • 第13行:拷贝构造传值后ap已经置为NULL,14行解引用ap将会导致程序crash,SPrinter报错如下:

     warning: Ownership transfer of `auto_ptr' happens here: [smartpointersafety-autoptr-ownership-transfer]
    obj[1]->foo(ap);
                ^
    
    
所有权丢失(leaked ownership)

std::unique_ptr的release()方法的返回值是指向原对象的指针,同时释放原对象的所有权,但是并不释放内存(析构对象),因此假如有如下代码:

 std::unique_ptr<int> ptr(new int{3});
 int* p = ptr.release();
 if (ptr == NULL) std::cout << "ptr is NULL now" << std::endl; // ptr == ptr.get() == NULL
 std::cout <<*p << std::endl; // *p is 3
 // Must delete p, or memory leak
 delete p;

后面的delete p;是必须的,否则就会造成内存泄漏;同时,调用完release后,原来的ptr就会变为NULL;

std::unique_ptr的get()方法的返回值是指向原对象的指针,但是不释放原对象的所有权。因此如果像下面这样,最后delete p; p指向的内存将会在ptr析构时再次释放,造成double free。

void foo() {
	 std::unique_ptr<int> ptr(new int{3});
	 int* p = ptr.get();
	 std::cout <<*p << std::endl; // *p is 3
	 // do not delete p, or double free
	 delete p;
}

std::unique_ptr的reset()方法默认参数是NULL,返回值是空,它释放原对象的所有权同时释放内存,并将ptr置为NULL。
std::shared_ptr没有release()方法,因为可能有多个shared pointer指向同一个对象,所以不可能通过release()来达到std::unique_ptr的release()的效果:比如下面来自boost shared_ptr FAQ的例子:

shared_ptr<int> a(new int);
shared_ptr<int> b(a); // a.use_count() == b.use_count() == 2

int * p = a.release();

如果release()减少了user_count, 那么b和a析构的时候最后的user_count是多少?另外如果外部调用delete p释放对象内存,shared_ptr是否还需要释放内存?基于这些考虑,std::shared_ptr没有release()方法。
比如下面的例子:
在这里插入图片描述
这段程序用SPrinter检查会提示下面的warning:

warning: Undeallocated released pointer may cause memory leaked. [smartpointersafety-undeallocated-released-pointer]
    return *p.release();
            ^

但这个程序本身没有内存方面的问题:

  • 第16行通过get()方法将指针的所有权存在了vector中,但p本身还拥有对象的所有权
  • 第18行通过release()方法释放了对象的所有权,因此第7行的delete不会造成double free

但是这段代码是不易维护,不易扩展的,它很容易出bug:

  • 如果第18行没有调用release()方法,就会造成double free
  • 如果第14行定义的是shared_ptr, 因为shared_ptr没有release()方法,因此不可能有18行存在,必然会造成double free
  • 如果第18行返回的是release()获得的指向对象的指针,并在外部调用处delete,也会造成double free

最佳的实践守则是:

  • 不要调用delete释放通过get()获得的指针所指的内存;
  • 必须通过delete等机制释放release()获得的指向对象的指针所指的内存;

推荐的写法如下:

class Container {
public:
// Acquire ownership:
    void take(int* p) {
        v.push_back(p);
    }
// Destruct memory:
    Container() {
        for (auto &i : v) delete i;
    }
private:
    std::vector<int*> v;
} C;

int foo() {
    std::unique_ptr<int> p = std::make_unique<int>(42);

    int ret = *p;
    C.take(p.release()); //Transfer ownership from p to C.v
    return ret;
}
所有权分叉(forked ownership)

所有权分叉是指同一份内存交给两个完全独立的智能指针进行管理,如下:

在这里插入图片描述
由于不同的智能指针拥有独立的析构函数,上面的函数势必会造成Double free; 如果想让不同的智能指针管理,比如将第4行改为sp2是通过sp1拷贝构造的,这虽然可以避免Double free的问题,但是还是有潜在的风险,如果foo()函数后续有人添加代码,不可避免有人会写出第3行类似的代码来初始化新的sp3指针。正确的原则是:不要使用raw pointer来初始化智能指针。

采用Sprinter来检查上面的代码片段,给出了非常有用的提示信息:

warning: Raw pointer used for initiating different smart pointers for twice. [smartpointersafety-raw-pointer-initiation]
    int* p = new int(42);
         ^
warning: Initiating smart pointer with raw pointer. [smartpointersafety-raw-pointer-initiation]
    std::shared_ptr<int> sp1(p);
                             ^
无效的内存所有权(invalid memory ownership)

无效的内存所有权主要有三种情形:

  1. 通过智能指针管理栈上(非堆)内存(智能指针只能管理堆上创建的对象)

  2. 通过智能指针管理堆上创建的数组:

    shared_ptr<int> sp(new int[10]);
    

    上面的代码,由于shared_ptr指向的是一个int 数组,但是释放内存时仍然会调用delete ptr; 而不是delete [] ptr, 因此不推荐使用智能指针管理堆上创建的数组,SPrinter在上面情况也会报错。
    但是如果提供自定义的deleter, 如下面这样,也是可以实现采用智能指针管理堆上数组的:

    template< typename T >
    struct array_deleter
    {
      void operator ()(T const * p)
      { 
        delete [] p; 
      }
    };
    std::shared_ptr<int> sp(new int[10], array_deleter<int>());
    // or as below
    std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
    // or use lambda expression
    std::shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
    

    在C++17中,不需要提供自定义的deleter,只需要声明智能指针时就直接支持了这种用法:

    std::shared_ptr<int[]> sp(new int[10]);
    
  3. 访问释放了的内存,必然会造成crash或者undefined behavior

下面的示例代码:
在这里插入图片描述
第3行对应上面的第1种情形,第4行对应第2种情形,第7行对应第三种情形:由于tp过了作用域后自动释放,解引用wp.lock()返回的内存地址时非法的,推荐的用法是对于任意的weak_ptr,解引用通过lock()返回的内存地址均必须先检查其是否为NULL。

采用SPrinter工具检查上面的代码,得到的warning分别是:

warning: Initiating smart pointer with non-allocated memory. [smartpointersafety-non-allocated-memory-initiation]
    std::unique_ptr<int> up(&v);
                            ^
warning: Initiation CXXNewExpr type dose not match its declaration type `int'. [smartpointersafety-type-mismatch-initiation]
    std::auto_ptr<int> ap(new int[42]);
                          ^
warning: Validity of locked weak_ptr is unchecked, may have been expired. [smartpointersafety-unchecked-locked-weak-pointer]
    *wp.lock() = 0;
    ^

SPrinter的实现机制

SPrinter是基于Clang-Tidy框架:Clang-Tidy当检查一个源文件时,源文件的代码会被转换为抽象语法树(AST),Clang-Tidy内部会注册一些语法检查的matcher来遍历AST,当某个matcher匹配时,就会提示相应的warning或error信息,SPrinter就是提供了一些这样的matcher来检查智能指针中潜在的使用风险。

SPrinter的不足

基于SPrinter的实现机制,SPrinter只能通过分析AST来查找某种模式是否存在,如下基于数据流的NULL check不能检查:

  • 情形1:

    std::unique_ptr<int> ptr;
    	// ...
    	// ...
    *ptr;
    
  • 情形2:

    std::unique_ptr<int> ptr(new int(3));
    	// ...
    std::unique_ptr<int> ptr2 = ptr;
    	// ...
    *ptr;
    

SPrinter的优点

SPrinter通过分析AST的方法来实现检查, 准确率较高,使用起来独立方便,其命令行使用方法如下:

python ${llvm_path}/share/clang/run-clang-tidy.py -p ${repo_path}  -clang-tidy-binary ${llvm_path}/bin/clang-tidy -quiet -checks='smartpointersafety-*' -j 8 -extra-arg=-ferror-limit=0 ${directory_or_file_to_check}

注意点:需要结合compile database(compile_commands.json)等使用,${repo_path}目录下应有compile database文件


Reference:

  1. https://github.com/Snape3058/SPrinter
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值