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.

ph3rin
  • 4,426
  • 1
  • 18
  • 42

1 Answers1

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 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) – ph3rin Oct 17 '19 at 17:07
  • What if I do `const std::string& str = []() -> auto& { thread_local const std::string& ret = "Hello, World!"; return ret; }();`? – ph3rin Oct 17 '19 at 17:32
  • @KaenbyouRin: That works, but why not just make a static class member for the default and save the damage to your soul? – Davis Herring Oct 18 '19 at 01:49
  • @DavisHerring To make sure that it only get initialized when the function is called? – ph3rin Oct 18 '19 at 02:25
  • @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? – Davis Herring Oct 18 '19 at 02:34
  • `std::string_view` fix issue in current case. In general case, referencing (lazy) static variable would work too (but seems too verbose and easy to forget). My preference would go to CTAD of linked question. – Jarod42 Oct 18 '19 at 07:37