使用聚合初始化模拟默认函数参数是否有任何陷阱?

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

问题:

例如,如果您想要一个可变参数函数,它接受任意数量的参数 Args&&...args,并打印所有这些参数 t 次。更重要的是,你希望t默认为1,所以默认打印一次所有args

您首先要尝试的是:

template <typename ... Args>
void foo(Args... args, unsigned t = 1) {
    for (unsigned i = 0; i < t; ++i) {
        (std::cout << ... << args);
    }
}

显然这不起作用,除非您显式传入模板参数:

// Error: expected 1 argument, got 2
foo(0, "Hello, world!");

因为模板推导时默认参数被当作普通参数处理,参数包永远为空。这会阻止您使该功能有用。 (Related question)

然后我决定使用聚合初始化(尤其是 designated initializer 自 c++ 20 以来)来模拟更强大的 "default parameter"。它看起来像这样:

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;
}

此外,这可能会解决人们对他们无法 "skip" 默认函数参数的抱怨,借助 c++ 20 指定的初始化程序:

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;
}

我想知道这样做是否有任何潜在的陷阱。

背景(可以放心地忽略)

所以昨天我在SO周围闲逛,遇到一个问题(但后来被删除了),询问如何将默认std::source_location参数与可变参数模板结合起来:

template<typename... Args>
void log(Args&&... args, const std::experimental::source_location& location = std::experimental::source_location::current()) {
    std::cout << location.line() << std::endl;
}

显然,正如问题中所述,这并没有按预期工作。所以我想出了以下代码:

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;
}

但是发现它可以做更多,所以这个问题。

生命周期(扩展)至少存在一个缺陷:

const std::string& str = "Hello, world!"; 创建悬挂指针,(不延长成员的生命周期)。

以下是可以的:

void repeat_print(const std::string& str = "Hello, world!", int t = 1) {/*..*/}

int main()
{
    repeat_print();
}

但以下不是:

struct bar_t {
    const std::string& str = "Hello, world!";
    int t = 1;
    void operator() () const { /*..*/ }
};

int main()
{
    bar_t{}();
}

您可以修复 bar_t 以按值获取成员,但在某些情况下您会进行额外的复制。