1

Note: this following code was modified based on the this post: https://stackoverflow.com/a/27375389

#include <iostream>
#include <type_traits>

template<class Head>
void print_args(std::ostream& s, Head&& head) {
    s << std::forward<Head>(head);
}

template<class Head, class... Tail>
void print_args(std::ostream& s, Head&& head, Tail&&... tail) {
    if (std::is_same<Head, uint8_t>::value)
        s << static_cast<int>(head) << ",";       // cast uint8_t so that the value of 1 or 0 can be displayed correctly in console
    else
        s << std::forward<Head>(head) << ",";
    print_args(s, std::forward<Tail>(tail)...);
}

template<class... Args>
void print_args(Args&&... args) {
    print_args(std::cout, std::forward<Args>(args)...);
}

int main()
{
    uint8_t val = 1;
    print_args(std::string("hello"), val); // error: invalid static_cast from type 'std::basic_string<char>' to type 'int'
    print_args("hello", val); // error: invalid static_cast from type 'const char [6]' to type 'int'
}

Question> I need to cast uint_8 to int so the value can be displayed correctly in console. However, the above code has build issue for either std::string or const char*.

What is the fix for the function?

q0987
  • 34,938
  • 69
  • 242
  • 387
  • You need to defer evaluation. Both branches of the `if` should be valid no matter the type, which is not the case right now. Or upgrade your compiler and use `if constexpr`. – Hatted Rooster Aug 31 '18 at 14:01
  • 1
    Yup, `if constexpr` or SFINAE. I think it's a dupe btw. It's akin to this one: https://stackoverflow.com/questions/51432244/enable-if-using-a-constexpr-bool-test-not-working/51432422#51432422 – luk32 Aug 31 '18 at 14:03

3 Answers3

5

That if is evaluated at run-time, so that the compiler has to compile both branches for it and static_cast<int>(head) fails to compile for non-arithmetic types (e.g. std::string).

In C++17 you can fix that with if constexpr(std::is_same<Head, uint8_t>::value).

Prior to C++17, add an extra function that you can overload for uint8_t:

template<class T>
auto convert(T&& t) -> decltype(std::forward<T>(t)) {
    return std::forward<T>(t);
}

int convert(uint8_t t) {
    return t;
}

template<class Head>
void print_args(std::ostream& s, Head&& head) {
    s << convert(std::forward<Head>(head));
}

template<class Head, class... Tail>
void print_args(std::ostream& s, Head&& head, Tail&&... tail) {
    s << convert(std::forward<Head>(head)) << ',';
    print_args(s, std::forward<Tail>(tail)...);
}
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
2

You have a couple of problems. For the compile time issue:

if (std::is_same<Head, uint8_t>::value)

to this:

if constexpr (std::is_same<Head, uint8_t>::value)

That's because you only want to compile the body of the if when it's valid to cast to a uint. Just because a runtime if is false doesn't mean the code inside it doesn't have to be valid. A constexpr if solves this.

Next, your type comparison is too strict. Comparing a reference to a non-reference will return false. So you want to decay the type before the is_same test. I also prefer is_same_v here:

if constexpr (std::is_same_v<std::decay_t<Head>, uint8_t>)

Finally, your base case, the last element, still needs the same kind of "if constexpr" to print it correctly, but you only applied this logic in the main loop.

Putting it together:

template<class Head>
void print_args(std::ostream& s, Head&& head) {
    if constexpr (std::is_same_v<std::decay_t<Head>, uint8_t>)
        s << static_cast<int>(head);
    else
        s << head << ",";}

template<class Head, class... Tail>
void print_args(std::ostream& s, Head&& head, Tail&&... tail) {
    if constexpr (std::is_same_v<std::decay_t<Head>, uint8_t>)
        s << static_cast<int>(head) << ",";
    else
        s << head << ",";
    print_args(s, std::forward<Tail>(tail)...);
}

template<class... Args>
void print_args(Args&&... args) {
    print_args(std::cout, std::forward<Args>(args)...);
}

You could factor out the common if constexpr into a single helper function, to reduce the redundant code, keeping it DRY. But rather than go there, I'll propose something else.

