Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have the following class:

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

And I want to forward arguments to it like this:

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

If I replace {} with () the output is 1 1 2 instead of 2 1 1.

Which would make the most sense for my class?

like image 751
user6319548 Avatar asked May 11 '16 09:05

user6319548


People also ask

What is forwarding reference?

I understand that a forwarding reference is "an rvalue reference to a cv-unqualified template parameter", such as in. template <class T> void foo(T&& ); which means the above function can take both l-value and r-value reference.

What is forwarding in c++?

What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.

What does std forward?

std::forward This is a helper function to allow perfect forwarding of arguments taken as rvalue references to deduced types, preserving any potential move semantics involved.


3 Answers

In this case, because the class has a constructor which take an std::initializer_list, using {} in the factory function will often change the semantic meaning of any argument list you pass in - by turning any pack of arguments longer than 2 into an initializer_list.

This will be surprising to users of the function.

Therefore, use the () form. Users who want to pass an initializer_list may to so explicitly by calling

forward_args({ ... });

NB for the above syntax to work you'll need to provide another overload:

template <class T>
auto forward_args(std::initializer_list<T> li)
{
    return foo(li).size();
}
like image 175
Richard Hodges Avatar answered Oct 13 '22 10:10

Richard Hodges


The use of {} vs. () determines which constructor is called.

  • The {} will call the form foo(std::initializer_list<int> d) and you get the results you seem to expect.

  • The () will call the explicit foo(std::size_t s) and foo(std::size_t s, int v), and in all cases, the first element is the size, so given the arguments, you get the results you see.

Which form to favour depends on what semantics you want the forward_args method to support. If you want the parameters to be passed "as is", then the () should be used (in this case the user will need to provide an initialiser_list as an argument to begin with).

Possibly (probably) the form you wish to favour is () and used as follows;

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

Side note; the cppreference page on the initializer_list has a good example of this behaviour.

If desired (i.e. to support forward_args({1,2})), you can provide an overload for initializer_list;

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

As noted: given the initializer_list, the class constructors are ultimately the source of the confusion, and this presents itself during the object construction. There is a difference between foo(1,2) and foo{1,2}; the latter calls the initializer_list form of the constructor. One technique to resolve this is to use a "tag" to differentiate the "normal" constructor forms from the initializer_list form.

like image 30
Niall Avatar answered Oct 13 '22 10:10

Niall


Really, you shouldn't be defining your class foo like this (provided it is within your control). The two constructors have an overlap and result in a bad situation where the two initializations:

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

Mean two different things; and people who need to write forwarding functions face unsolvable problems. This is covered in detail in this blog post. Also std::vector suffers from the same problem.

Instead, I propose you use a 'tagged' constructor:

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

    // ...
}

Now you have no overlap, and you can safely use the {} variant.

like image 35
Andrzej Avatar answered Oct 13 '22 09:10

Andrzej