/* Smart Pointers in C++, aka Motivations for Rust What could go wrong with memory allocation: 1. functions allocates memory on the heap, but doesn't deallocate it before exit 2. returning pointer to local (stack-allocated) data, which then gets popped off the stack. 3. assigning a pointer to pointer somewhere else, without deallocating what it was pointing to before. 4. An object is shared (multiple pointers point to it), resulting in invalid deletion. 5. An object is shared and gets deleted twice (ouch!) Manual memory management can be tricky. There are some general approaches: 1. Always use stack allocated memory, never allocate on heap: this approach is not feasible in many situations (linked lists). 2. Centralize all memory allocation and deallocation in one location, say main. Only ever pass pointers to pre-existing structures to functions; functions never create new structures. This approach also cannot always be used because we can't always know statically how much memory will be needed. 3. Use a "smart pointer". Smart pointers in C++ go back decades. The idea is to use operator overloading to create an object with pointer like behavior, but which will managed code underneath. An object created on the stack in C++ will have its destructor automatically called when it goes out of scope. This is a memory management technique in C++. But smart pointers, as this program demonstrates, also do not form a complete solution: 1. There is nothing in C++ that says you HAVE TO use them. Of course it's possible to write good programs in any language if you're always disciplined and don't make mistakes. Always, 100%, for every programmer. Google "linked list class in C++" and see what you get. Pray that these people never write programs for you. 2. Even if you use smart pointers, the rest of the language is so unrestricted that it is very easy to break them. Furthermore, no matter how sophisticated the smart pointer implementation, the controls over memory safety can only be applied at runtime. At compile time, let's just say that C++ is rather open minded about a lot of things... A language with a garbage collector will never have any of these problems. But what if you want to WRITE a garbage collector? A garbage collector should not require a garbage collector to run! This is where you have to use a language that's more suited for systems programming. But benefiting from strong static checks also implies compromises in terms how you can write programs. */ #include using namespace std; bool trace = 0; template class Smptr // my own generic smart pointer class { protected: T *p; // encapsulated pointer bool owned; // avoids duplicate delete after data MOVED - Rust termo public: Smptr(T* p0) { p = p0; owned=1; } // constructor ~Smptr() { if (owned && p!=NULL) { /*owned=0;*/ delete(p); } } // overload operators: T& operator *() { return *p; } //most sp impls contain this, not safe T operator +() { return *p; } T& operator -() { if (p->cdr!=NULL) delete(p->cdr); return *p; }//testing T* operator &() { return p; } T* operator ->() { return p; } void operator =(Smptr &sp) { delete(p); // delete local before changing it p = sp.p; // copy over pointer ... sp.owned=0; // and TRANSFER OWNERSHIP to this Smptr - Rust termo again. owned=1; // however, this control is only implemented at runtime } }; //Smptr class cons // basic linked list class for experiment { public: int car; cons *cdr; cons(int a, cons* b) {car=a; cdr=b;} ~cons() { if (cdr) delete(cdr); // not efficient but ok for example } // destructor deletes all cells void print() { for (cons* i=this;i!=NULL;i=i->cdr) cout<< i->car << " "; cout << endl; } };//cons class for linked lists /////////// Key test function /////// void DoILeak() { Smptr list1(new cons(2,new cons(3,new cons(5,new cons(7,NULL))))); //*list1=*(new cons(10,new cons(220,NULL))); // memory leak Smptr list2(new cons(1,new cons(10,new cons(100,NULL)))); //*list1 = *list2; // double delete, sementation fault or worse list1 = list2; // no memory leak because using overloaded = } int main(int argc, char** argv) { if (argc>1) trace = 1; Smptr list(new cons(2,new cons(3,new cons(5,NULL)))); cons *b = list->cdr->cdr; *list = *(new cons(10, new cons(20,b))); // what happened to 2 and 3? b->print(); // b list was not dealloc'ed list->print(); // no problem Smptrlist2(list->cdr); list2->print(); // still ok, but list, list2 will cause double delete // when main ends (expect core dump at end). for(int i=0;i<20000000;i++) { DoILeak(); if (trace) {cout << "---endfun---\n"; cout.flush();} } cout << "Did it leak?\n"; return 0; }// main /* My conclusion from this experiment is that, while smart pointers look like a good idea, ultimately it does not provide a practical solution to memory management. It is too fragile and too difficult to use correctly, and when mixed with "normal" pointers, creates a situation that's even more difficult to manage than not using them at all. Furthermore, what protections it does offer are mostly at runtime, not compile time. All this calls for some changes to the C++ language itself, which happened in 2011... */