Zhang
Zhang

Reputation: 3346

Specialization for variadic template

I want to write a function for any number of parameters,

inline void show_log(const char* s)
{
    std::cout << s << std::endl;
}

And I write it as https://en.cppreference.com/w/cpp/language/parameter_pack instructs.

inline void show_log(const char* s)
{
    std::cout << s << std::endl;
}
template<typename T, typename... Targs>
inline void show_log(T s, Targs ... args)
{
    show_log(s);
    show_log(args...);
}

inline void test()
{
    show_log("abc", "ttt", "ccc");
}

It works fine. But I want more strict – make it only accepts const char* parameters. I tried this:

Declare a general template function (but not implement it.)

template<typename T, typename... Targs>
inline void show_log(T s, Targs ... args);

Then implement a specilization only for const char*

template<const char*, typename... Targs>
inline void show_log(const char* s, Targs ... args)
{
    std::cout<<"show_log spelicalized for const char*\n"
    show_log(s);
    show_log(args...);
}

Call the function,

inline void test()
{
    show_log("abc", "ttt", "ccc");
}

It didn't compile with LNK2019 `show_log(const char*, const char*, const char*)' didn't implement error.

Upvotes: 0

Views: 140

Answers (2)

Christopher Mauer
Christopher Mauer

Reputation: 365

The linker error that you're seeing is caused by the fact that you're creating an overload, not a specialization. Functions must be fully specialized since they cannot be overloaded. Classes are allowed to be partially specialized since they cannot be overloaded.

That said, you need to impose a constraint to your template arguments to achieve this behavior.

First, you need to define a meta-function:

// Returns true if all types are the same
// Returns false if any types are different
// Fails to compile if 0 or 1 arguments are passed in
template<typename first_t, typename ... rest_t>
struct is_all_same : std::conjunction<std::is_same<first_t, rest_t>...> {};

template<typename ... types_t>
constexpr auto is_all_same_v = is_all_same<types_t...>::value;

And then you can use the meta function in a number of ways to restrict your function usage.

// Using a static assert
template<typename T, typename... Targs>
inline void show_log(T s, Targs ... args)
{
    static_assert(is_all_same_v<const char*, T, Targs...>, "Arguments must be of type: const char*");
    show_log(s);
    show_log(args...);
}

// requires clause is only available in c++20
template<typename T, typename... Targs> requires is_all_same_v<const char*, T, Targs...>
inline void show_log(T s, Targs ... args)
{
    show_log(s);
    show_log(args...);
}

// Use SFINAE (do yourself a favor and use C++ requires instead of this if it's available to you)
template<typename T, typename... Targs>
inline std::enable_if_t<is_all_same_v<const char*, T, Targs...>> show_log(T s, Targs ... args)
{
    show_log(s);
    show_log(args...);
}

One thing to note, is that you appear to be using recursion to accomplish this. However, you do not need to use recursion here if you have C++17 available (which I assume you do, given the tag). I would recommend doing something like this for c++17:

inline void show_log(const char* s)
{
    std::cout << s << std::endl;
}

// No recursion. Be nice to your compiler when you can
template<typename ... args_t>
inline auto show_log(args_t ... args)
{
    static_assert(is_all_same_v<const char*, args_t...>, "Arguments to show_log must have the type: const char*");
    (show_log(args), ...);
}

If you are going to be using variadics (and templates in general), I would highly recommend keeping the above meta function handy. I use it everywhere in my template code. Understand it and digest it, so you can add more meta functions like it to your tool belt.

Edit: As Keijo excellently points out below, you may actually be interested in the single-function implementation of your pattern. Combining all of the recommendations on this thread yields you with something like this:

// Returns true if all types are the same
// Returns false if any types are different
// Fails to compile if 0 or 1 arguments are passed in
template<typename first_t, typename ... rest_t>
struct is_all_same : std::conjunction<std::is_same<first_t, rest_t>...> {};

template<typename ... types_t>
constexpr auto is_all_same_v = is_all_same<types_t...>::value;

template<typename ... args_t>
auto show_log(args_t ... args) noexcept -> void
{
    using wanted_type = const char*;
    static_assert(is_all_same_v<wanted_type, args_t...>, "Arguments must be of type: const char*");
    ((std::cout << args << '\n'), ...) << std::flush;
}

This solution:

  • Is defined by a single variadic function
  • Is C++17 compatible
  • Throws a compile error if any of the arguments are not of type const char*
  • Allows you to reuse is_all_same_v<args_t...> wherever you see fit
  • Gives a useful compiler error
  • Avoids unnecessary flushes by using '\n' rather than std::endl
  • Removes the inline keyword, since this is a function template (which natively has the same semantics as an inline function and is therefore redundant... Note that this does not apply to full specializations of function templates, since specializations are not templates)

Upvotes: 3

tirimatangi
tirimatangi

Reputation: 174

If you don't need endl between the arguments, you can do without specialization.

using WantedType = const char *;

template <class... Args>
std::enable_if_t<std::conjunction_v<std::is_same<WantedType, Args>...>>
show_log(Args... args)
{
    (std::cout << ... << args) << std::endl;
}

Can anyone think of how to add linefeeds into this solution without specialization? Maybe it is not possible?

--- EDIT ---

As Christopher points out in comments, there is a neat single function solution:

using WantedType = const char *;

template <class... Args>
std::enable_if_t<std::conjunction_v<std::is_same<WantedType, Args>...>>
show_log(Args... args)
{
    ((std::cout << args << '\n'), ...) << std::flush;
}

Upvotes: 2

Related Questions