转发参数时应该使用()或{}吗

Should I use () or {} when forwarding arguments?

本文关键字:参数 转发      更新时间:2023-10-16

我有以下类:

struct foo
{
    std::size_t _size;
    int* data;
public:
    explicit foo(std::size_t s) : _size(s) { }
    foo(std::size_t s, int v)
        : _size(s)
    {
        data = new int[_size];
        std::fill(&data[0], &data[0] + _size, v);
    }
    foo(std::initializer_list<int> d)
        : _size(d.size())
    {
        data = new int[_size];
        std::copy(d.begin(), d.end(), &data[0]);
    }
    ~foo() { delete[] data; }
    std::size_t size() const { return _size; }
};

我想提出这样的论点:

template <typename... Args>
auto forward_args(Args&&... args)
{
    return foo{std::forward<Args>(args)...}.size();
    //--------^---------------------------^
}
std::cout << forward_args(1, 2) << " " << forward_args(1) << " " 
          << forward_args(2) << "n";

如果我用()替换{},则输出为1 1 2而不是2 1 1

哪一个对我的课最有意义?

在这种情况下,因为类有一个采用std::initializer_list的构造函数,所以在工厂函数中使用{}通常会改变传入的任何参数列表的语义,方法是将任何长于2的参数包变成initializer_list。

这将使该功能的用户感到惊讶。

因此,使用()表单。想要传递initializer_list的用户可以通过调用来显式传递

forward_args({ ... });

注意,要使上述语法正常工作,您需要提供另一个重载:

template <class T>
auto forward_args(std::initializer_list<T> li)
{
    return foo(li).size();
}

{}()的使用决定了调用哪个构造函数。

  • {}将调用表单foo(std::initializer_list<int> d),您将得到您所期望的结果。

  • ()将调用explicit foo(std::size_t s)foo(std::size_t s, int v),在所有情况下,第一个元素都是大小,所以给定参数,就会得到您看到的结果。

支持哪种形式取决于您希望forward_args方法支持什么语义。如果希望参数按原样传递,则应使用()(在这种情况下,用户需要首先提供initialiser_list作为参数)。

可能(可能)您希望使用的形式是(),使用如下

template <typename... Args>
auto forward_args(Args&&... args)
{
    return foo(std::forward<Args>(args)...).size();
    //--------^---------------------------^
}
int main()
{
    std::cout << forward_args(std::initializer_list<int>{1, 2}) << " "
              << forward_args(std::initializer_list<int>{3}) << " " 
              << forward_args(std::initializer_list<int>{4}) << "n";
}

旁注;CCD_ 19上的cppreference页面就是这种行为的一个很好的例子。

如果需要(即支持forward_args({1,2})),您可以为initializer_list提供过载;

template <class Arg>
auto forward_args(std::initializer_list<Arg> arg)
{
    return foo(std::move(arg)).size();
}

如前所述:给定initializer_list,类构造函数最终是混乱的根源,这在对象构造过程中表现出来。CCD_ 23与CCD_;后者调用构造函数的CCD_ 25形式。解决这个问题的一种技术是使用"标记"来区分"普通"构造函数形式和initializer_list形式。

实际上,您不应该这样定义类foo(前提是它在您的控制范围内)。这两个构造函数有重叠,并导致两个初始化的糟糕情况:

foo f1(3);
foo f2{3};

意思是两种不同的东西;并且需要编写转发函数的人面临无法解决的问题。这在本博客文章中有详细介绍。CCD_ 28也存在同样的问题。

相反,我建议您使用一个"标记"构造函数:

struct with_size {}; // just a tag
struct foo
{
    explicit foo(with_size, std::size_t s);
    explicit foo(with_size, std::size_t s, int v);
    foo(std::initializer_list<int> d);
    // ...
}

现在您没有重叠,并且可以安全地使用{}变体。