c++templates

Is there any way to export what types are used by a C++ template, while hiding implementation?


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().


Solution

  • 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;
    }
    

    Demo

    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.

    Demo