【问题标题】:Stroustroup: free(): double free detected in tcache 2 when implementing vector::reserve()Stroustroup: free(): 实现 vector::reserve() 时在 tcache 2 中检测到双重空闲
【发布时间】:2021-08-12 14:27:46
【问题描述】:

Bjarne Stroustrup - 编程原理与实践 - 第 19.5.6 章“向量的 RAII”中给出的代码在调用 push_back() 然后 reserve() 时不起作用。它们分别工作正常。

我收到此错误“free(): double free detected in tcache 2”
通过调试,我看到在 Reserve() 中调用 swap() 后,变量 b.elem[0] 具有

书中的 uninitialized_copy 写错了,所以也许还有其他遗漏?还是我做错了什么?

17.08 更新:我为 vector_base 和 vector 添加了构造函数、析构函数、复制和移动操作(灵感来自 C++ 编程语言),现在它可以工作了。感谢您的回复!

我有一个问题:“编程原理与实践”使vector类成为vector_base类的派生类,而“C++编程语言”使vector_base对象成为vector类的数据成员。我想知道哪个选项更好?不过它们看起来是等价的。

#include <iostream>
#include <stdexcept>
#include <string>
#include <algorithm>

template <typename T>
class allocator
{
public:
    T *allocate(int n) { return (T *)malloc(n * sizeof(T)); } // allocate space for n objects of type T
    void construct(T *p, const T &v) { new (p) T{v}; }        // construct a T with the value v in p
    void destroy(T *p) { p->~T(); }                           // destroy the T in p
    void deallocate(T *p, int n) { free(p); }                 // deallocate n objects of type T starting at p
};

template <typename T, typename A = allocator<T>>
struct vector_base
{
    A alloc;   // allocator,
    T *elem;   // start of allocation
    int sz;    // number of elements
    int space; // amount of allocated space

    vector_base()
        : sz{0}, elem{nullptr}, space{0} {}
    vector_base(const A &a, int n)
        : alloc{a}, elem{alloc.allocate(n)}, sz{0}, space{n} 
    {
    }
    ~vector_base() { alloc.deallocate(elem, space); }

    vector_base(const vector_base &) = delete; // no copy operations
    vector_base &operator=(const vector_base &) = delete;
    vector_base(vector_base &&); // move operations
    vector_base &operator=(vector_base &&);
};

template <class T, class A>
vector_base<T, A>::vector_base(vector_base &&a)              // move constructor
    : alloc{a.alloc}, elem{a.elem}, sz{a.sz}, space{a.space} // no free space if space{a.sz}
{
    a.elem = nullptr;
    a.sz = a.space = 0;
}
template <class T, class A>
vector_base<T, A> &vector_base<T, A>::operator=(vector_base &&a) // move assignment
{
    elem = a.elem;
    sz = a.sz;
    space = a.space; // also move space
    a.elem = nullptr;
    a.sz = a.space = 0;
    return *this;
    // swap(*this, a); // also works
    // return *this;
}

template <typename T, typename A = allocator<T>>
class vector : private vector_base<T, A>
{
public:
    vector()
        : vector_base<T, A>() {}

    explicit vector(int s);
    vector(std::initializer_list<T> lst); // 18.2 initializer-list constructor
    vector(const vector &);               // copy constructor
    vector &operator=(const vector &);    // copy assignment

    vector(vector &&);            // move constructor
    vector &operator=(vector &&); // move assignment

    ~vector(); // destructor

    T &operator[](int n) { return this->elem[n]; }
    const T &operator[](int n) const { return this->elem[n]; }
    int size() const { return this->sz; }
    int capacity() const { return this->space; }
    T *get_elem() const { return this->elem; }  
    A get_alloc() const { return this->alloc; } 
    void reserve(int newalloc);
    void push_back(const T &d);
    void resize(int newsize, T val = T());
};

template <typename T, typename A>
vector<T, A>::vector(int s)
    : vector_base<T, A>(this->alloc, s)
{
    for (int i = 0; i < s; ++i)                     
        this->alloc.construct(&this->elem[i], T()); 
}
template <typename T, typename A>
vector<T, A>::vector(std::initializer_list<T> lst) 
    : vector_base<T, A>(this->alloc, lst.size())   
{
    std::copy(lst.begin(), lst.end(), this->elem); 
    this->sz = lst.size();                         
}
template <typename T, typename A>
vector<T, A>::vector(const vector &a) // copy constructor
    : vector_base<T, A>(a.alloc, a.size())
{
    uninitialized_copy(a.elem, a.elem + a.sz, this->elem); 
    this->sz = a.sz;
}
template <typename T, typename A>
vector<T, A> &vector<T, A>::operator=(const vector &a) // copy assignment
{
    if (this == &a) // self-assignment, no work needed
        return *this;
    if (a.sz <= this->space) // enough space, no need for new allocation 
    {
        uninitialized_copy(a.get_elem(), &a.get_elem()[a.size()], this->elem);
        this->sz = a.sz;
    }
    // took it from C++ Programming Language
    vector temp{a}; // copy allocator
    std::swap(*this, temp);
    return *this;
}

