Sep 4, 2011

[C++] C++0x Rvalue in Depth

"lvalueness versus rvalueness is a property of expressions, not of objects"
"Lvalue references and rvalue references are distinct types."
functions returning by value should return Type (like strange()) instead of const Type (like charm()). The latter buys you virtually nothing (forbidding non-const member function calls) and prevents the move semantics optimization.

  • Move constructors and move assignment operators are never implicitly declared.
  • The implicit declaration of a default constructor is inhibited by any user-declared constructors, including copy constructors and move constructors.
  • The implicit declaration of a copy constructor is inhibited by a user-declared copy constructor, but not a user-declared move constructor.
  • The implicit declaration of a copy assignment operator is inhibited by a user-declared copy assignment operator, but not a user-declared move assignment operator.

Basically, the automatic generation rules don't interact with move semantics, except that declaring a move constructor, like declaring any constructor, inhibits the implicitly declared default constructor.

Remember from C++98/03 that named lvalue references are lvalues (if you say int& r = *p; then r is an lvalue) and unnamed lvalue references are also lvalues (given vector v(10, 1729), calling v[0] returns int& , an unnamed lvalue reference whose address you can take).

Rvalue references behave differently:
  • Named rvalue references are lvalues.
  • Unnamed rvalue references are rvalues.

#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;
 
template <typename T> struct RemoveReference {
     typedef T type;
};
 
template <typename T> struct RemoveReference<T&> {
     typedef T type;
};
 
template <typename T> struct RemoveReference<T&&> {
     typedef T type;
};
 
template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
    return t;
}
 
class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;
 
        m_p = NULL;
    }
 
    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;
 
        m_p = new int(n);
    }
 
    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;
 
        m_p = NULL;
        *this = other;
    }
 
#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;
 
        m_p = NULL;
        *this = Move(other); // RIGHT
    }
#endif // #ifdef MOVABLE
 
    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;
 
        if (this != &other) {
            delete m_p;
 
            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }
 
        return *this;
    }
 
#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;
 
        if (this != &other) {
            delete m_p;
 
            m_p = other.m_p;
            other.m_p = NULL;
        }
 
        return *this;
    }
#endif // #ifdef MOVABLE
 
    ~remote_integer() {
        cout << "Destructor." << endl;
 
        delete m_p;
    }
 
    int get() const {
        return m_p ? *m_p : 0;
    }
 
private:
    int * m_p;
};
 
remote_integer frumple(const int n) {
    if (n == 1729) {
        return remote_integer(1729);
    }
 
    remote_integer ret(n * n);
 
    return ret;
}
 
int main() {
    remote_integer x = frumple(5);
 
    cout << x.get() << endl;
 
    remote_integer y = frumple(1729);
 
    cout << y.get() << endl;
}



#include <iostream>
#include <ostream>
using namespace std;
 
template <typename T> struct Identity {
    typedef T type;
};
 
template <typename T> T&& Forward(typename Identity<T>::type&& t) {
    return t;
}
 
void inner(int&, int&) {
    cout << "inner(int&, int&)" << endl;
}
 
void inner(int&, const int&) {
    cout << "inner(int&, const int&)" << endl;
}
 
void inner(const int&, int&) {
    cout << "inner(const int&, int&)" << endl;
}
 
void inner(const int&, const int&) {
    cout << "inner(const int&, const int&)" << endl;
}
 
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
    inner(Forward<T1>(t1), Forward<T2>(t2));
}
 
int main() {
    int a = 1;
    const int b = 2;
 
    cout << "Directly calling inner()." << endl;
 
    inner(a, a);
    inner(b, b);
    inner(3, 3);
 
    inner(a, b);
    inner(b, a);
 
    inner(a, 3);
    inner(3, a);
 
    inner(b, 3);
    inner(3, b);
 
    cout << endl << "Calling outer()." << endl;
 
    outer(a, a);
    outer(b, b);
    outer(3, 3);
 
    outer(a, b);
    outer(b, a);
 
    outer(a, 3);
    outer(3, a);
 
    outer(b, 3);
    outer(3, b);
}


rvalue references: template argument deduction and reference collapsing
Rvalue references and templates interact in a special way. This demonstrates what happens:
#include <iostream>
#include <ostream>
#include <string>
using namespace std;
 
template <typename T> struct Name;
 
