Stroustroup:free():实现 vector::reserve() 时在 tcache 2 中检测到双重释放

Stroustroup: free(): double free detected in tcache 2 when implementing vector::reserve()

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

我收到此错误“free():在 tcache 2 中检测到双重释放”
通过调试,我看到在调用 swap() inside reserve() 之后,变量 b.elem[0] 有 <错误:无法访问地址为 0x55555556d 的内存>

书上的uninitialized_copy写错了,是不是还漏了什么?还是我做错了什么?

更新 17.08: 我为 vector_base 和向量(受 C++ 编程语言启发)添加了构造函数、析构函数、复制和移动操作,现在可以使用了。谢谢回复!

我有一个问题:《编程原理与实践》使向量class成为vector_baseclass的导数,而《C++程序设计》 Language”使 vector_base 对象成为 class 向量的数据成员。我想知道哪个选项更好?不过它们看起来是一样的。

#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;
}

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

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

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