Proper way of deallocating memory in SLL? So I'm making my own singly linked list class, with methods that you'd expect such as remove() and insert(). I don't use pointers that much (almost never), so I thought by making a SLL class it would be good practice for me. But I'm not sure if I'm deallocating my memory correctly.
Here's the structure of my SLL:
class singly_linked_list
{
private:
    struct Node
    {
        Node* next;
        int value;
    };

private:
    int _length;
    Node* head;

public:
    ~singly_linked_list();

public:
    void remove(int index);
    void remove_front();
    void remove_back();
};

I'm most concerned about the destructor, and the three remove methods, and those are the ones deallocating memory, and I'm not sure if I'm doing it correctly.
Here's their definitions:
class singly_linked_list
{
private:
    struct Node
    {
        Node* next;
        int value;
    };

private:
    int _length;
    Node* head;

public:
    ~singly_linked_list()
    {
        Node* node_ptr = head;
        for (int a = 0; a &lt; _length; ++a)
        {
            node_ptr = node_ptr-&gt;next;

            delete head;
            head = node_ptr;
        }
    }

public:
    void remove(int index)
    {
        if (index &lt; 0 || index &gt;= _length)
            throw &quot;index is out of range&quot;;

        if (index == 0)
        {
            this-&gt;remove_front();
            return;
        }

        Node* node_ptr = head;
        for (int a = 0; a &lt; index - 1; ++a)
            head = head-&gt;next;

        Node* temp = head-&gt;next;
        head-&gt;next = temp-&gt;next;
        delete temp;

        _length -= 1;
    }

    void remove_front()
    {
        if (_length == 1)
        {
            // should I delete head instead?
            head = nullptr; // delete head;
            _length = 0;

            return;
        }

        Node* temp = head;
        head = head-&gt;next;
        delete temp;

        _length -= 1;
    }

    void remove_back()
    {
        if (_length == 1)
        {
            head = nullptr;
            _length = 0;

            return;
        }

        Node* node_ptr = head;
        for (int a = 0; a &lt; _length - 2; ++a)
            node_ptr = node_ptr-&gt;next;

        Node* temp = node_ptr-&gt;next;
        node_ptr-&gt;next = nullptr;
        delete temp;

        _length -= 1;
    }
};

I checked a few other questions including this one, but it didn't go too much in depth on memory management.
So a couple questions:

Am I causing any memory leaks, and is there a better, safer way to do things?
In remove_front(), should I set head to nullptr, or should I delete it instead?

Thanks
Answers to your questions

Am I causing any memory leaks, and is there a better, safer way to do things?

Yes, see for example your second question. A safer way is to use something like std::unique_ptr to manage pointers for you.

In remove_front(), should I set head to nullptr, or should I delete it instead?

You should delete it of course. If you just set head to nullptr, the memory for the head node is still allocated.
Avoid using names that start with an underscore
Names that start with underscores are reserved for the standard library. There are some more specific rules, but in general it is best to avoid them completely. You can safely append an underscore to the end of a name though.
On the other hand, while you have _length, head is written without an underscore. If you are not consistent in marking private variables with an underscore, there is not much point to it at all.
Flawed logic in remove(int index)
There is an issue with the for-loop in remove(int index): you are moving the head pointer instead of node_ptr. In fact you are not using node_ptr at all after initializing it. The compiler should have given you a warning about this (be sure to enable compiler warnings and fix them).
To make this work correctly, even when removing the head node, you should use an extra layer of redirection: you want a pointer to the pointer you need to update, like so:
Node **node_ptr = &amp;head;           // node_ptr is pointing to the variable `head`
for (int a = 0; a &lt; index - 1; ++a)
    node_ptr = &amp;(*node_ptr)-&gt;next; // node_ptr is updated to point to some node's `next` variable

Node* temp = *node_ptr;            // get the actual Node pointer to
*node_ptr = (*node_ptr)-&gt;next;     // update `head` or a `next` to skip one Node
delete temp;                       // delete the target Node

Once you grasp this you've become a two-star programmer!
Unnecessary code duplication
You have three different functions for removing an element, but they all do mostly the same. In fact, the first one, remove(int index), does everything you need. You don't need to treat index == 0 as a special case, the code will work correctly anyway. You can make remove_front() and remove_back() call the generic remove() function:
void remove(int index)
{
    if (index &lt; 0 || index &gt;= _length)
        throw &quot;index is out of range&quot;;

    Node **node_ptr = &amp;head;
    for (int a = 0; a &lt; index - 1; ++a)
        node_ptr = &amp;(*node_ptr)-&gt;next;

    Node* temp = *node_ptr;
    *node_ptr = (*node_ptr)-&gt;next;
    delete temp;

    _length -= 1;
}

void remove_front()
{
    remove(0);
}

void remove_back()
{
    remove(_length - 1);
}