I have a function that should work with many numeric types, but I don't want to really support all.
For the sake of simplification, let's have the following files:
add.hpp
#pragma once
template <typename T>
T add(T, T);
add.cpp
#include "add.hpp"
template <typename T>
T add(T a, T b) {
return a + b;
}
// Only supported types
template int add(int, int);
template unsigned add(unsigned, unsigned);
template float add(float, float);
template double add(double, double);
main.cpp
#include <stdio.h>
#include "add.hpp"
int main() {
printf("Output is: %d\n", add<int>(1,1));
printf("Output is: %f\n", add<float>(1,3));
// I want it to raise a compiler error, not linker error
printf("Output is: %d\n", add<char>(0,1));
return 0;
}
Is there any way for add<char>
to raise a compilation error instead of a linker error?
If templates are not the tool for this, then what is? I don't want to do function overloading because I don't want to repeat the code in add()
.
Having a compiler error while just including add.hpp
in your main.cpp
file implies that add.hpp
holds the information that will allow/reject the usage of some template argument. In other words, you can't have this information only in add.cpp
as you want to do, otherwise main.cpp
(as a single compilation unit) would not have this information.
From that observation, setting the "allowed/rejected" types in add.hpp
can be done in different ways. For instance, you could define a type list holding the allowed types, e.g.
using allowed_types = std::tuple<int,float>;
then your add
definition becomes with the help of concepts
template <typename T>
requires (has_type<T,allowed_types>::value)
T add(T a, T b) { return a+b; }
where has_type
is a type trait telling whether a type is in a tuple (see here)
template <typename T, typename Tuple> struct has_type;
template <typename T, typename... Us> struct has_type<T, std::tuple<Us...>> : std::disjunction<std::is_same<T, Us>...> {};
So, you will get a compilation error with the following (and not a linker one)
int main()
{
printf("Output is: %d\n", add<int>(1,1));
printf("Output is: %f\n", add<float>(1,3));
// one gets a compiler error here (not a linker one)
printf("Output is: %d\n", add<char>(0,1));
return 0;
}
Note that it is still possible to keep the actual implementation in add.cpp
and call it from add.hpp
(see StoryTeller's answer for instance). One would then have
template <typename T>
requires (has_type<T,allowed_types>::value)
T add(T a, T b) { return detail::add(a,b); }
Update
If one keeps an implementation file add.cpp
, it is important to explicit all the required add
instantiations for all the required types. Instead of having one line per instantiation, it seems possible (see here) to force these instantiations by using the types list allowed_types
, e.g.
// file add.cpp
#include "add.hpp"
namespace detail {
template <typename T>
T add(T a, T b) {
return a + b;
}
}
template<typename Tup>
auto instantiate() {
static auto funcs = [] <std::size_t...Is> (std::index_sequence<Is...>) {
return std::make_tuple (add<std::tuple_element_t<Is,Tup>>...);
} (std::make_index_sequence<std::tuple_size_v<Tup>>());
return &funcs;
}
template auto instantiate<allowed_types>();
Hence, the add.cpp
file doesn't need to be modified if one changes in arg.hpp
the allowed_types
types list.