template <> struct Name<string> {
    static const char * get() {
        return "string";
    }
};
 
template <> struct Name<const string> {
    static const char * get() {
        return "const string";
    }
};
 
template <> struct Name<string&> {
    static const char * get() {
        return "string&";
    }
};
 
template <> struct Name<const string&> {
    static const char * get() {
        return "const string&";
    }
};
 
template <> struct Name<string&&> {
    static const char * get() {
        return "string&&";
    }
};
 
template <> struct Name<const string&&> {
    static const char * get() {
        return "const string&&";
    }
};
 
template <typename T> void quark(T&& t) {
    cout << "t: " << t << endl;
    cout << "T: " << Name<T>::get() << endl;
    cout << "T&&: " << Name<T&&>::get() << endl;
    cout << endl;
}
 
string strange() {
    return "strange()";
}
 
const string charm() {
    return "charm()";
}
 
int main() {
    string up("up");
    const string down("down");
 
    quark(up);
    quark(down);
    quark(strange());
    quark(charm());
}


C++0x transforms both the function parameter type and the function argument type before matching them together.


    It transforms the function argument type. A special rule is activated (N2798 14.8.2.1 [temp.deduct.call]/3):

  • when the function parameter type is of the form T&& where T is a template parameter, and the function argument is an lvalue of type A, the type A& is used for template argument deduction.
    (This special rule doesn't apply to function parameter types of the form T& or const T& , which behave as they did in C++98/03, nor does it apply to const T&& .)

  • It transforms the function parameter type. Both C++98/03 and C++0x discard references (C++0x discards both lvalue references and rvalue references). In all four cases, this means that we transform T&& into T .

  • References to references are collapsed in C++0x, and the reference collapsing rule is that "lvalue references are infectious". X& & , X& && , and X&& & collapse to X& . Only X&& && collapses to X&& . So, string& && collapses to string& . In templates, things that look like rvalue references aren't necessarily so.

  • So, in Template argument deduction, T&& can accecpt
    • Lvalue -- Special deduction
    • const Lvalue -- Special deduction
    • Rvalue
    • const Rvalue


perfect forwarding: how std::forward() and std::identity work


Fortunately, unnamed lvalue references are lvalues, and unnamed rvalue references are rvalues. So, in order to forward t1 and t2 to inner(), we need to pass them through helper functions that preserve their types but remove their names. This is what std::forward() does:

template <typename T> struct Identity {
    typedef T type;
};
 
template <typename T> T&& Forward(typename Identity<T>::type&& t) {
    return t;
}

When we call Forward(t1), Identity doesn't modify T1 (we'll soon see what it's doing). So Forward() takes T1&& and returns T1&& . This leaves t1's type unchanged (whatever it is; we've seen that it can be string& , const string& , string&& , or const string&&) but removes its name. inner() will observe Forward(t1) as having the same type, lvalueness/rvalueness, and constness as whatever was passed as outer()'s first argument.

what would happen if you accidentally wrote Forward(t1) . (This mistake is slightly tempting, since outer() takes T1&& t1 .) Fortunately, nothing bad would happen. Forward() would take and return T1&& && . This collapses to T1&& . Therefore, Forward(t1) and Forward(t1) are

What is Identity doing? Why wouldn't this work:


template <typename T> T&& Forward(T&& t) { // BROKEN
    return t;
}

If Forward() were written like that, it could be called without explicit template arguments. Template argument deduction would kick in, and we've seen what happens to T&& - it becomes an lvalue reference when its function argument is an lvalue. And the whole problem that we're trying to solve with Forward() is that within outer(), the named t1 and t2 are lvalues even when their types T1&& and T2&& are rvalue references. With the BROKEN implementation, Forward(t1) would still work, but Forward(t1) would compile (so tempting!) and do the wrong thing, acting just like plain t1 . That would be a continual source of pain, so Identity is used to disable template argument deduction. Experienced template programmers should be familiar with this, as it works identically in C++98/03 and C++0x: the double colon in typename Identity::type is a sheet of lead, and template argument deduction can't see through to the left of it. (Explaining that is another story.)



Reference:
Rvalue References: C++0x Features in VC10, Part 2
Lambdas, auto, and static_assert: C++0x Features in VC10, Part 1
MSDN : Lvalues and Rvalues
How to: Write a Move Constructor
Rvalue Reference 與 String 的實作

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.