union + shared_ptr 导致的一次内存泄漏

https://zhuanlan.zhihu.com/p/56161425

Union 在 C 中经常出现,shared_ptr 也广泛用于 C++ 中,两者结合在一起,如果不严格按照 C++ 规范来使用,会惹下大麻烦。

下面是一个抽象出来的最简代码,来复现 Union + shared_ptr 内存泄漏问题:

#include <iostream>
#include <memory>

class A {
    public:
    A() {
        std::cout << "A()";
    }
    A(const A& a) {
        std::cout << "A(const A&)";
    }
    ~A() {
        std::cout << "~A() " << std::endl;
    }
};

union U {
    public:
    U() {};
    ~U() {};
    
    struct {
        std::shared_ptr<A> a;
    } a;
    struct {
        int b;
    }b;
};


int main() {
    U u;
    u.a.a = std::make_shared<A>();
    return 0;
}

输出:
A()

可以看到,shared_ptr 就这么无声无息的被泄漏了。事实上,泄漏 shared_ptr 没有那么容易。

能想到的有:

  1. shared ptr 循环引用;
  2. 脑抽在堆上分配 shared ptr;

但是这里,通过 Union + Shared_ptr,就轻而易举的泄漏掉了 shared_ptr。

我们来分析一下 Union 的特性。

从 C++ 14 标准 [2] 可以查阅到:

If any non-static data member of a union has a non-trivial default constructor (12.1), copy constructor (12.8), move constructor (12.8), copy assignment operator (12.8), move assignment operator (12.8), or destructor (12.4), the corresponding member function of the union must be user-provided or it will be implicitly deleted (8.4.3) for the union.

也就是说,只要 Union 里面包含了非平凡的 member,那么就需要定义构造和析构函数,才能安全的初始化对象,以及释放资源。

回头看上面的例子,其实 C++ 规范考虑到了可能没有释放的问题,强制要求定义构造析构函数,给你一次机会,释放资源,但是我们的例子,析构是空,错过了最后释放的机会。

继续看手册:

Consider an object u of a union type U having non-static data members m of type M and n of type N. If M has a non-trivial destructor and N has a non-trivial constructor (for instance, if they declare or inherit virtual functions), the active member of u can be safely switched from m to n using the destructor and placement new operator as follows:

u.m.~M();
new (&u.n) N;

也就是说,如果定义了一个 非平凡 的成员,从一个 active member 切换到另外一个 member,需要手动调用 desctruct 和 placement new。否则,造成了资源释放不干净,如果是 shared ptr,就是内存泄漏了。下面是正确切换以及释放 union 的 active member 的例子:

#include <iostream>
#include <string>
#include <vector>
 
union S
{
    std::string str;
    std::vector<int> vec;
    ~S() {} // needs to know which member is active, only possible in union-like class 
};          // the whole union occupies max(sizeof(string), sizeof(vector<int>))
 
int main()
{
    S s = {"Hello, world"};
    // at this point, reading from s.vec is undefined behavior
    std::cout << "s.str = " << s.str << '\n';
    s.str.~basic_string();
    new (&s.vec) std::vector<int>;
    // now, s.vec is the active member of the union
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';
    s.vec.~vector();
}

为了避免手动释放资源,C++ 17 的 std::variant 在部分场景,可以解决这个问题。

std::variant

C++ 17 提供的 std::variant 在很多时候,可以替代 union 的功能。例如:

#include <iostream>
 
// S has one non-static data member (tag), three enumerator members (CHAR, INT, DOUBLE), 
// and three variant members (c, i, d)
struct S
{
    enum{CHAR, INT, DOUBLE} tag;
    union
    {
        char c;
        int i;
        double d;
    };
};
 
void print_s(const S& s)
{
    switch(s.tag)
    {
        case S::CHAR: std::cout << s.c << '\n'; break;
        case S::INT: std::cout << s.i << '\n'; break;
        case S::DOUBLE: std::cout << s.d << '\n'; break;
    }
}
 
int main()
{
    S s = {S::CHAR, 'a'};
    print_s(s);
    s.tag = S::INT;
    s.i = 123;
    print_s(s);
}

可以通过使用 std::variant 简写成:

#include <variant>
#include <iostream>
 
int main()
{
    std::variant<char, int, double> s = 'a';
    std::visit([](auto x){ std::cout << x << '\n';}, s);
    s = 123;
    std::visit([](auto x){ std::cout << x << '\n';}, s);
}

更多 std::variant 的例子如下:

#include <variant>
#include <iostream>


struct aa {
  void operator()(char x ) {std::cout << "char " << x << std::endl;}
  void operator()(int x){std::cout << "int " << x << std::endl;}
  void operator()(double x) {std::cout << "double " << x << std::endl;}
};

struct v1 {
  void f() {std::cout << "v1" << std::endl;}   
};

struct v2 {
  void f() {std::cout << "v2" << std::endl;}  
};
 
int main()
{
    std::variant<char, int, double> s = 'a';
    std::variant<struct v1, struct v2> v = v2();
    auto b = std::get<char>(s);
    std::cout << b << std::endl;

    std::visit([](auto& x) {x.f();}, v);
    s = 123;
    auto c = std::get<int>(s);
    std::cout << c << std::endl;
    std::visit(aa(), s);

}

结合 shared ptr,可以看到,std::variant 可以安全的释放掉资源。

#include <memory>
class A {
    public:
    A() {
        std::cout << "A()";
    }
    A(const A& a) {
        std::cout << "A(const A&)";
    }
    ~A() {
        std::cout << "~A() " << std::endl;
    }
};

typedef std::shared_ptr<A> APtr;

int main()
{
    std::variant<int, APtr> test = std::make_shared<A>();
    return 0;
}

输出:
A()~A()

总结

这种问题,如果是历史遗留代码,极难定位。所以需要防范于未然。在 code review 阶段,就需要及时制止这种行为。

办法总比困难多,将 non-trival 成员提升到外部,或者使用 C+ + 17 新加的 std::variant 来确保安全构造,和析构 non-trival 成员,避免手动调用 destruct 和 placement new,将出问题的风险降到最低。

参考:

[1] Union declaration

[2] Unions - C++14

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值