منظور از move-semantics در c++11 چیه ؟ - هفت خط کد انجمن پرسش و پاسخ برنامه نویسی

منظور از move-semantics در c++11 چیه ؟

+9 امتیاز

enter image description here

چرا به move sematnic نیاز داریم ؟
چه زمانی استفاده از move بهتر از کپی کردنه ؟
rvalue refrence چیه ؟
چه وقت استفاده از move خطری نداره ؟
نحوه نوشتن move برای کلاس
منظور از perfect forwarding چیه ؟
توضیحاتی در مورد universal refrence , نحوه پیاده سازی move

سوال شده آذر 1, 1392  بوسیله ی toopak (امتیاز 2,458)   16 47 66
ویرایش شده بهمن 16, 1393 بوسیله ی BlueBlade

4 پاسخ

+8 امتیاز
 
بهترین پاسخ

پست شماره ۱   move چیست , چرا به move نیاز داریم

 

چرا به move sematnic نیاز داریم ؟

شما فرض کنید یک کلاس دارین مثل vector که یک بلاک حافظه برای شما می گیره :

typedef std::vector<T> TVec;
TVec createTVec(); // factory function
TVec vt;
…
vt = createTVec(); // copy return value object to vt,
// then destroy return value object

الان داخل کد بالا خط 5 ام این اتفاقات میفته :

1_  createTVec صدا زده میشه و  یک شی از TVec ساخته میشه

2_ operator= صدا زده میشه و  مقادیر بازگشتی از createTVec کپی میشن در vt (یعنی ما در یک لحظه 2 تا کپی از وکتور داریم)  

3_ نهایتا وکتور ساخته شده در مرحله اول پاک میشه

 

یعنی  در یک زمان مشخص ما 2 تا کپی از اطلاعات داریم  که این کپی شدن نه از نظر مصرف حافظه بهینست نه از نظر زمان مصرفی .

به نظرتون بهتر نبود که به جای کپی شدن فقط محلی که vt بهش اشاره می کنه رو عوض کنیم ؟ ( که اصطلاحا به این کار میگیم move )

حالا اگر به هر روشی بتونیم این کار رو انجام بدیم هم  سرعت اجرای برنامه بیشتر میشد هم مصرف حافظه .

 

خب حالا یک مثال دیگه رو در نظر می گیریم :

فرض کنیم یک وکتور دیگه داریم به این شکل که

شامل  یک سری عضو هست که هر کدوم به یک محل دیگه از حافظه اشاره می کنن (T State)

 

بعد وقتی که از push_back استفاده کنیم چه اتفاقی میفته ؟

یک بلاک بزرگتر حافظه گرفته میشه و تک تک عناصر کپی میشن باز هم این جا ما 2 تا کپی داریم

std::vector<T> vt;
...
vt.push_back(T object); 

 

 

حالا بهتر نبود که عملیات این جوری انجام میشد ؟

 

یعنی فقط محلی که اشاره گر ها بهش اشاره می کنن رو عوض کنیم .

 

چیزایی که گفتم برای  بقیه عملیات ها مثل insert erase و ... هم برقراره . به همین دلیله که  listوvector  و....توی c++11 سرعتشون از قبل بهتر شده .

 

یک مثال دیگه رو هم در نظر می گیریم  برای swap کردن 2 تا مقدار :

template<typename T> // straightforward std::swap impl.
void swap(T& a, T& b)
{
T tmp(a); // copy a to tmp (⇒ 2 copies of a)
a = b; // copy b to a (⇒ 2 copies of b)
b = tmp; // copy tmp to b (⇒ 2 copies of tmp)
} // destroy tmp

 

این جا چه اتفاقی میفته ؟

مرجله اول یک کپی از a گرفته میشه و داخل b گذاشته میشه یعنی 2 تا کپی از a داریم

مرحله دوم یک کپی از b گرفته میشه و داخل a میزاریم یعنی 2 تا کپی از b داریم .

