My favorite container the list. Even doubly linked to make it more fun. ##Issues in your current code. You don't implement the rule of three/five. Any class that manages resources needs to correctly implement the rule of three (optionally the rule of five). Otherwise you will have issues because of the compiler generated methods. If you do not define them the compiler will generate the following methods: * Default Constructor * Copy Constructor * Copy Assignment operator Under some situations it will also generate * Move Constructor * Move Assignment operator Because of the default implementation of these methods a class that "Owns" a pointer will not work as you expect. Note: Owns means that the destructor will delete it. But the general rule is if you define the destructor or any of the above operators you must define them all. If we look at your current implementation: { List data; // List one data.push_back(5); List data2(data); // This is not a copy of data. // Internally this has the same pointers // as data. } // Here you get a double delete // Both destructors will delete the same pointer. There are simple implementations for most of these. List& operator=(List const& rhs) { List copy(rhs); // use the copy and swap idiom copy.swap(*this); return *this; } List(List&& rhs) noexcept : head(nullptr) , tail(nullptr) , size_(0) { rhs.swap(*this); // Move construct: Simply swap the data } List& operator=(List&& rhs) noexcept { rhs.swap(*this); // Move assignment: Simply swap the data return *this; // the return. } void swap(List& other) noexcept { using std::swap; swap(head, other.head); swap(tail, other.tail); swap(size_, other.size_); } // The only one that is hard is the copy constructor. List(List const& rhs) : head(nullptr) , tail(nullptr) , size_(0) { // But since you have iterators defined. // that makes it relatively easy. for(T const& value: rhs) { push_back(value); } } Your implementation is fine. But there is another technique. If you list is circular (head joined to tail) and you add a sentinel node (a marker node at the head/tail that is always there). Then the actual implementation becomes a lot easier because there is no longer any need to check for `null` (because there is always a sentinel). void push_back(const T &_data) { new Node(_data, sent, sent->prev); ++size_; } void push_front(const T &_data) { new Node(_data, sent->next, sent); ++size_; } Node::Node(T const& data, Node* next, Node* prev) : data(data) , next(next) , prev(prev) { prev->next = this; next->prev = this; } void pop_back() { if (size > 0) { delete sent->prev; --size_; } } void pop_front() { if (size > 0) { delete sent->next; --size_; } } Node::~Node() { prev->next = next; next->prev = prev; } Next you always copy the objects into the list. That's fine but you should also think about moving objects into the list and emplacing them (building them in place). void push_back(T const& copy); // You have this. void push_back(T&& move); // Move into the list template<typename... Args> void emplace_back(Args...&& args); // Build in-place You don't need two version of size. // This version is not necessary. // The const version will work in all situations. size_t size() { return size_; } size_t size() const { return size_; } Youre const methods for getting iterators does not return the correct type. Iterator begin() const { return Iterator(head, tail); } Iterator end() const { Iterator iter(nullptr, tail); return iter; } Technically you can still use the iterators returned to modify the object. So these should not really callable from a const version of the object. From the const version of `begin()` and end you should return a const version of the iterator that allows you to read the container but not write to it. While we are on iterators you should also add `cbegin()` and `cend()` that always return const iterators.