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?
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 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.
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.
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();
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With