مرحله آخر هم یک کپی از  temp گرفته میشه و توی b میزاریم یعنی 2 تا کپی از temp داریم !

 

حالا بهتر نبود تمام این کپی های غیر ضروری رو انجام ندیم و به این شکل عمل کنیم ؟

اطلاعات داخل a رو move کنیم به temp

اطلاعات داخل b رو move کنیم به a

اطلاعات داخل temp رو move کنیم به b

یعنی به جای کپی کردن فقط محلی که اشاره گر ها بهش اشاره می کنن رو عوض کنیم ؟

 

توی c++11 این کار شدنیه :

template<typename T> // straightforward std::swap impl.
void swap(T& a, T& b)
{
T tmp(std::move(a)); // move a’s data to tmp
a = std::move(b); // move b’s data to a
b = std::move(tmp); // move tmp’s data to b
}


چه زمانی استفاده از move بهتر از کپی کردنه ؟

1_ همون جوری که توی مثال های بالا بهش اشاره کردم زمانی move سریع تره که بتونیم اشاره گر ها رو عوض کنیم .(یهنی عناصر توی 2 قسمت جدا از حافظه قرار گرفته باشن )

یعنی فرضا اگر داخل مثال دومی که زدم T State هم داخل یک بلاک حافظه بود این جا کپی کردن یا move کردن فرقی نداشت .

2_ زمان های که قراره  deep copyانجام بشه استفاده از move خیلی بهتر جواب میده .

نکته ای هم که هست اینه که move هیچ وقت از copy کند تر نیست و اغلب وقت ها هم سریع تره پس بهتره همیشه استفاده بشه .

 

نمودار زیر مقایسه move و copy برای = و push_back برای 10000 تا عنصره

vt = createTVec(); // return/assignment
vt.push_back(T object); // push_back

 

همون طوری که مشخصه move خیلی وقت ها به مراتب سریع تر از copy هستش .

پاسخ داده شده اسفند 10, 1392 بوسیله ی BlueBlade (امتیاز 15,315)   15 18 89
ویرایش شده اسفند 11, 1392 بوسیله ی BlueBlade
+7 امتیاز

پست شماره ۲  محتویات :  rvalue refrence چیه ؟  چه وقت استفاده از move خطری نداره ؟ نحوه نوشتن move برای کلاس

خب حالا 2 تا سوال پیش میاد :

1_ چجوری بفهمیم کی باید از move به جای کپی استفاده کنیم

2_ چطوری اطلااعات رو move کنیم

برای جواب دادن به سوال اول لازمش دونستن اطلاعاتی درباره rvalue و lvalue هستش .

قبل از خوندن ادامه متن اول این تاپیک رو ببینید : معنی rvalue و lvalue چیه ؟

استفاده از move زمانی که مقادیر lvalue هستن درست نیست مثلا کد زیر رو در نظر بگیرید:

TVec vt1;
…
TVec vt2(vt1);

این جا شما انتظار دارین بعد از تعریف vt2 باز هم بتونین از vt1 استفاده کنین .

ولی حالا این کد :

TVec vt2 { createTVec() };

این جا بعد از اجرای کد شما دیگه به مقدار createTVec دسترسی ندارین . پس می تونین بدون این که خطری داشته باشه از move استفاده کنین .

یک مثال ساده تر رو در نظر بگیرید :

func(string a);


string a;

func(a);
func("qwer");

توی کد بالا هم به همین شکل توی حالت اول ما نمی تونیم a رو تغییر بدیم چون بعد از صدا زدن تابع ممکنه به a نیاز داشته باشیم ولی توی حالت دوم چون دیگه به "qwer " دسترسی نداریم میشه از move استفاده کرد .

یعنی برای استفاده از move نیاز به راهی داریم که بتونیم مقادیر rvalue  رو تشخیص بدیم تا بتونیم بدون خطر از move استفاده کنیم .

یک سری مثال :

