Someone at Recurse Center recommended building a key value store as a beginner C++ project. This exposes you to a lot of corners of C++ such as templates (so that you can store whatever you want in your key value store in a type safe way), file access (so that you can persist) and networking (making it available over the internet). I got started the same way I would in any language: by building an interface for the core logic that could be plugged in to a persistence layer and a networking stack. Separations of concerns is great!
Unfortunately I pretty quickly ran in to a problem. I had three files. A key value store data structure header (
#include <string>
#include <map>
template <typename T>
class KeyValueStoreInterface
{
public:
virtual T get(std::string key) = 0;
virtual void put(std::string key, T value) = 0;
virtual void remove(std::string key) = 0;
};
template <typename T>
class InMemoryKVS : KeyValueStoreInterface<T>
{
private:
std::map<std::string, T> internalKeyValue;
public:
InMemoryKVS();
virtual T get(std::string key);
virtual void put(std::string key, T value);
virtual void remove(std::string key);
};
Here I'm defining a class,
Below that is another class
#include "kvslib.h"
template <typename T>
InMemoryKVS<T>::InMemoryKVS()
{
}
template <typename T>
T InMemoryKVS<T>::get(std::string key)
{
return this->internalKeyValue[key];
}
template <typename T>
void InMemoryKVS<T>::put(std::string key, T value)
{
this->internalKeyValue.insert(std::make_pair(key, value));
}
template <typename T>
void InMemoryKVS<T>::remove(std::string key)
{
this->internalKeyValue.erase(key);
The implementation file is pretty simple. It only implements functions for
#include <iostream>
#include "kvslib.h"
int main()
{
InMemoryKVS<int> x = InMemoryKVS<int>();
x.put("hello", 5);
std::cout << x.get("hello") << std::endl;
x.remove("hello");
std::cout << x.get("hello") << std::endl;
InMemoryKVS<std::string> y = InMemoryKVS<std::string>();
std::string str("world");
y.put("hello", str);
std::cout << y.get("foo") << std::endl;
return 0;
}
Finally I use the
Unfortunately compiling this results in a pretty inscrutable error:
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<int>::InMemoryKVS()'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<int>::put(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<int>::get(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<int>::remove(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<int>::get(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::InMemoryKVS()'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::put(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:function main: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::get(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::get(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::put(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >: error: undefined reference to 'InMemoryKVS<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::remove(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<int>: error: undefined reference to 'InMemoryKVS<int>::get(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<int>: error: undefined reference to 'InMemoryKVS<int>::put(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)'
bazel-out/k8-fastbuild/bin/_objs/kvs/main.pic.o:main.cpp:vtable for InMemoryKVS<int>: error: undefined reference to 'InMemoryKVS<int>::remove(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: error: ld returned 1 exit status
What on earth?
I see a lot of
In order to explain what's happening here we need to understand what C++ generics, also known as templates, actually do.
(This won't be a super in depth explanation, since I'm just learning this myself!)
To tell the C++ that a class is generic you use the
InMemoryKVS<int> x = InMemoryKVS<int>();
When the compiler sees this it generates a whole new version of
int InMemoryKVS<T>::get(std::string key)
With this in mind there's a plausible explanation for the above error message. The
(This also won't be super in depth because C++ compilers are very complicated)
Remember back when you compiled your very first C(++) program? It probably looked something like this:
g++ main.cpp -o main
Later on you had a project that had two files some main file and some library file. You maybe compiled them like this:
g++ library.cpp -o library.o # compile the library
g++ main.cpp -o main.o # compile main
g++ main main.o library.o # Link the object code, creating an executable named main
(Even if you compiled them like
When the compiler is compiling
Now we can understand what's happening with my key value store interface.
When the compiler compiles main it looks at
However, when we reference
Separately the compiler also compiles
Then when the linker comes along to link these two translation units together it is only there that it sees there is no definition of
The solution is simple. Move the implementation of
This was, as a newcomer, super confusing and I'm now beginning to understand why people curse C++. :D
Things I'm going to look in to based off of this, that you might want to look in to as well if you find yourself here: