Circular Buffer

My process for Exercism’s exercises is this: I write everything in VSCode, test and troubleshoot it, then paste into the online editor, and try to interpret the CMake errors, if any. I’m using C++17 and g++13.
I’ve tried all the tests locally in VSCode and everything works without errors (except what’s expected from the tests, e.g. std::domain_error), and all of the expected results match the tests. When I submit it, I receive a million undefined reference errors (I included only a few lines, otherwise this post exceeds the character limit):

We received the following error when we ran your code:
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: CMakeFiles/circular-buffer.dir/circular_buffer_test.cpp.o: in function `CATCH2_INTERNAL_TEST_0()':
/tmp/circular-buffer/circular_buffer_test.cpp:16: undefined reference to `circular_buffer::circular_buffer<int>::read()'
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: CMakeFiles/circular-buffer.dir/circular_buffer_test.cpp.o: in function `CATCH2_INTERNAL_TEST_2()':
/tmp/circular-buffer/circular_buffer_test.cpp:24: undefined reference to `circular_buffer::circular_buffer<int>::write(int)'
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/circular-buffer/circular_buffer_test.cpp:27: undefined reference to `circular_buffer::circular_buffer<int>::read()'
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: CMakeFiles/circular-buffer.dir/circular_buffer_test.cpp.o: in function `CATCH2_INTERNAL_TEST_4()':
/tmp/circular-buffer/circular_buffer_test.cpp:34: undefined reference to `circular_buffer::circular_buffer<int>::write(int)'
/usr/lib/gcc/x86_64-alpine-linux-musl/12.2.1/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/circular-buffer/circular_buffer_test.cpp:37: undefined reference to `circular_buffer::circular_buffer<int>::read()'
...
collect2: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/circular-buffer.dir/build.make:115: circular-buffer] Error 1
make[1]: *** [CMakeFiles/Makefile2:85: CMakeFiles/circular-buffer.dir/all] Error 2
make: *** [Makefile:91: all] Error 2

Here is my hpp [h file, not hpp, typo, see response below] file:

#pragma once

#include <stdexcept>
#include <string>
#include <vector>


namespace circular_buffer {

    template <class T>
    class circular_buffer {
        private:
            int size;
        public:
            circular_buffer(int s) {
                size = s;
            }
            int current_size{0};
            std::vector<T> buffer{};
            T read(void);
            void write(T);
            void clear(void);
            void overwrite(T);
    };

}  // namespace circular_buffer

here is the cpp file:

#include "circular_buffer.h"


namespace circular_buffer {

    template <class T>
    T circular_buffer<T>::read() {
        T output;
        if (circular_buffer::buffer.size() == 0) {
            throw std::domain_error("");
        } else {
            output = circular_buffer::buffer[0];
            circular_buffer::buffer.erase(circular_buffer::buffer.begin());
            circular_buffer::current_size -= 1;
        }
        return output;
    }

    template <class T>
    void circular_buffer<T>::write(T in_data) {
        if (circular_buffer::current_size < circular_buffer::size) {
            circular_buffer::buffer.emplace_back(in_data);
            circular_buffer::current_size += 1;
        } else {
            throw std::domain_error("");
        }
    }

    template <class T>
    void circular_buffer<T>::clear(void) {
        circular_buffer::buffer = {};
        circular_buffer::current_size = 0;
    }

    template <class T>
    void circular_buffer<T>::overwrite(T in_data) {
        if (circular_buffer::current_size == int(circular_buffer::size)) {
            circular_buffer::buffer.erase(circular_buffer::buffer.begin());
            circular_buffer::buffer.emplace_back(in_data);
        } else {
            circular_buffer::buffer.emplace_back(in_data);
            circular_buffer::current_size += 1;
        }
    }

}  // namespace circular_buffer

As you can guess, I’m stuck, that’s why I’m asking for help. So, what am I doing wrong? And how can I avoid a similar situation in the future (works on VSCode but not with CMake and the online editor)? I’m not familiar with CMake at all, in case that wasn’t obvious.
Thanks for your time!

Just double checking. Are you using a .h or .hpp file and do they line up?

The problem is that circular_buffer is a class template but its member functions are defined in the .cpp file.

The tests #include the .h file with the definition of the class and the declarations of the member functions.
When the compiler compiles the tests and sees an instantiation of a class template (such as circular_buffer::circular_buffer<int> buffer(1);), it instantiates the template with the given type. But it can only do that for the code it has seen in the current translation unit. And in this case that does not include the definitions of the member functions because they are in a separate .cpp file.

For function and class templates that are supposed to be used by other .h and .cpp files, define them in the .h file. That’s how everybody and their dog does it.
That might one day change when C++20’s modules become more wide-spread but until then: “template” → .h file.

Wow, thank you for such a clear and informative response! Cheers!

Just double checking. Are you using a .h or .hpp file and do they line up?

Yes, they lineup. Good eyes, my mistake for writing “hpp” in the post.