Just getting it to work is not always the best goal. Recursive templates can use more memory in the compiler and slow down builds, so solutions that avoid recursion are better if they produce the same results.

As such, consider a fold expression (c++17) and a lambda to print the code:

All of the above can be replaced with this:

template<class... Args>
void print_args(Args&&... args) {
    ([](auto&& arg)
    {
        if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, uint8_t>)
            std::cout << static_cast<int>(std::forward<Args>(arg));
        else
            std::cout << arg;
        if (sizeof...(Args) > 1)
            std::cout << ",";
    }(args), ...);
}

Breaking it down, it's using the fold expression on the comma operator, using an IILE (Immediately Invoked Lambda Expression) of the form (f(x) op ...) where f(x) is the lambda (given the current arg), and "op" is comma operator for sequencing the calls.) The second "if" prevents a trailing comma.

Chris Uzdavinis
  • 6,022
  • 9
  • 16
0

One possible solution:

#include <iostream>
#include <type_traits>

namespace detail
{
    namespace cpp11
    {
        template<class arg>
        void print(std::ostream& s, arg&& a, typename std::enable_if<std::is_same<typename std::remove_reference<typename std::remove_cv<arg>::type>::type, uint8_t>::value>::type* =0)
        {
            s << static_cast<int>(a) << ",";
        }

        template<class arg>
        void print(std::ostream& s, arg&& a, typename std::enable_if<!std::is_same<typename std::remove_reference<typename std::remove_cv<arg>::type>::type, uint8_t>::value>::type* =0)
        {
            s << std::forward<arg>(a) << ",";
        }
    }

    namespace cpp14
    {
        template<class arg>
        void print(std::ostream& s, arg&& a, std::enable_if_t<std::is_same<typename std::remove_reference<typename std::remove_cv<arg>::type>::type, uint8_t>::value>* =0)
        {
            s << static_cast<int>(a) << ",";
        }

        template<class arg>
        void print(std::ostream& s, arg&& a, std::enable_if_t<!std::is_same<typename std::remove_reference<typename std::remove_cv<arg>::type>::type, uint8_t>::value>* =0)
        {
            s << std::forward<arg>(a) << ",";
        }
    }

    namespace cpp17
    {
        template<class arg>
        void print(std::ostream& s, arg&& a)
        {
            if constexpr (std::is_same_v<std::remove_reference_t<std::remove_cv_t<arg>>, uint8_t>)
                s << static_cast<int>(a) << ",";
            else
                s << std::forward<arg>(a) << ",";
        }
    }

    namespace cpp20
    {
        template<class arg>
        void print(std::ostream& s, arg&& a)
        {
            if constexpr (std::is_same_v<std::remove_cvref_t<arg>, uint8_t>)
                s << static_cast<int>(a) << ",";
            else
                s << std::forward<arg>(a) << ",";
        }
    }

    template<class Head>
    void print_args(std::ostream& s, Head&& head)
    {
        //cpp11::print(s, std::forward<Head>(head));
        //cpp14::print(s, std::forward<Head>(head));
        //cpp17::print(s, std::forward<Head>(head));
        cpp20::print(s, std::forward<Head>(head));
    }

    template<class Head, class... Tail>
    void print_args(std::ostream& s, Head&& head, Tail&&... tail)
    {
        print_args(s, std::forward<Head>(head));
        print_args(s, std::forward<Tail>(tail)...);
    }
}

template<class... Args>
void print_args(Args&&... args)
{
    detail::print_args(std::cout, std::forward<Args>(args)...);
}

int main()
{
    uint8_t val = 1;
    print_args(std::string("hello"), val);
    print_args("hello", val);
}

Few additional notes:

  • it is only checks for uint8_t, but you may face the problem for other POD types too, like int8_t, byte, etc.
  • usally these print-like function should return the std::ostream parameter by ref: template<...> std::ostream& print(...) {...; return s;}
  • you can implement easily std::remove_cvref{_t} with std::remove_cv{_t} and std::remove_reference{_t}
Balázs Árva
  • 246
  • 5
  • 15