Theodore
Theodore

Reputation: 309

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

The code given in Bjarne Stroustrup - Programming Principles and Practice - Chapter 19.5.6 "RAII for vector" doesn't work when calling push_back() then reserve(). Separately they work fine.

I get this error "free(): double free detected in tcache 2"
With debugging I see that after calling swap() inside reserve() the variable b.elem[0] has <error: Cannot access memory at address 0x55555556d>

The uninitialized_copy is written wrong in the book so maybe there's something else missing? Or am I doing something wrong?

Update 17.08: I added constructors, destructors, copy and move operations for vector_base and vector (inspired from C++ Programming Language) and now it works. Thanks for the replies!

I have a question: "Programing Principles and Practice" makes vector class a derivate of vector_base class while "C++ Programming Language" makes a vector_base object a data member of class vector. I wonder which option is better? They seem equivalent though.

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

Upvotes: 0

Views: 763

Answers (1)

eerorika
eerorika

Reputation: 238351

The problem is that vector_base in the example fails to follow the rule of 5 (previously, of 3). It implements a destructor where it releases a resource, but doesn't implement copy / move constructors nor assignment operators to handle that. When reserve calls swap with vector_base arguments, it will move (previously copy) the instance, then do two move (previously copy) assignments after which the intermediate object is destroyed... and that destructor frees the same pointer that is still owned by one of the arguments which results in undefined behaviour when the pointer is freed again in the destructor of that object.

One correct C++03 implementation would be to remove ~vector_base() and instead deallocate at the end of vector::reserve and vector::~vector (which needs to be added). Or maybe you could use std::auto_ptr in vector_base. You also need to define copy constructor and assignment operator of the vector.

In C++11 and later, you would define the move constructor and assignment operator for vector_base.

Upvotes: 3

Related Questions