3

I came across variadic templates while reading a book and thought it would be pretty cool to implement a python style print function.

Here is the code.

#include <iostream>
#include <string>

#define None " "

template<typename T, typename... Tail>
void print(T head, Tail... tail, const char* end = "\n")
{
    std::cout << head << end;
    if constexpr (sizeof...(tail) > 0)
    {
        print(tail..., end);
    }
}

int main()
{
    // Error: no instance of function template "print" matches the argument list
    print("apple", 0, 0.0f, 'c', true);

    // Error: no instance of function template "print" matches the argument list
    print("apple", 0, 0.0f, 'c', true, None);
}

Expected result from those two function calls:

First:    Second:
apple     apple 0 0.0f 'c' 1
0
0.0f
'c'
1

Removing const char* end = "\n" from the function signature gets the code to compile, but I want that functionality of specifying the last parameter to state whether to print a newline.

Is this possible at all?

JeJo
  • 30,635
  • 6
  • 49
  • 88
Cool_Cornflakes
  • 315
  • 2
  • 13
  • 3
    You would have to include that last parameter in the variadic list and manually check for it when unrolling. But how do you intend to differentiate if that last parameter is meant to be the separation character and not just another thing to print? – perivesta Jul 28 '21 at 12:48
  • python has named parameters which C++ does not have. [Designated initializers](https://en.cppreference.com/w/cpp/language/aggregate_initialization) are going in this direction but it is something different. Different languages have different solutions for same problems. Instead of trying to mickick pythons `print` syntax as close as possible I suggest to mickick something that has the same functionality – 463035818_is_not_an_ai Jul 28 '21 at 12:53
  • @dave I see. Could do that. New line character and " " are the things to print though. Could I somehow access the last value in the `tail` sequence and `std::cout` it between each successive print recursion calls? – Cool_Cornflakes Jul 28 '21 at 12:54
  • 1
    just as an idea, how about a calling it like this `print(None)("apple", 0, 0.0f, 'c', true)`. `print` would take the sperator as parameters and return a callable that does the actual printing. Then you can provide a default for the seperator `print()("foo")` – 463035818_is_not_an_ai Jul 28 '21 at 12:56

1 Answers1

5

It is possible, but not in the way you have tried.

You could do something like the following (one possible solution in ):

  • Provide an enum (Ending) which will be used to specify the way of printing (i.e newline, with space, etc).
  • Split function print and one of them will be used to print one argument at a time, where we will check for the way of printing;
  • The other print will be used to call the variadic arguments by the caller. Here we're using fold expressions to call the first print overload.

Something like: (Live Demo)

enum struct Ending {  NewLine = 0, Space };

template<Ending end, typename Type>
void print(const Type& arg) noexcept
{
    if constexpr (end == Ending::NewLine)  {
        std::cout << arg << '\n';
    }
    else if constexpr (end == Ending::Space) {
        std::cout << arg << ' ';
    }
}

template <Ending end, typename... Args>
void print(const Args& ... args) noexcept
{
    (print<end>(args), ...);
}

Now you can specify, how to end the line

print<Ending::NewLine>("apple", 0, 0.0f, 'c', true);
print<Ending::Space>("apple", 0, 0.0f, 'c', true);

"You don't like the overload!?" Then we can make it inside a single print function with help of an immediately invoking lambda function.

(Live Demo)

template <Ending end, typename... Args>
void print(const Args& ... args) noexcept
{
    ([] (Ending, const auto& arg) noexcept {
        if constexpr (end == Ending::NewLine) {
            std::cout << arg << '\n';
        }
        else if constexpr (end == Ending::Space) {
            std::cout << arg << ' ';
        }
    }(end, args), ...);
    //^^^^^^^^^^  ^^^^  ---> Invoke the lambda & expand the fold
}
JeJo
  • 30,635
  • 6
  • 49
  • 88
  • Thanks for the answer. I am curious as to why you are using rvalue references with `Type` and `Args` i.e. `Type&&` instead of just `Type`and same with Args. I see this everywhere in the standard library code but never know why – Cool_Cornflakes Jul 28 '21 at 13:13
  • 1
    `const Type&` would be enough here. – Jarod42 Jul 28 '21 at 13:14
  • 1
    @Cool_Cornflakes It is called [`perfect forwarding`](https://stackoverflow.com/questions/6829241/perfect-forwarding-whats-it-all-about). By which you can perfectly forward (avoid copy) the argument. As Jarod42 mentioned, you do not require here. A `const&` should be enough as the paras are not been modified under the function scope. – JeJo Jul 28 '21 at 13:30