TVec vt1;
vt1 = createTVec(); // rvalue source: move okay
auto vt2 { createTVec() }; // rvalue source: move okay
vt1 = vt2; // lvalue source: copy needed
auto vt3(vt2); // lvalue source: copy needed
f("Hello"); // rvalue (temp) source: move okay
std::string s("C++0x");//move okay
f(s); // lvalue source: copy needed

توی c++ برای فرستادن  حافظه مقادیر temp به توابع باید از const استفاده کرد :

void func(int&)
{}
int a=2;
func(a);//ok
func(5);//error

void func(const int&)
{}
int a=2;
func(a);//ok
func(5);//ok

ولی move کاری که می کنه اینه که میاد کل مقادیر داخل متغیر رو جابه جا می کنه یعنی اگر از const استفاده کنیم نمی تونیم از move استفاده کنیم . بدون const هم که برنامه ارور میده .

توی c++11 برای حل این مشکل سینتکس جدید rvalue refrence برای این کار اضافه شده که همزمان هر 2 تا مشکل بالا رو حل می کنه هم اجازه میده که rvalue رو عوض کنیم  و هم میشه تشخیص داد چه موقع از rvalue استفاده شده .

نحوه تعریف rvalue با && هست مثال :

void func(string && a)

با استفاده از این تعریف جدید به راحتی کامپایلر تشخیص میده که چه وقت از rvalue استفاده شده .

void f1(const TVec&); // const lvalue ref
TVec vt;
f1(vt); // ok
f1(createTVec()); // ok

void f2(const TVec&); // #1: const lvalue ref
void f2(TVec&&); // #2: non-const rvalue ref
f2(vt); // lvalue ⇒ #1
f2(createTVec()); // non-const rvalue ⇒ #2

void f3(const TVec&&); // #1: const rvalue ref
void f3(TVec&&); // #2: non-const rvalue ref
f3(vt); // lvalue error! 
f3(createTVec()); // both viable, non-const rvalue ⇒ #2

نحوه پیاده سازی :

با یک مثال شروع می کنیم

فرض کنید این کلاس رو داریم :

class Widget {
public:
Widget(const Widget&); // copy constructor
Widget(Widget&&); // move constuctor
Widget& operator=(const Widget&); // copy assignment op
Widget& operator=(Widget&&); // move assignment op
…
};

حالا در صورت استفاده از کلاس ممکنه حالت های زیر پیش بیاد :

Widget createWidget(); // factory function
Widget w1;
Widget w2 = w1; // lvalue src ⇒ copy constructor
w2 = createWidget(); // rvalue src ⇒ move assignment
w1 = w2; // lvalue src ⇒ copy assignment
Widget w2 = createWidget();// rvalue src ⇒ move constructor

یعنی برای این که از move استفاده کنین فقط کافیه که move constructor , move assignement رو تعریف کنین بصورت خودکار کامپایلر تشخیص میده که از کدوم استفاده کنه .

 برای استفاده از move باید به این شکل عمل کنین :

class Widget {
public:
    Widget(Widget&& rhs)
        : pds(std::move(rhs.pds) ) // take source’s value
    {
          rhs.pds = nullptr; 
     } // leave source in valid state

private:
    struct DataStructure;
    DataStructure *pds;
};

توی کد بالا اول مقادیر داخل rhs ریخته میشن داخل کلاس فعلی بعد هم rhs.pds برابر با null گذاشته میشه .( دلیلی هم که باید برابر null بزاریم اینه که اگر null تزاریم و کلاس destructor داشته باشه احتمال این که rhs.pds پاک بشه هم وجود داره و این که راه استاندارد برای پیاده سازی به همین شکل هست  . )

