开发者

Pass by reference declarations in C++

I want to establish whether my understanding to some basic C++ reference principles are correct. To my understanding, declaring;

void foo(string &arg); //Means get the memory reference of passed argument
{
  cout << arg;        
}  

string arg;
string& arg1; //Means pass the memory reference of 开发者_如何学JAVAarg

Am I correct?

EDITED


The difference lies to what the & qualifier applies to : A type, or a variable?

Let's say you have a type T.

For declarations/parameters (& is a type qualifier) :

T v1 = 13 ;    // v1 is a variable of type T, whose value is 13
T v2 = 42 ;    // v2 is another variable of type T, whose value is 42
T * v3 ;       // v3 is an uninitialized pointer to a variable of type T
T * v4 = &v1;  // v4 is pointer pointing to v1 (the value of v4 is the
               // address of v1)
T & v5 = v1 ;  // v5 is an alias of v1
T & v6 ;       // WON'T COMPILE. An alias MUST be initialized.

For operations (& is then an operator) :

&v1 ;    // returns the address of v1 (e.g. 0x00ABCDEF)
v4 ;     // returns 0x00ABCDEF (because v4 was initialized to point to
         // v1)
*v4 ;    // returns the dereferenced value of pointer v4, that is: 13
v5 ;     // returns the value inside v1 (the aliased variable of v5),
         // that is: 13

We can now mix the two notations :

// We can reattribute the variables pointed by pointers
T * v7 = &v1 ; // v7 is a pointer to the variable v1
*v7 ;          // returns v1's value, that is 13
v7 = &v2 ;     // v7 is now pointing to v2
*v7 ;          // returns v2's value, that is 42

// We cannot reattribute the aliases referencing variables
// because once initialized, aliases **are** the variable they
// were initialized to.
v5 = v2 ;      // v5 is an alias of v1, so this won't reattribute it
               // instead, it will put the value of v2 into v5 and
               // thus v1
               // this, writing v5 = v2 is like writing v1 = v2
v2 ;           // v2 is still 42
v1 ;           // v1 has a value of 42 (since the v5 = v2 line above)
v5 ;           // v5 is still the alias of v1, and thus, its value is 42
v2 = 57 ;      // v2's value is now 57
v1 ;           // v1's value is still 42 (changing v2 has no impact on
               // v1 because they are NOT aliases. They are distinct
               // variables
v5 ;           // v5 is still the alias of v1, and thus, its value is
               // still 42

Detail : About C

The C language had only the notion of value, and pointer to value (and pointer to pointer to value, and pointer to... etc.), meaning that you had a notion of referencing/dereferencing (unrelated to C++ references...) with unary operators & and *.

T ** p ;     // is the declaration of a pointer to a pointer
             // to a value of type T

p ;          // is still the pointer to pointer
&p ;         // returns the address of the p variable
             // meaning you can put that address in a variable
             // of type T ***:
T *** pp = &p ;

&&p ;        // This has no meaning whatsoever in C and C++
             // because an address is a simple raw number, and a
             // a number has no address: Only variables have
             // addresses

*p ;         // this is p, dereferenced once, meaning you get
             // to have the value at the address given by p, which
             // is still an address (a pointer of type T *)
**p ;        // this is p, dereferenced twice, meaning you get
             // to have the value at the address given by *p,
             // which is of type T

The problem is that unary operators & and * are not really symetrical. For example :

T t = v ;
T * p = &t ;
T * p2 = &t ;       // p and p2 are two different pointers containing
                    // the same address, and thus pointing at the same
                    // value v
p == p2 ;           // is true, because both different pointers contain
                    // the same address
*p == *p2 ;         // is true, because both different pointers point
                    // to the same value (as they contain the same
                    // address)
&p == &p2  ;        // is false, because each variable p and p2 is
                    // distinct from the other, and thus, have different
                    // addresses

So, in C :

  • unary operator * will retrieve a value at the address contained by a pointer variable
  • unary operator & will retrieve the address of a variable

Detail : About C++

In C++, for multiple reasons (but the need was first discovered for operators, but there are multiple other reasons, like value constructors, and mostly avoiding polluting the code with pointers and useless NULL tests), there is a notion of (C++) reference, that is, an alias to a value :

In C++, in addition to applying the & qualitifer to a variable (which retrieves its address), you can apply it instead to a type (which makes its variable a reference/alias).

So, when you have :

T t = v ;
T * p = &t ;    // p is a pointer containing the address of the t
                // variable
