If you want, for example, a variadic function that takes arbitrary number of parameter Args&&...args
, and prints all those arguments t
times. What's more is that you want t
to be defaulted to 1
, so it prints all args
one time by default.
The first thing you would try is:
template <typename ... Args>
void foo(Args... args, unsigned t = 1) {
for (unsigned i = 0; i < t; ++i) {
(std::cout << ... << args);
}
}
Apparently this doesn't work unless you explicitly pass in the template parameters:
// Error: expected 1 argument, got 2
foo(0, "Hello, world!");
Because default parameters are treated as normal parameters while template deduction, and the parameter pack would always be empty. This prevents you from making the function useful. (Related question)
I then decided to use aggregate initialization (especially designated initializer since c++ 20) to simulate a more powerful "default parameter". It looks like this:
struct foo_t {
unsigned t = 1;
template <typename ... Args>
void operator() (Args... args) {
for (unsigned i = 0; i < t; ++i) {
(std::cout << ... << args);
}
}
};
int main() {
foo_t{}("Hello, ", "World!\n"); // prints 1 line
foo_t{ 5 }(0, "Hello, world!\n"); // prints 5 lines
return 0;
}
Moreover, this may solve people's complaint that they cannot "skip" default function parameters, with the help of c++ 20 designated intializers:
struct bar_t {
const std::string& str = "Hello, world!";
int t = 1;
void operator() () {
for (int i = 0; i < t; ++i) {
std::cout << str << std::endl;
}
}
};
int main() {
// Skips .str, using the default "Hello, World!"
bar_t{ .t = 10 }();
return 0;
}
I am wondering whether there are any potential pitfalls to do this.
So yesterday I was wandering around SO and encountered a question (but it was later deleted) that asked about how to combine default std::source_location
parameter with variadic template:
template<typename... Args>
void log(Args&&... args, const std::experimental::source_location& location = std::experimental::source_location::current()) {
std::cout << location.line() << std::endl;
}
Apparently this does not work as expected, just as stated in the question. So I came up with the following code:
struct logger {
const std::experimental::source_location& location = std::experimental::source_location::current();
template <typename... Args>
void operator() (Args&&... args) {
std::cout << location.line() << std::endl;
}
};
int main(int argc, char** argv) {
logger{}("I passed", argc, "arguments.");
return 0;
}
But found out it can do more, thus this question.
There is at least one pitfall with lifetime (extension):
const std::string& str = "Hello, world!";
create dangling pointer, (No lifetime extension for member).
Following is fine:
void repeat_print(const std::string& str = "Hello, world!", int t = 1) {/*..*/}
int main()
{
repeat_print();
}
but following is not:
struct bar_t {
const std::string& str = "Hello, world!";
int t = 1;
void operator() () const { /*..*/ }
};
int main()
{
bar_t{}();
}
You might fix bar_t
to take member by value, but then you would do extra copy in some cases.