综合编程

C++ the good parts — std::swap

微信扫一扫,分享到朋友圈

C++ the good parts — std::swap

std::swap is quite fundamental. And fundamentally glanced over by you :) Which is a pity, because understanding std::swap concept fully, will make you very clear on many other fundamental concepts. Not just in C++.

It is now up to you. Just leave?  Or tap on “more” below.

The easy part

The values. As you know by now, modern C++ (after 11 that is)  is predominantly an “value semantics” based language.  Swap + value semantics = easy, no?

// swap two integer values
int A{ 42 };
int B{ 13 };
int Temp{};
 
Temp = A ;
A = B;
B = Temp;
// check the swap has worked
assert( A == 13 ) ;
assert( B == 42  ) ;

What was B, now is A, and what was A ,now is B. And here is this Temp . To make it all possible. No it can not be done without it. Many have tried. In case you know how to do a swap without a temporary, please publish your work.

Boring. And where is in here this C++ “good part”? It is in realization that “swap” is not “exchange”. Above snippet is exchange; not swap. A and B are both well and alive. Before we go any further a bit more on the role of swap and move.

Leaving the scope

move is needed to be defined and seen separately because of the mechanism deployed upon C++ objects/values when leaving the scope.  And what is happening is: nothing in particular. They dissapear as ever before.

{  int k{} ; }
// k does not exist here

And as you all know if function is the scope, to preserve the value upon leaving the function one has to return it. Not set a reference or pointer argument to it.

//
int function (int & a, int * b)
{
   int internal{ 42 };
   a = internal; // reference to temporary
   b = & internal; // pointer to temporary
   return internal;
  // internal goes out of scope here
}
 
int v1;
int v2 ;
int x = function( v1, & v2) ;

After that only x will contain 42. v1 and v2 will contain garbage.  That is  because return value is moved to a x during the call. Let me repeat that: moved . Recap.

//
struct T { int val{}; };
// return T by value
T function ()
{
  T internal{ 42 };
  return internal;
  // internal goes out of scope here
  // it is *moved* to a function return value
}
// move constructor of T is used
T x { function( ) };

What happens in assembly code is basicaly this

// pseudo code
// special hidden temporary is created
T _special_ = function() ;
// move constructor of T is used
// T ( T && )
// to move _special_ to x
T x { function( ) } ;

__special__ is thus basically and realy dissaperaing after it is moved to x.

Hence the argument to the move constructor T ( T && another_ ) does not survive after constructor exits It is an value argumemt. rva;ue argument. Please read further for more details.

Swap must not be an exchange

Exchange of values is the other fundamental and distinctive operation.  Language you code in, must allow you to separate thefundamental value operations.  The key difference between swap and exchange is lifetime of values swapped or exchanged. Pseudo code.

Swap ( A , B )  // B life is over, it is a zombie. Do not deal with zombies
Exchange (A, B) // both B and A are alive and well, content exchanged

That is fundamental to introducing moving into the to modern C++.  And moving is fundamental in making value semantics feasible. Hence the addition of “move constructor” and “move assignment”. They are core language features of modern C++.  As you know modern C++ generates 6 functions for each of your seemingly empty classes. I assume you know that and will not bother you here with that “detail”.

Evasive maneuvers

Now. to avoid making this post into an e-book about std::move , std::forward . RRef’s, URef’s and all those lovely little devils of C++ let me try an evasive maneuver. Please refresh your knowledge quickly, on reading this Scott Meyers post .

And in there is a comment from one Dave Abrahams which is also crucial for this post:

“..You use move(x) when you know you want to move from x, because it is a value that is no longer needed for anything else. Think of this, at a high level, as enabling optimization of copy…”

So that is it. Value semantics.  Moving is cheap. The copy is expensive.  Why is move cheap? Because you only swap internals inside move. Let dive deeper in a simple example from above.

// type T
struct T {
   int data{};
// canonical move ctor
T ( T && another )
{
   swap( this->data, another.data ) ;
}
} ; // eof T

Thus you  need for the above, that swap:

// arguments are references
void swap ( int & A, int & B )
{
  int T = A;
  A = B ;
  B = T ; // T is temporary!
}

B is moved to A above. B is a zombie now. How is this achieved? B has been assigned an temporary value. As simple as that. After leaving that swap above, B exists but is a zombie. B has no meaningfull content any more. It is said to be moved from.

Psuedo code for std::swap is

template < typename T>
void std::swap ( T & a, T & b )
{
   T temp = std::move( b ) ;
     a = std::move( b );
     b = std::move( temp );
}

That is generic. Hence the use of std::move.

Values swapping

That’s it. Done. Let’s go out and have some fun?! Well … not yet.  What comes is:

The important part

Where std::move can teach you even more fundamentals o yet another very important concept: pointer swapping.

Now please consider this and implement that swap function in here.

//
type T struct T {
  // native pointer to an int value
  int * data{};
  // canonical move ctor
T ( T && another ) {
   swap( data, another.data ) ;
}
} ; // eof T

I am sure here and now, 99% or more of you will produce the following, as your generic swap for pointer arguments

// your generic swap overload for pointers
template < typename T>
void swap ( T * a, T * b )
{
   T * temp = b ;
   a = b ;
   b = temp ;
}

The problem is: that is not swap, that is pointer exchange.  Also called “ pointer rewiring “.  b will be alive and kicking and importantly addresses of a and b will be exchanged too. Thus  (most importantly ) address of a will be changed. Watch carefully.

Pointer rewiring step ONE

That is the siutation upon entering your rewiring function.

Pointer rewiring step TWO

This is where “pointer to A” loses its address.

Pointer rewiring step THREE

This is where “pointer to B” loses its address. Effectively pointer A and B have exchanged addresses of the values they have been pointing to.

In some situations that might work. In some, it definitely will not. Ditto. It is not a generic solution. One tricky question is when do you free the “value A”. It now belongs to the object that is moved from. That swap can not be used inside moving operations.

Quick check. Swap two integer values (as above). Print their addresses before and after the swap. They stay the same.

Why does that matter? Because we need a generic swap solution. It must not swap anything but values.

So how do we do swap with pointers then?

Simple. We swap the values that pointers point to. We do not touch the pointers.

// synopsis of an  generic swap overload for pointers
template < typename T>
void swap ( T * a, T * b )
{
   T temp = *b ;
   *a = *b ;
   *b = *temp ;
}

Swapping the values as ever before. In reality here is our friend std::move applied to easy the implementation burden imposed by genericity required.

// a bit better synopsis of an  
// generic swap overload for pointers
template < typename T>
void swap ( T * a, T * b )
{
   T temp = std::move( *b );
   *a = std::move( *b ) ;
   *b = std::move( *temp );
}

Conclusion

Modern C++ is built on few of these kind of mechanisms. I do hope by reading this post you understood the intricacies of swapping, moving and value based semantics.

Solving dependency management in Python with Poetry

上一篇

C++ Signals

下一篇

你也可能喜欢

评论已经被关闭。

插入图片

热门栏目

C++ the good parts — std::swap

长按储存图像,分享给朋友