Sousuke said:
The optimizations that rvalue references make possible are nice, but
I'm having one problem with them (rvalue refs): they sometimes lead to
too much verbosity.
When defining a constructor or a setter, you usually take a const T&
and assign it to one of the class's members. In C++0x, you can
additionally define an overload that takes a T&&:
class OneString
{
public:
OneString(const string& s) : m_s(s)
{
}
OneString(string&& s) : m_s(move(s))
{
}
private:
string m_s;
};
One additional overload is not too much verbosity, but see the two-
argument case:
class TwoStrings
{
public:
TwoStrings(const string& s1, const string& s2) : m_s1(s1),
m_s2(s2)
{
}
TwoStrings(string&& s1, const string& s2) : m_s1(move(s1)),
m_s2(s2)
{
}
TwoStrings(const string& s1, string&& s2) : m_s1(s1),
m_s2(move(s2))
{
}
TwoStrings(string&& s1, string&& s2) : m_s1(move(s1)),
m_s2(move(s2))
{
}
private:
string m_s1;
string m_s2;
};
I don't even know how many overloads would there be for 3 arguments
(27 maybe?).
Is there a way to avoid this verbosity?
I've had another look at this and wonder if the following example might
offer something useful. I have not found a way to reproduce the
semantics of the multiple constructors exactly, but it's an idea, at
least. (The key differences that I have noticed are highlighted in the
analysis below.)
Consider:
// file: 2arg_ctor.cpp
#include <iostream>
#include <utility>
class A
{
public:
A(int i) { std::cout << "A::A(int)\n"; } // ctor
A(const A&) { std::cout << "A::A(const A&)\n"; } // copy ctor
A(A&&) { std::cout << "A::A(A&&)\n"; } // move ctor
};
class D
{
public:
template<typename T1, typename T2>
D(T1&& t1, T2&& t2)
: a1_(std::forward<T1>(t1))
, a2_(std::forward<T2>(t2))
{ print_constructor<T1, T2>(); }
private:
// print helper for outputting constructor specialization
template<typename T1, typename T2>
void print_constructor() const;
private:
A a1_;
A a2_;
};
// Specializations for printing constructor
template<>
void D:
rint_constructor<A, A>() const
{ std::cout << "D:
<A, A>(A&&, A&&)\n"; }
template<>
void D:
rint_constructor<A, A&>() const
{ std::cout << "D:
<A, A&>(A&&, A&)\n"; }
template<>
void D:
rint_constructor<A&, A>() const
{ std::cout << "D:
<A&, A>(A&, A&&)\n"; }
template<>
void D:
rint_constructor<A&, A&>() const
{ std::cout << "D:
<A&, A&>(A&, A&)\n"; }
template<>
void D:
rint_constructor<int, int>() const
{ std::cout << "D:
<int, int>(int&&, int&&)\n"; }
template<>
void D:
rint_constructor<int, int&>() const
{ std::cout << "D:
<int, int&>(int&&, int&)\n"; }
template<>
void D:
rint_constructor<A, const A&>() const
{ std::cout << "D:
<A, const A&>(A&&, const A&)\n"; }
int main()
{
std::cout << "0: Initialize lvalues (A)...\n";
A a1(1), a2(2);
const A a3(3);
std::cout << "\n1: Construct D with (lvalue, lvalue)...\n";
D d1(a1, a2);
std::cout << "\n2: Construct D with (lvalue, rvalue)...\n";
D d2(a1, A(2));
std::cout << "\n3: Construct D with (rvalue, lvalue)...\n";
D d3(A(1), a2);
std::cout << "\n4: Construct D with (rvalue, rvalue)...\n";
D d4(A(1), A(2));
std::cout << "\n5: Construct D with (rvalue, rvalue)!...\n";
D d5(1, 2);
std::cout << "\n6: Construct D with (rvalue, lvalue)...\n";
int i2 = 2;
D d6(1, i2);
std::cout << "\n7: Construct D with (rvalue, const lvalue)...\n";
D d7(A(1), a3);
std::cout << "\n...etc\n";
}
/**
* Compile:
* i686-pc-cygwin-g++-4.5.0 -std=c++0x -static -o 2arg_ctor
* 2arg_ctor.cpp
*
* Output:
* 0: Initialize lvalues (A)...
* A::A(int)
* A::A(int)
* A::A(int)
*
* 1: Construct D with (lvalue, lvalue)...
* A::A(const A&)
* A::A(const A&)
* D:
<A&, A&>(A&, A&)
*
* 2: Construct D with (lvalue, rvalue)...
* A::A(int)
* A::A(const A&)
* A::A(A&&)
* D:
<A&, A>(A&, A&&)
*
* 3: Construct D with (rvalue, lvalue)...
* A::A(int)
* A::A(A&&)
* A::A(const A&)
* D:
<A, A&>(A&&, A&)
*
* 4: Construct D with (rvalue, rvalue)...
* A::A(int)
* A::A(int)
* A::A(A&&)
* A::A(A&&)
* D:
<A, A>(A&&, A&&)
*
* 5: Construct D with (rvalue, rvalue)!...
* A::A(int)
* A::A(int)
* D:
<int, int>(int&&, int&&)
*
* 6: Construct D with (rvalue, lvalue)...
* A::A(int)
* A::A(int)
* D:
<int, int&>(int&&, int&)
*
* 7: Construct D with (rvalue, const lvalue)...
* A::A(int)
* A::A(int)
* A::A(A&&)
* A::A(const A&)
* D:
<A, const A&>(A&&, const A&)
*
* ...etc
*/
Here we can see that the single parameterized constructor handles all
combinations of lvalue/rvalue refs as given, including const lvalues.
If we replace the definition of class D (above) with one that provides
an overloaded constructor to cover all lvalue/rvalue pairs, so:
class D
{
public:
D(const A& a1, const A& a2)
: a1_(a1), a2_(a2)
{ std::cout << "D:
(const A&, const A&)\n"; }
D(const A& a1, A&& a2)
: a1_(a1), a2_(std::move(a2))
{ std::cout << "D:
(const A&, A&&)\n"; }
D(A&& a1, const A& a2)
: a1_(std::move(a1)), a2_(a2)
{ std::cout << "D:
(A&&, const A&)\n"; }
D(A&& a1, A&& a2)
: a1_(std::move(a1)), a2_(std::move(a2))
{ std::cout << "D:
(A&&, A&&)\n"; }
private:
A a1_;
A a2_;
};
(and remove the specializations for D:
rint_constructor, which are
not then needed) we can compare the output from the two examples in
terms of efficiency of the occurrence and number of copies/moves, etc.
=================================+============================
template ctor | overloaded ctor
=================================+============================
0: Initialize lvalues (A)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(int) | A::A(int)
A::A(int) | A::A(int)
=================================+============================
1: Construct D with (lvalue, lvalue)...
---------------------------------+----------------------------
A::A(const A&) | A::A(const A&)
A::A(const A&) | A::A(const A&)
D:
<A&, A&>(A&, A&) | D:
(const A&, const A&)
=================================+============================
2: Construct D with (lvalue, rvalue)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(const A&) | A::A(const A&)
A::A(A&&) | A::A(A&&)
D:
<A&, A>(A&, A&&) | D:
(const A&, A&&)
=================================+============================
3: Construct D with (rvalue, lvalue)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(A&&) | A::A(A&&)
A::A(const A&) | A::A(const A&)
D:
<A, A&>(A&&, A&) | D:
(A&&, const A&)
=================================+============================
4: Construct D with (rvalue, rvalue)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(int) | A::A(int)
A::A(A&&) | A::A(A&&)
A::A(A&&) | A::A(A&&)
D:
<A, A>(A&&, A&&) | D:
(A&&, A&&)
=================================+============================
5: Construct D with (rvalue, rvalue)!...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(int) | A::A(int)
| A::A(A&&)
| A::A(A&&)
D:
<int, int>(int&&, int&&) | D:
(A&&, A&&)
=================================+============================
6: Construct D with (rvalue, lvalue)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(int) | A::A(int)
| A::A(A&&)
| A::A(A&&)
D:
<int, int&>(int&&, int&) | D:
(A&&, A&&)
=================================+============================
7: Construct D with (rvalue, const lvalue)...
---------------------------------+----------------------------
A::A(int) | A::A(int)
A::A(A&&) | A::A(A&&)
A::A(const A&) | A::A(const A&)
D:
<A, const A&>(A&&, const A&) | D:
(A&&, const A&)
=================================+============================
...etc | ...etc
---------------------------------+----------------------------
As I read this it appears that the parameterized constructor version is
*as* efficient in this sense as the overloaded-constructor version.
What is more, in the case where the parameters are lvalue or rvalue
ints, two moves are avoided in all cases.
The main difference, however, is that non-const lvalues are passed into
the constructor by non-const lvalue refs for the parameterized
constructor version (see 1., 2. & 3, above). And, there is a further
significant difference. If you modify A's constructor so that it is
explicit then the code fails for the overloaded-constructor version when
invoked with lvalue or rvalue integers. However, for the parameterized
constructor version, it *succeeds*, instantiating the constructor with a
combination of int/int& template arguments (see 6. & 7., above). This
may, or may not, be a boon or a bane, depending upon what is required by
the model as a whole.
Note: the above was compiled with gcc-4.5.0, which correctly implements
the change from March 2009 (so Pete informs us) preventing rvalue
references binding to lvalue refs.
Regards
Paul Bibbings