Search code examples
c++c++20designated-initializer

Are there any pitfalls to simulate defaulted function parameters using aggregate initialization?


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.


Solution

  • 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.