template <typename T, typename A> // move constructor
vector<T, A>::vector(vector &&a)
    : vector_base<T, A>(a.alloc, a.size()) // get a's data members (alloc, elem, space)
{
    this->sz = a.sz; // also get a's size
    a.elem = nullptr;
    a.sz = a.space = 0;
}
template <typename T, typename A>
vector<T, A> &vector<T, A>::operator=(vector<T, A> &&a) // move assignment
//: vector_base<T, A>(a.alloc, a.size())
{
    for (int i = 0; i < this->sz; ++i)
        this->alloc.destroy(&this->elem[i]);
    this->alloc.deallocate(this->elem, this->space);

    this->elem = a.elem;
    this->sz = a.sz;
    this->space = a.space;
    a.elem = nullptr;
    a.sz = a.space = 0;
    return *this;
}

template <typename T, typename A>
vector<T, A>::~vector() // destructor
{
    for (int i = 0; i < this->sz; ++i)
        this->alloc.destroy(&this->elem[i]); 
}

template <typename T, typename A>
void print(const vector<T, A> &v, const std::string &s)
{
    std::cout << '\n';
    std::cout << s << ' ' << "size: " << v.size() << ' ' << "capacity: " << v.capacity() << ' ';
    std::cout << "elements: ";
    for (int i = 0; i < v.size(); ++i)
        std::cout << v[i] << ' ';
    std::cout << '\n';
}

template <typename T, typename A>
void vector<T, A>::reserve(int newalloc)
{
    if (newalloc <= this->space)
        return;                                 // never decrease allocation
    vector_base<T, A> b(this->alloc, newalloc); // allocate new space
    uninitialized_copy(this->elem, &this->elem[size()], b.elem); // copy from caller of reserve() to b
    b.sz = this->sz;                                             // vector_base b has size = 0 because of the constructor so you have to change it
    for (int i = 0; i < this->sz; ++i)
        this->alloc.destroy(&this->elem[i]); // destroy old
    std::swap<vector_base<T, A>>(*this, b); // swap representations
} // at exit ~vector_base will deallocate b

template <typename T, typename A> // 19.3.7
void vector<T, A>::push_back(const T &val)
{
    if (this->space == 0)
        reserve(8);
    else if (this->sz == this->space)
        reserve(2 * this->space);
    this->alloc.construct(&this->elem[this->sz], val); // add val to position sz
    ++this->sz;
}
template <typename T, typename A>
void vector<T, A>::resize(int newsize, T val) //  deleted =T() from here
{
    reserve(newsize);
    for (int i = this->sz; i < newsize; ++i)  // if newsize is bigger than currect size construct default values
        this->alloc.construct(&this->elem[i], val); // construct
    for (int i = newsize; i < this->sz; ++i)   // if newsize is smaller than currenct size destroy the elements
        this->alloc.destroy(&this->elem[i]); // destroy
    this->sz = this->space= newsize; // reset space - like in C++PL

}

int main()
try
{
   vector<std::string> vs;
    vs.push_back("test");
    print(vs, "vs.push_back():");
    vs.reserve(10);
    print(vs, "vs.reserve(10):");
}
catch (std::exception &e)
{
    std::cerr << "exception: " << e.what() << '\n';
    return 1;
}
catch (...)
{
    std::cerr << "exception\n";
    return 2;
}

【问题讨论】:

  • 在操作std::vector 实例时,您永远不应该依赖指向std::vectors 内部data 部分的指针。
  • 本书的哪个版本?你检查勘误表了吗?
  • 第 2 版和勘误表在第 19 章没有说什么:stroustrup.com/PPPslides/PPP2errata.html
  • std::swap 在这种情况下使用默认的移动构造函数和/或移动赋值运算符(我不确定现在涉及哪些,但至少其中一个)。因此T *elem 的值从b 复制到*this,因此两者具有相同的地址-> 双重释放。你确定书中某处没有介绍移动构造函数和/或赋值运算符吗?

标签: c++ raii


【解决方案1】:

问题是示例中的vector_base 未能遵循 5 规则(之前为 3 规则)。它实现了一个析构函数来释放资源,但没有实现复制/移动构造函数或赋值运算符来处理它。当reserve 使用vector_base 参数调用swap 时,它将移动(先前复制)实例,然后执行两次移动(先前复制)分配,之后销毁中间对象......并且该析构函数释放相同的指针当指针在该对象的析构函数中再次被释放时,它仍然由其中一个参数拥有,这会导致未定义的行为。

一个正确的 C++03 实现是删除 ~vector_base() 并在 vector::reservevector::~vector 的末尾取消分配(需要添加)。或者你可以在vector_base 中使用std::auto_ptr。还需要定义向量的拷贝构造函数和赋值运算符。

在 C++11 及更高版本中,您将为 vector_base 定义移动构造函数和赋值运算符。

【讨论】:

  • 这本书有矢量移动构造函数和其他必需的东西。上面的例子不是书上的,而是OP根据书中的部分组装而成的。
  • 感谢 erorika!我会实现它们,看看它是如何工作的
  • @Theodore 这本书的源代码根本没有vector_basegithub.com/Chrinkus/stroustrup-ppp/blob/master/chapter19/…
  • 这是第 19.5.6 小节之前的版本,其中引入了 vector_base。它确实运行良好。
猜你喜欢
  • 2021-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-22
  • 1970-01-01
  • 2021-12-19
  • 2022-01-08
  • 1970-01-01
相关资源
最近更新 更多