For templates to work the compiler needs access to the complete definition/implementation of the function(s) in order to instantiate/create them. With your current implementation you haven't provided the actual implementation of the functions, the compiler will then simply assume that they are provided elsewhere in your source. Since you don't have any template specialization for the type Event
anywhere in your source, you will get this undefined reference error.
A code example will make it more clear. Consider the following (alert: this is bad code, its just for the sake of describing the problem);
#include <iostream>
// In your header file
template<typename T>
T sum(T a, T b);
int main()
{
std::cout << sum(int(10), int(10));
}
This will give you the following error:
/tmp/ccppEJmY.o: In function `main':
main.cpp:(.text.startup+0xf): undefined reference to `int sum<int>(int, int)'
collect2: error: ld returned 1 exit status
Now you could fix it by explicitly defining (read: specializing) your template for the type int
since that is what your pass to the function when called in main.
#include <iostream>
// In your header file
template<typename T>
T sum(T a, T b);
// In your source file, a specialization of function sum for type `int`
template<>
int sum<int>(int a, int b) { return a + b; }
int main()
{
std::cout << sum(int(10), int(10));
}
With this, everything compiles just fine, since there exists a specialized version with the actual implementation of the sum function where type T
is int
. However, if we would now change the call in main
to use a double
type, e.g.:
// ...
std::cout << sum(double(3.14), double(2.0));
We have the same undefined reference error again, there is no specialization available of the sum function where type T
is double
. Now you could provide another specialization for a double in your source file, but this completely defeats the purpose of using templates. That is, making generic code, implement once, and use it for many different types. If you are going to do it like I showed above, you could've just removed the whole template garbage and just provide two function overloads of sum
where one accepts int
and one accepts double
, e.g.:
int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }
Then how to implement this properly? Simply provide the actual implementation of the sum function in the template definition:
#include <iostream>
// In your header file
template<typename T>
T sum(T a, T b) { return a + b; }
int main()
{
std::cout << sum(int(10), int(10));
std::cout << sum(double(3.14), double(2.0));
}
With this implementation the compiler has access to the function implementation (e.g. the body of the function) the moment we make two calls in main
. Because of this the compiler is able to instantiate two sum
functions; one where the type is int
and one where the type is double
. So general rule of thumb, if you have a template function in your header file, you need to provide the definition of that function in the header file as well.
Back to your code, to fix it simply copy the function bodies you have in your source file for put
and get
over to your header file and it should compile. I would personally do it a bit different, because if you copy the contents of those two functions to the header file, it means you will also have to include Wire.h
in your own I2cEeprom
header. This is just a bit ugly since everyone who will include your I2cEeprom
header file will get a free and unwanted include of Wire.h
, not so nice IMHO.
My suggestion would be something as follows (it compiles, but is not tested):
#include <cstdint>
// In your header file
class I2cEeprom
{
public:
template<typename T>
uint16_t put(uint16_t addr, T const & data)
{
put_n(addr, &data, sizeof(data));
return sizeof(data);
}
template<typename T>
uint16_t get(uint16_t addr, T & data) // Note that I removed `const` here, const is immutable, since we need to write bytes to it, don't make it `const`
{
get_n(addr, &data, sizeof(data));
return sizeof(data);
}
private:
void put_n(uint16_t addr, void const * src, int size);
void get_n(uint16_t addr, void * src, int size);
};
// In your source file
// #include <Wire.h>
void I2cEeprom::put_n(uint16_t addr, void const * src, int size)
{
uint8_t const * data = reinterpret_cast<uint8_t const *>(src);
// Copy `size` bytes from `data` into eeprom
}
void I2cEeprom::get_n(uint16_t addr, void * src, int size)
{
uint8_t * data = reinterpret_cast<uint8_t *>(src);
// Copy `size` bytes from eeprom into `data`
}
struct Foo
{
int a;
int b;
};
int main()
{
I2cEeprom eeprom;
double val1 = 10.0;
int val2 = 3;
Foo val3 = {10, 20};
// Write to eeprom
{
uint16_t address = 0;
address += eeprom.put(address, val1);
address += eeprom.put(address, val2);
address += eeprom.put(address, val3);
}
// Read from eeprom
{
uint16_t address = 0;
address += eeprom.get(address, val1);
address += eeprom.get(address, val2);
address += eeprom.get(address, val3);
}
}
This would still give you the benefit of passing in different types, e.g. an int
, a double
or a struct Foo
, but you won't pollute your header file with the implementation of writing things to eeprom which requires the inclusion of Wire.h
. Why is this better than not just publicly exposing put_n
and get_n
? Well, then you burden the user with providing the correct size for the type, he/she can make mistakes doing that. With these helper template functions you don't have to worry about that anymore.
Obviously users can still pass in types that are not trivially copyable (e.g. types that can't just be copied with memcpy or alike). I'm not entirely sure if the compiler that comes with Arduino support it, but something like these static asserts can work. If you pass in an argument with a type that is not copyable, the code won't compile.
// See: https://coliru.stacked-crooked.com/a/c4d8404b96805ed9 for a full example
// ...
#include <type_traits>
// ...
template<typename T>
uint16_t put(uint16_t addr, T const & data)
{
static_assert(std::is_trivially_copyable_v<T>);
// ...
}
template<typename T>
uint16_t get(uint16_t addr, T & data)
{
static_assert(std::is_trivially_copyable_v<T>);
// ...
}