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