T ** pp = &p ;  // pp is a pointer containing the address of the p
                // variable
T & r = t ;     // r is a reference to/an alias of t. It behaves as
                // if it was t in all aspects
T *& r = p ;    // If you understand that line, then you're ready for
                // C++ references (i.e. r is an alias of a pointer to T,
                // here, an alias of p)
T **& rr = pp ; // rr is an alias of a pointer to a pointer to T,
                // here, an alias of pp)

I'm guessing here, but it is quite probable the references r and rr are optimized away at compile time (i.e. only t and p remains)

Detail : About C++11 (formerly known as C++0x)

As this question was tagged C++0x, I'll speak about it, and the new && r-value reference.

The references/aliases did not change from C++ to C++11. But there is another type of "reference" that was introduced (as the && type qualifier), that is, the r-value reference, in addition to C++ simple references/aliases.

Because C++ has value semantics, some processing can be quite expensive. For example, you can have a lot of useless temporaries if you write your code the wrong way.

The move semantics were added to handle this problem: Why creating a lot of copies of the same object, if in the end, we will dump the copies to the garbage, and only keep the last one ?

For example, the following code:

  1  |  T foo()
  2  |  {
  3  |     T a ;
  4  |     // put some values in T
  5  |     return a ;
  6  |  }
  7  |  
  8  |  void bar()
  9  |  {
 10  |     T b = foo() ;
 11  |  }

Barring optimizations (return-value-optimization comes to mind, but also inlining), this code will create a value a (line 3) or type T. As it returns a type T (line 5), it will make a temporary copy of a, which we'll call x, and then destroy a.

At line 10, the value b will be initialized using the temporary value x (which is the so called r-value), and then, x will be destroyed.

Meaning that to initialize b, you created two variables, one explicitly (a) and one implicitely x) that were destroyed soon after, which can be expensive if the construction of a type T is expensive.

(As an amusing side node, I had to add a lot of complexity to this example to stop g++ from optimizing through r-v-o and demonstrate the move semantics effect on my example code...)

The solution is to create a move constructor (and probably a move operator =, for completeness), that is, something with the following prototype:

T::T(T && p_t) ;                 // move constructor
T & T::operator = (T && p_t) ;   // move operator =

Which can be compared to C++ usual copy constructors/operator =:

T::T(const T & p_t) ;                 // copy constructor
T & T::operator = (const T & p_t) ;   // operator =

So back to the example above, we add move semantics to T:

class T
{
   V * m_v ; // some complex data, expensive to create
             // and expensive to destroy
   // etc.
}

// destructor :
// Clean its internals if needed
T::~T()
{
   delete this->m_v ; // potentially expensive if m_v is not NULL
}

// copy constructor :
// Do not modify the original, and make a copy of its internals
T::T(const T & p_t)
{
   this->m_v = new V(p_t.m_v) ; // potentially expensive
}

// move constructor
// the original is a temporary (guaranteed by the compiler)
// so you can steal its internals, as long as you keep the
// temporary clean, no one cares
T::T(T && t)
{
   this->m_v = t.m_v ; // stealing the internals of the temporary
   t.m_v = NULL ;      // the temporary is now "empty"
}

This way, the code above (with foo and bar, without any change) will avoid creating two temporary objects of type T, because of T's support of move semantics.

P.S.: adding a move constructor means you should add a move operator =, too.


string str;
string &arg1 = str; 
string& arg2 = str; 
string *ptr = &str;

Means arg1 & arg2 are references to a variable str which is of the type string, it means they are just an alias name for variable str.
They Both essentially declare a reference variable as said above it is just a matter of style where the & is placed.

ptr is a pointer to a variable str which is of the type string.

Note:
A reference must be initialized to a variable at the time of creation, and it cannot be made to refer any other variable after the initialization. A Reference always remains alias to the same variable. So you should not be just doing just:

string& arg2;

The compiler will give you an error for this, something like:

error: ‘arg2’ declared as reference but not initialized


None of your examples look legal because of the semi-colons. But ignoring those

  1. Doesn't look legal at all
  2. Assuming arg is a pointer, then *arg means the value of the thing being pointed to (strictly its a refernce to the value but ignore that for now)
  3. Looks like a declaration and if so is declaring arg as a reference to a string
  4. Is the same as 3, the whitespace doesn't matter

It's not really possible to say the meaning without the context. So if this isn't clear post more complete code.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