当转发参数时,我应该使用()还是{}?

14

我有如下的类:

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,而() 则不是。这就是差异所在。只有知道您希望该类如何行事,因此它应该做什么。 - Angew is no longer proud of SO
你应该避免同时拥有这些构造函数,你可以添加一些标记来明确:foo(size_with_value_tag, std::size_t s, int v) - Jarod42
3个回答

2

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

  • {}将调用形式为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";
}
顺便提一下:关于initializer_listcppreference页面有一个很好的例子。
如果需要(即支持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,类构造函数最终是混淆的源头,并在对象构造期间表现出来。foo(1,2)和foo{1,2}之间有区别;后者调用初始化列表构造函数。解决这个问题的一种技巧是使用“标签”来区分“正常”的构造函数形式和initializer_list形式。

1
一个完全没有构造函数的类可以使用{}来支持。 - Yakk - Adam Nevraumont
@Yakk,没错。std::make_unique也有这个问题;http://coliru.stacked-crooked.com/a/7a95e11d3238ea9c - Niall

2
在这种情况下,因为该类有一个构造函数可以使用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();
}

2
如果你想要明确地调用"may to so explicitly by calling",你可能需要使用forward_args(std::initializer_list<type>{ ... });,否则它将无法被推导。 - Piotr Skotnicki
有道理。我尝试过的所有标准库都使用()std::make_unique内部进行转发。 - user6319548

1

如果可以的话,您不应该这样定义您的类foo。两个构造函数有重叠,并导致两个初始化的情况出现问题:

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

“mean”可以有两种不同的含义;需要编写转发函数的人面临无法解决的问题。这在这篇博客文章中详细介绍。同时,std::vector也存在同样的问题。

相反,我建议使用“带标签”的构造函数:

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

    // ...
}

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

你还可以使用 struct foo_size{ std::size_t size; } 然后以非显式方式调用 foo( foo_size )。不过,必须承认 foo({1}) 仍然令人困惑。 :) - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接