3

Question:

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.

Background (Can be safely ignored)

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.

3
  • 1
    OT: Another solution might be to employ tuples: live demo. Commented Oct 17, 2019 at 7:45
  • My question is not deleted, it's here: stackoverflow.com/q/57547273 :) Commented Oct 17, 2019 at 9:03
  • @L.F. Oh I was referring to another question, not yours :) Commented Oct 17, 2019 at 16:10

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

6 Comments

Interesting. I checked the standard (n4830). It says a temporary expression bound to a reference member from a default member initializer is ill-formed. So my code is actually ill-formed? (although all major compilers accept it, with clang giving a warning about dangling-reference)
What if I do const std::string& str = []() -> auto& { thread_local const std::string& ret = "Hello, World!"; return ret; }();?
@KaenbyouRin: That works, but why not just make a static class member for the default and save the damage to your soul?
@DavisHerring To make sure that it only get initialized when the function is called?
@KaenbyouRin: Why does that matter? Is the cost of one std::string (that probably isn’t even using the heap) significant? Does the timing of its construction affect anything?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.