I am doing some homework in C++ and I just encountered an error I couldn't fix. I defined a RingBuffer class that stores values, the logic works but I have a problem with the includes. If I don't include the .cpp file with the implementation of the methods I get an "undefined reference to function" error for every single function I defined.
rbtest.cpp (This class was given and is used to test the implementation)
#include <iostream>
#include <stdexcept>
#include "ringbuffer.hpp"
int main(void) {
    RingBuffer<int, 3> rb;
    rb.print();
    std::cout << std::boolalpha;
    std::cout << "Empty: " << rb.empty() << " Full: " << rb.full() << std::endl;
    rb.push_back(2);
    std::cout << "Empty: " << rb.empty() << " Full: " << rb.full() << std::endl;
    rb.push_front(1);
    rb.push_back(3);
    std::cout << "Empty: " << rb.empty() << " Full: " << rb.full() << std::endl;
    rb.push_back(4);
    std::cout << "Empty: " << rb.empty() << " Full: " << rb.full() << std::endl;
    rb.print();
    std::cout << "pop_front: " << rb.pop_front() << std::endl;
    std::cout << "Empty: " << rb.empty() << " Full: " << rb.full() << std::endl;
    std::cout << "pop_front: " << rb.pop_front() << std::endl;
    rb.push_back(5);
    rb.print();
    std::cout << "pop_back: " << rb.pop_back() << std::endl;
    std::cout << "pop_back: " << rb.pop_back() << std::endl;
    rb.print();
    try {
        rb.pop_back();
        std::cout << "This should not be printed!" << std::endl;
    }
    catch (std::exception& rangeError) {
        std::cout << "Error: " << rangeError.what() << std::endl;
    }
}
ringbuffer.hpp
#ifndef X
#define X
#include <iostream>
#include <stdexcept>
/**
 * @brief Ring Buffer class
 *
 * @tparam T typename
 * @tparam size int
 */
template <typename T, int size>
class RingBuffer {
private:
    int back = 0, front = 0;
    T content[size];
    int counter = 0;
public:
    void push_back(T item);
    void push_front(T item);
    T pop_back();
    T pop_front();
    bool full();
    bool empty();
    void print();
};
#endif
ringbuffer.cpp
#include "ringbuffer.hpp"
/**
 * @brief Pushes an Item into the ring buffer from the back
 *
 * @tparam T
 * @tparam size
 * @param item
 */
template <typename T, int size>
void RingBuffer<T, size>::push_back(T item) {
    if (full()) return;
    content[back] = item;
    back++;
    if (back == size) back = 0;
    counter++;
}
/**
 * @brief Pushes an item into the ring buffer from the front
 *
 * @tparam T
 * @tparam size
 * @param item
 */
template <typename T, int size>
void RingBuffer<T, size>::push_front(T item) {
    if (full()) return;
    front--;
    if (front < 0) front = size - 1;
    content[front] = item;
    counter++;
}
/**
 * @brief Pops an Item from the back and returns it
 *
 * @tparam T
 * @tparam size
 * @return T
 */
template <typename T, int size>
T RingBuffer<T, size>::pop_back() {
    if (empty()) {
        throw std::out_of_range("pop_back on empty buffer");
    }
    back--;
    if (back < 0) back = size - 1;
    T item = content[back];
    counter--;
    return item;
}
/**
 * @brief Pops an Item from the front and returns it
 *
 * @tparam T
 * @tparam size
 * @return T
 */
template <typename T, int size>
T RingBuffer<T, size>::pop_front() {
    if (empty()) {
        throw std::out_of_range("pop_front on empty buffer");
    }
    T item = content[front++];
    if (front == size) front = 0;
    counter--;
    return item;
}
/**
 * @brief Checks if ring buffer is full
 *
 * @tparam T
 * @tparam size
 * @return true if full
 * @return false if not full
 */
template <typename T, int size>
bool RingBuffer<T, size>::full() {
    return counter == size;
}
/**
 * @brief Checks if ring buffer is empty
 *
 * @tparam T
 * @tparam size
 * @return true if empty
 * @return false if not empty
 */
template <typename T, int size>
bool RingBuffer<T, size>::empty() {
    return counter == 0;
}
/**
 * @brief Prints the ring buffer with all its elements front to back, prints "Buffer is empty" if empty
 *
 * @tparam T
 * @tparam size
 */
template <typename T, int size>
void RingBuffer<T, size>::print() {
    if (empty()) std::cout << "Buffer is empty" << std::endl;
    else {
        int i = front;
        do {
            std::cout << content[i++] << "\t";
            if (i >= size) i = 0;
        } while (i != back);
        std::cout << std::endl;
    }
}
Makefile
all: rbtest.cpp ringbuffer.hpp ringbuffer.cpp
    g++ -o ringbuffer rbtest.cpp ringbuffer.hpp ringbuffer.cpp
clean:
    rm ringbuffer
