Array and Vector Slides from Hung Ngo Designing a Vector Class Amortized Analysis 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 3 In header file Decide the operations “users” can do on a UBVector object THE INTERFACE 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 4 UBVector’s Interface // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: … // accessors std::string& operator[](size_t index); const std::string& operator[](size_t index) const; std::string& front(); const std::string& front() const; std::string& back(); const std::string& back() const; … } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 5 UBVector’s Interface // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: … // capacity size_t size() const; size_t capacity() const; bool empty(); void reserve(size_t n); … } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 6 UBVector’s Interface // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: … // modifiers void push_back(const std::string& value); void pop_back(); void insert(size_t position, const std::string& value); void erase(size_t position); void swap(UBVector& another_ubvec); // swap content … } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 7 UBVector’s Interface // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: // constructors and assignment operator UBVector(); // default constructor UBVector(size_t n); // UBVector with n strings UBVector(const UBVector&); // copy constructor UBVector& operator=(const UBVector& another_ubvec); // destructor ~UBVector(); … private: // ... more to come } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 8 Data structure used inside the class to implement the interface THE INNER VIEW 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 9 Array item_ptr[0] “this” item_ptr[2] “is” “a” *(item_ptr+4) “good” “example” item_ptr[1] item_ptr item_ptr + 3 string* item_ptr The storage will have to be dynamically allocated 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 10 UBVector’s Internal class UBVector { public: // ... as above size_t size() const { return num_items; } size_t capacity() const { return current_capacity; } bool empty() { return num_items == 0; } // ... as above private: size_t size_t static string }; 5/28/2016 num_items; current_capacity; const size_t INITIAL_CAPACITY; *item_ptr; // points to the start of the array CSE 250, SUNY Buffalo, © Hung Q. Ngo 11 Pictorially UBVector Object size_t num_items; size_t current_capacity; string *item_ptr; “this” 5/28/2016 “is” “a” “good” CSE 250, SUNY Buffalo, © Hung Q. Ngo “example” 12 Default constructor Initialization list Explicit constructor CONSTRUCTORS & DESTRUCTOR 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 13 UBVector’s Constructors // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: // constructors and assignment operator UBVector(); // default constructor UBVector(size_t n); // UBVector with n strings UBVector(const UBVector&); // copy constructor UBVector& operator=(const UBVector& another_ubvec); // destructor ~UBVector(); … private: // ... more to come } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 14 Default Constructor // UBVector.cpp #include <string> #include "UBVector.h" using namespace std; // actually allocate the const member const size_t UBVector::INITIAL_CAPACITY = 5; UBVector::UBVector() num_items current_capacity item_ptr } “” 5/28/2016 “” { = 0; = INITIAL_CAPACITY; = new string[current_capacity]; “” “” “” CSE 250, SUNY Buffalo, © Hung Q. Ngo 15 UBVector’s Constructors // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: // constructors and assignment operator UBVector(); // default constructor UBVector(size_t n = 0); // UBVector with n strings UBVector(const UBVector&); // copy constructor UBVector& operator=(const UBVector& another_ubvec); // destructor ~UBVector(); … private: // ... more to come } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 16 Default Constructor & Size Constructor // UBVector.cpp #include <string> #include "UBVector.h" using namespace std; // actually allocate the const member const size_t UBVector::INITIAL_CAPACITY = 5; UBVector::UBVector(size_t n) { num_items = n; current_capacity = max(n, INITIAL_CAPACITY); item_ptr = new string[current_capacity]; } “” 5/28/2016 “” “” “” “” CSE 250, SUNY Buffalo, © Hung Q. Ngo 17 Initialization List // UBVector.cpp #include <string> #include "UBVector.h" using namespace std; // actually allocate the const member const size_t UBVector::INITIAL_CAPACITY = 5; UBVector::UBVector(size_t n) : num_items(n), current_capacity(max(n, INITIAL_CAPACITY)), item_ptr(new string[max(n, INITIAL_CAPACITY)]) { /* constructor has empty body */ } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 18 Initialization List Can Be More Efficient T x; x = expression; • • • • Expression is evaluated, object type T created Assignment operator for T is called Temporary object is destroyed If x = expression is in constructor, a copy of x is created before doing assignment 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 19 Destructor UBVector::~UBVector() { delete [] item_ptr; } • I’m using raw pointer here • In C++11, you can use unique_ptr template class, then no need to delete – delete [] automatically called when it goes out of scope 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 20 Subscripting operator Front Back Push_back Reserve IMPLEMENTATION OF SOME SIMPLE METHODS 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 21 Subscripting operator – const string& UBVector::operator[](size_t index) { if (index < num_items) return item_ptr[index]; else throw out_of_range("index is out of range"); } Return a reference so we can do ubvec[12] = “abc”; later const string& UBVector::operator[](size_t index) const { if (index < num_items) return item_ptr[index]; else throw out_of_range("index is out of range"); } This const means you can’t modify the returned cell This const says this function doesn’t modify data members const version used when the object itself is const 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 22 front() and back() string& UBVector::front() { return (*this)[0]; } const string& UBVector::front() const { return (*this)[0]; } string& UBVector::back() { return (*this)[num_items-1]; } const string& UBVector::back() const { return (*this)[num_items-1]; } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 23 Unit Testing // driver.cpp: test the UBVector class #include <iostream> #include <stdexcept> #include "UBVector.h" using namespace std; int main() { UBVector ubv(3); ubv[0] = "this"; ubv[1] = "is"; ubv[2] = "good!"; cout << ubv[0] << " " << ubv[1] << " " << ubv[2] << endl; ubv.front() = "THIS"; ubv.back() = "GOOD!"; cout << ubv.front() << " " << ubv[1] << " " << ubv[2] << endl; try { cout << ubv[4] << endl; } catch (exception &e) { cout << e.what() << endl; } } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 24 push_back() void UBVector::push_back(const string& item) { if (num_items == current_capacity) reserve(2*current_capacity); item_ptr[num_items++] = item; } “ab” “cd” “ef” “gh” “ijk” Memory Is Fragmented item_ptr “ab” “cd” occupied “ef” “gh” new_item_ptr = new string[2*current_capacity] reserve() void UBVector::reserve(size_t n) { if (n > current_capacity) { current_capacity = max(n, 2*current_capacity); string *new_data_ptr = new string[current_capacity]; for (size_t i=0; i<num_items; i++) { new_data_ptr[i] = item_ptr[i]; } delete [] item_ptr; item_ptr = new_data_ptr; } } Amortized analysis: The simple case • Each push_back takes O(1)-time on average • Capacities vs cost vs credits 1 2 3 4 5 6 7 8 9 10 11 12 13 14 … 1 1 1 1 1 6 1 1 1 1 11 1 1 1 2 2 2 2 2 2 2 2 2 2 2 When to shrink capacity Implementations of erase and pop_back STL behaviors on capacity Amortized analysis ERASE, POP_BACK & AMORTIZED ANALYSIS When Do We Shrink UBVector’s Capacity? • Option 1: – Cut the capacity in half if the number of items stored is < half the current capacity • Option 2: – Cut the capacity in half if the number of items stored is < ¼ of the current capacity Let’s See How C++’s STL Does It #include <iostream> #include <vector> using namespace std; int main() { static const size_t C = 20; vector<int> vec; cout << "Initial Capacity = " << vec.capacity() << endl; for (size_t i=0; i<C; i++) { vec.push_back(i); cout << ”C[" << i << ”]=" << vec.capacity() << " "; } for (size_t i=C; i>0; i--) { vec.pop_back(); cout << ”C[" << i-1 << ”]=" << vec.capacity() << " "; } cout << endl; } Result Initial capacity = 0 C[0] = 1 C[4] = 8 C[8] = 16 C[12] = 16 C[16] = 32 C[1] = 2 C[5] = 8 C[9] = 16 C[13] = 16 C[17] = 32 C[2] = 4 C[6] = 8 C[10] = 16 C[14] = 16 C[18] = 32 C[3] = 4 C[7] = 8 C[11] = 16 C[15] = 16 C[19] = 32 C[19] = 32 C[15] = 32 C[11] = 32 C[7] = 32 C[3] = 32 C[18] = 32 C[14] = 32 C[10] = 32 C[6] = 32 C[2] = 32 C[17] = 32 C[13] = 32 C[9] = 32 C[5] = 32 C[1] = 32 C[16] = 32 C[12] = 32 C[8] = 32 C[4] = 32 C[0] = 32 So How To Shrink the Capacity of an STL Vector? int main() { static const size_t C vector<int> vec; cout << "Initial capa for (size_t i=0; i<C; vec.push_back(i); cout << ”C[" << i } = 20; = " << vec.capacity() << endl; i++) { << ”]=" << vec.capacity() << " "; cout << endl; for (size_t i=C; i>0; i--) { vec.pop_back(); vector<int>(vec).swap(vec); // the swap trick! cout << ”C[" << i-1 << ”]=" << vec.capacity() << “ "; } cout << endl; } C++11’s vector has a shrink_to_fit() member function Result Initial capacity = 0 C[0] = 1 C[1] = 2 C[2] = 4 C[3] = 4 C[4] = 8 C[5] = 8 C[6] = 8 C[7] = 8 C[8] = 16 C[9] = 16 C[10] = 16 C[11] = 16 C[12] = 16 C[13] = 16 C[14] = 16 C[15] = 16 C[16] = 32 C[17] = 32 C[18] = 32 C[19] = 32 C[19] = 19 C[18] = 18 C[17] = 17 C[16] = 16 C[15] = 15 C[14] = 14 C[13] = 13 C[12] = 12 C[11] = 11 C[10] = 10 C[9] = 9 C[8] = 8 C[7] = 7 C[6] = 6 C[5] = 5 C[4] = 4 C[3] = 3 C[2] = 2 C[1] = 1 C[0] = 0 erase() and pop_back() void UBVector::erase(size_t position) { if (position < num_items) { --num_items; for (size_t i=position; i<num_items; ++i) item_ptr[i] = item_ptr[i+1]; item_ptr[num_items].~string(); // explicit destructor call new (item_ptr + num_items) string(); // placement new } } void UBVector::pop_back() { erase(num_items-1); } Remember to swap pointers instead of contents INSERT AND SWAP Insert() void UBVector::insert( size_t position, const string& new_item) { if (num_items == current_capacity) reserve(2*current_capacity); for (size_t i=num_items; i>position; --i) item_ptr[i] = item_ptr[i-1]; item_ptr[position] = new_item; ++num_items; } Swap() void UBVector::swap(UBVector& the_other) { std::swap(num_items, the_other.num_items); std::swap(current_capacity, the_other.current_capacity); std::swap(item_ptr, the_other.item_ptr); } Copy constructor Assignment operator Destructor THE RULE OF THREE The Copy Constructor // UBVector.h #ifndef _UBVECTOR_H #define _UBVECTOR_H class UBVector { public: // constructors and assignment operator explicit UBVector(size_t n = 0); // UBVector with n strings UBVector(const UBVector&); // copy constructor UBVector& operator=(const UBVector& another_ubvec); // destructor ~UBVector(); … private: // ... more to come } 5/28/2016 CSE 250, SUNY Buffalo, © Hung Q. Ngo 40 When is a Copy Constructor Called? 1. Declare and initialize a variable – UBVector vec1(vec2); 2. Pass an object by value – void Foo(UBVector ubv); – UBVector a(400); – foo(a); 3. Return an object by value – Watch out for Return Value Optimization The Rule of Three • If a class defines one of the following, it should define all three: – Copy constructor – (Copy) Assignment operator – Destructor • No need if there’s no dynamic memory • C++11: rule of five. Let’s not go there. What Happens if We Don’t Define Them int main() { UBVector a(4); a[0] = "this"; a[1] = "is"; a[2] = "very"; a[3] = "interesting"; print_vec(a); UBVector b(a); // equivalent to UBVector b = a; a[2] = "NOT"; print_vec(b); } this is very interesting this is NOT interesting a.out(3229) malloc: *** error for object 0x1067009f0: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug Abort trap: 6 Default Behavior Compiler Gives Us • If we don’t define them, the compiler support default versions • Copy Constructor & Assignment Operator – Member-wise copy • Destructor – Destruct all data members Assignment Operator – Code Reuse!!! UBVector& UBVector::operator=(const UBVector& the_other) { // uses (deep) copy constructor here UBVector temp(the_other); swap(temp); return *this; } Copy Constructor UBVector::UBVector(const UBVector& other) : num_items(other.num_items), current_capacity(other.num_items), item_ptr(new string[other.num_items]) { for (size_t i=0; i<num_items; ++i) item_ptr[i] = other.item_ptr[i]; }