This is my very first implementation of a full-fledged ADT, which could potentially be use-ready. Now, I'm still learning, therefore I would like to ask you what I can do to further improve the following code.
#ifndef LIST_H
#define LIST_H
#include <utility>
#include <type_traits>
#include <stdexcept>
#include <assert.h>
template<typename T>
class List {
    class Node {
        friend List;
        Node *m_next;
        typename std::aligned_storage<sizeof(T), alignof(T)>::type m_data;
        Node() : m_next(nullptr) {}
        Node(const T &data, Node *next) : m_next(next) {
            new(&m_data) T(data);
        }
    };
    Node *m_sentinel,
         *m_head,
         *m_tail;
    size_t m_length;
    template<bool is_const>
    class Iterator_ {
        friend List;
        using NodeType           = typename std::conditional<is_const, const Node*, Node*>::type;
        using ValueReferenceType = typename std::conditional<is_const, const T&, T&>::type;
        NodeType m_node;
        Iterator_(NodeType node) : m_node(node) {}
        public:
            Iterator_(const List &list, size_t index = 0) : m_node(list.m_head) {
                if (index >= list.m_length) throw std::out_of_range("Iteration out of bounds");
                operator+(index);
            }
            Iterator_ &operator++() {
                assert(m_node != m_node->m_next);
                m_node = m_node->m_next;
                return *this;
            }
            Iterator_ operator++(int) {
                Iterator old = *this;
                operator++();
                return old;
            }
            Iterator_ &operator+(size_t index) {
                for (size_t cur_index = 0; cur_index < index; ++cur_index)
                    operator++();
                return *this;
            }
            bool operator==(const Iterator_ &cmp) const {
                return m_node == cmp.m_node;
            }
            bool operator!=(const Iterator_ &cmp) const {
                return !operator==(cmp);
            }
            ValueReferenceType &operator*() const {
                assert(m_node != m_node->m_next); // Dereferencing a sentinel (end iterator)
                return reinterpret_cast<ValueReferenceType>(m_node->m_data);
            }
    };
    using Iterator      = Iterator_<false>;
    using ConstIterator = Iterator_<true>;
    public:
        List() : m_sentinel(new Node()), m_head(m_sentinel), m_tail(m_sentinel), m_length(0) {}
        List(const List ©) : List() {
            append(copy);
        }
        List(List &&move) : List() {
            std::swap(m_sentinel, move.m_sentinel);
            std::swap(m_head,     move.m_head);
            m_tail   =            move.m_tail;
            m_length =            move.m_length;
        }
        virtual ~List() {
            clear();
            delete m_sentinel;
        }
        Iterator begin() {
            return Iterator(m_head);
        }
        Iterator end() {
            return Iterator(m_sentinel);
        }
        ConstIterator begin() const {
            return ConstIterator(m_head);
        }
        ConstIterator end() const {
            return ConstIterator(m_sentinel);
        }
        ConstIterator cbegin() const {
            return ConstIterator(m_head);
        }
        ConstIterator cend() const {
            return ConstIterator(m_sentinel);
        }
        List &clear() {
            if (m_head != m_sentinel) {
                ConstIterator iter     = cbegin(),
                              iter_end = cend();
                while (iter != iter_end) {
                    const Node *node = iter.m_node;
                    ++iter;
                    delete node;
                }
            }
            m_head = m_tail = m_sentinel;
            m_length = 0;
            return *this;
        }
        List &insert(const T &element, size_t index = 0) {
            if (index > m_length) {
                assert(0);
                index = m_length;
            }
            if (!index) {
                m_head = new Node(element, m_head);
                if (!m_length) m_tail = m_head; // Our first element, that makes the tail and head the same
            } else if (index == m_length - 1) append(element);
            else {
                Node *previous = Iterator(*this, index - 1).m_node,
                     *new_node = new Node(element, previous->m_next);
                previous->m_next = new_node;
            }
            ++m_length;
            return *this;
        }
        List &append(const T &element) {
            Node *old_tail = m_tail;
            m_tail = new Node(element, m_sentinel);
            if (!m_length) m_head = m_tail; // Our first element, that makes the tail and head the same
            else old_tail->m_next = m_tail;
            ++m_length;
            return *this;
        }
        List &append(const List ©) {
            for (const T &v : copy) append(v);
            return *this;
        }
        List &remove(size_t index) {
            if (index >= m_length) throw std::out_of_range("Removal out of bounds");
            if (!index) {
                Node *next = m_head->m_next;
                delete m_head;
                m_head = next;
            } else {
                Iterator iter(*this, index - 1);
                Node *previous = iter.m_node,
                     *rem      = previous->m_next;
                previous->m_next = rem->m_next;
                delete rem;
            }
            --m_length;
            return *this;
        }
        List &reverse() {
            if (m_length < 2) return *this;
            Node *previous = nullptr,
                 *next     = nullptr,
                 *cur      = m_head;
            while (cur != m_sentinel) {
                next = cur->m_next;
                if (cur != m_head) cur->m_next = previous;
                previous = cur;
                cur = next;
            }
            std::swap(m_head, m_tail);
            m_tail->m_next = m_sentinel;
            return *this;
        }
        bool operator==(const List &cmp) const {
            return m_head == cmp.m_head;
        }
        bool operator!=(const List &cmp) const {
            return !operator==(cmp);
        }
        T &operator[](size_t index) const {
            return *Iterator(*this, index);
        }
        List &operator=(const List ©) {
            clear();
            append(copy);
            return *this;
        }
        size_t size() const {
            return m_length;
        }
};
#endif