نکته دوم هم این خطه : pds(std::move(rhs.pds) ) این جا چون rhd.pds اسم داره و همون جوری هم که قبلا گفتم متغیری که اسم داشته باشه lvalue حساب میشه از std::move استفاده می کنیم که lvalue رو rvalue تبدیل کنیم ( اگر این جوری pds(rhs.pds مینوشتیم  به جای move کپی انجام می شد  به همین دلیل کامپایلر ارور میده .)

خب حالا فرض کنیم یک کلاس widget داریم که از wdigetBase ارث برده این جا هم به همین شکل باید عمل کرد :

class WidgetBase { 
   WidgetBase(const WidgetBase&); // copy ctor
   WidgetBase(WidgetBase&&); // move ctor
};
class Widget: public WidgetBase {
public:
    Widget(Widget&& rhs) // move constructor
        : WidgetBase(std::move(rhs)), // request move
          s(std::move(rhs.s)) // request move
    { 
        //...
    }
    Widget& operator=(Widget&& rhs) // move assignment
    {
        WidgetBase::operator=(std::move(rhs)); // request move
        s = std::move(rhs.s); // request move
        return *this;
    }
   // …
};

 

پاسخ داده شده اسفند 10, 1392 بوسیله ی BlueBlade (امتیاز 15,315)   15 18 89
ویرایش شده اسفند 11, 1392 بوسیله ی BlueBlade
+6 امتیاز

پست شماره  ۳   منظور از perfect forwarding چیه ؟

دقت داشته باشین که move semantic فقط مخصوص copy constructor , assignment operator نیست برای توابع هم میشه ازش استفاده کرد .

مثلا :

class Widget {
public:
    ...
    void setName(const std::string& newName)
    { name = newName; } // copy param
    void setName(std::string&& newName)
    { name = std::move(newName); } // move param
    void setCoords(const std::vector<int>& newCoords)
    { coordinates = newCoords; } // copy param
    void setCoords(std::vector<int>&& newCoords)
    { coordinates = std::move(newCoords); } // move param
    ...
private:
    std::string name;
    std::vector<int> coordinates;
};

همون طوری که توی کد بالا مشخصه ما برای استفاده از move باید هر تابع رو ۲ بار بنویسیم که خب هم حجم کد میره بالا هم خوانایی کم میشه .

به جای این کار ما می تونیم از روشی استفاده کنیم که اصطلاحا بهش prefect forwarding میگن یعنی یک تابع بنویسیم که با توجه به ورودیش move یا کپی رو انجام بده .

کد بالا رو با روش جدید میشه به این شکل نوشت :

class Widget { // revised
public: // example
    ...
    template<typename T>
    void setName(T&& newName) // forward
    { name = std::forward<T>(newName); } // newName
    template<typename T>
    void setCoords(T&& newCoords) // forward
    { coordinates = std::forward<T>(newCoords); } // newCoords
    ...
private:
    std::string name;
    std::vector<int> coordinates;
};

کاری که std::forward می کنه اینه که تشخصی میده پارامتر ورودیش نوعش چیه اگر lvalue باشه مقدار رو کپی می کنه داخل name - اگر rvalue باشه از move استفاده می کنه  . یعنی دیگه لازم نیست کد رو دوبار بنویسیم . و دقت داشته باشین که توی تعریف توابع ورودی ها حتما باید  از نوع && یا rvalue refrence باشن تا کد بالا درست کار کنه .

برای constructor تابع هم به همین شکل میتونیم عمل کنیم :

class Widget {
public:
    template<typename T1, typename T2>
    Widget(T1&& n, T2&& c)
        : name(std::forward<T1>(n)), // forward n to string ctor
          coordinates(std::forward<T2>(c)) // forward c to vector ctor
    {}
    ...
private:
    std::string name;
    std::vector<int> coordinates;
};

 

 

پاسخ داده شده اسفند 11, 1392 بوسیله ی BlueBlade (امتیاز 15,315)   15 18 89
ویرایش شده فروردین 11, 1393 بوسیله ی BlueBlade
+5 امتیاز

پست شماره ۴

توضیحاتی در مورد universal refrence

فرض کنید این تابع رو داریم :

template<typename T>void f(T&& param)		
{
   //...
}

شاید چیزی که توی اولین برخورد با این کد به ذهنتون برسه این باشه که T از نوع rvalue refrence هست ولی این طور نیست بسته به نوع مقدار ورود T میتونه lvalue باشه یا rvalue که در ادامه دلیلشو توضیح میدم . به T توی کد بالا universal refrence هم می گن .

حالتی رو در نظر بگیرید که تابع رو به این شکل صدا زدید :

Widget w;
f(w);	

این جا w از نوع lvalue هستش و به همین دلیل بصورت  &lvalue به تابع فرستاده میشه پس T این جا میشه  &Widget یعنی در مجموع && T میشه && &Widget

ولی مشکل اینه که توی c++ ما نمی تونیم & به refrence داشته باشیم یعنی اگر قرار باشه کد بالا همین شکل بمونه کامپایلر ارور میده .

توی c++11 برای حل این مشکل یک سری قواعد جدید تعریف شده به اسم ref_collapsing rule که به این شکل عمل می کنن :

T& & 	⇒ T&
T&& &	⇒ T&
T& && 	⇒ T&
T&& &&	⇒ T&&

یعنی کامپایلر refrence به refrence ها رو بصورت خودکار بر اساس قواعد بالا تبدیل می کنه .

خب حالا همون مثال قبل رو باز در نظر بگیرین :

Widget w;
f(w);

همون طوری که گفتم w به شکل&Widget فرستاده میشه به f و f به این شکل در میاد :‌

Widget& &&

این جا بر اساس قواعد بالا پارامتر ورودی تبدلی به این میشه :

Widget&

یعنی اگر شما الان به f  پارامتر از نوع lvalue بفرستین T هم بصورت &lvalue در میاد .

این چیزی که گفتم ۴ جا ممکنه اتفاق بیفته :

۱ـ استفاده از func template ( مثال بالا)

۲ـاستفاده از auto

3_ استفاده از decltype

4_استفاده از typedef

 

برای typedef مثال زیر رو در نظر بگیرید :

Widget w;	
typedef Widget&& RRtoW;	
RRtoW& v1 = w;	// v1’s type is Widget&
const RRtoW& v2 = std::move(w);	// v2’s type is const Widget&
RRtoW&& v3 = std::move(w);	// v3’s type is Widget&&

که همون طوری که مشخصه type ها بر اساس همون قواعد بالا شکل گرفتن.

درک نکردن Ref_collapsing rule بعضی جا ها باعث اشتباهاتی هم بشه :

Widget  w;
decltype(w)&& r1 = std::move(w);	// r1’s type is Widget&&
decltype((w))&& r2 = std::move(w);	// r2’s type is Widget&

توی کد بالا گراشتن یک پرانتز بیشتر باعث شده که نوع داده ما از rvalue به lvalue refrence تغییر کنه . دلیل هم اینه که وقتی () رو میزارین کامپایلر اون قسمت رو به شکل یک عبارت می ببینه (w) رو به این شکل تفسیر می کنه و مثل همون زمانی که به تابع میفرستین (w) رو بصورت &Widget در نظر می گیره و در مجوع عبارت میشه

 && &Widget  که تبدیل میشه به &Widget و احتمالا اون چیزی نیست که ما می خواستیم .

خب حالا میرسیم به این که std::move چه شکلی کار میکنه . این کد رو در نظر بگیرید :

template<typename T>
typename std::remove_reference<T>::type&& 
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

همون طوری که مشخصه این جا ورودی تابع move از نوع universal refrence هست که اگر rvlue بهش بدین T هم به هم rvalue تبدیل میشه . اگر هم lvalue بدین بر اساس قواعد ref_collapsing به lvalue تبدیل میشه .

و مقدار بازگشتی هم توسط  std::remove_reference به نوع مورد نظر تبدیل میشه.

std::forward هم از قواعد مشابه برای پیاده سازیش استفاده شده .

پاسخ داده شده اسفند 13, 1392 بوسیله ی BlueBlade (امتیاز 15,315)   15 18 89
ویرایش شده فروردین 11, 1393 بوسیله ی BlueBlade
...