Search…

Rvalue References và Move Semantics

Hoàng Tiến ĐạtHoàng Tiến Đạt
04/09/202021 min read
Khái niệm rvalue reference, và ứng dụng để định nghĩa "move semantic" làm giảm tối đa chi phí khi thực hiện việc gán và copy các đối tượng.

Giới thiệu

Nếu bạn là một người lập trình rất quan tâm đến vấn đề về performance and overhead trong C++ hẳn bạn sẽ rất quan tâm đến việc làm sao để có thể tối ưu khi thực hiện copy và gán các giá trị.

Vì đây sẽ là một khái niệm khó hiểu đối với những người chưa biết về nó nên bài viết không đưa ra khái niệm Move semantics ngay mà sẽ đi sơ lượt qua các khái niệm liên quan, và là nền tảng cho khái niệm Move semantics. Mục tiêu cuối cùng hướng tới của bài viết là để giúp người đọc có thể nắm vững được khái niệm này, nhờ đó giúp tăng khả năng tối ưu chương trình viết bằng C++ của mình. Một số khái niệm trong bài viết được tác giả giữ nguyên bằng tiếng anh, vì như vậy sẽ dễ hiểu hơn.

Mục tiêu của Move Semantics

Tại sao chúng ta lại cần tới move semantics?

Move Semantics đạt được 4 điều cơ bản:

  • Giúp loại bỏ những chi phí vô ích khi thực hiện copy dữ liệu từ đối tượng tạm.
  • Loại bỏ những chi phí "vô hình" khi hàm trả về một đối tượng.
  • Tối ưu hóa việc copy trong một số trường hợp nhất định, khi nắm rõ vòng đời của đối tượng.
  • Giúp chúng ta thực hiện việc "chuyển quyền sở hữu".

Các khái niệm

Temporary object

Đối tượng tạm là một khái niệm rất thông dụng, nó xuất hiện trong phần lớn các câu lệnh mà chúng ta viết ra. Ví dụ:

int a = 4 + (3 * b);

Ở dòng code trên, chương trình cần sử dụng đến 2 đối tượng tạm, gọi chung cho cả biến tạm và giá trị tạm.

  • Đầu tiên thực hiện tính 3 * b rồi gán vào đối tượng tạm, giả sử gọi nó là temp1.
  • Sau đó lấy giá trị 4 cộng với temp1 rồi lấy kết quả gán vào đối tượng tạm thứ 2, giả sử gọi nó là temp2.

Để rõ hơn ta có thể viết lại cách mà chương trình thực hiện như sau:

int temp1 = 3 * b;
int temp2 = 4 + temp1;
int a = temp2;

Hai đoạn code trên sẽ không khác nhau nhiều về chi phí thực thi. Chỉ khác ở chỗ đoạn code đầu chương trình sử dụng temporary object, còn đoạn code thứ hai tự ta sử dụng named object để chứa giá trị tạm.

Ta thường sẽ gặp các loại đối tượng tạm sau:

  • Giá trị trung gian trong quá trình tính toán của biểu thức. Ví dụ: int a = 4 + (3 * b);
  • Biến trả về từ hàm.
    • Ví dụ: std::vector<int> sortedArray = CreateSortedArray(inputedArray);
  • Biến được khởi tạo nhưng không đặt tên. Trong đoạn code ví dụ sau ta tạo ra đối tượng string để chứa giá trị "stdio.vn""www.stdio.vn" nhưng vì không được đặt tên nên 2 đối tượng này là 2 đối tượng tạm.
    • if (string("stdio.vn") == string("www.stdio.vn"));

Đối với những chương trình nhỏ thì không cần để ý. Nhưng đối với những chương trình lớn, việc tạo ra các đối tượng tạm tốn rất nhiều chi phí. Do đó, từ C++11 đã đưa vào thêm 2 khái niệm mới "rvalue reference" và "move semantics" để giúp giải quyết, tối ưu và giảm thiểu chi phí cho việc copy trạng thái (dữ liệu) của đối tượng tạm.

lvalue, rvalue và lvalue reference

lvalue và rvalue

Đây là một khái niệm ít được nhắc đến nhưng chúng ta phải đối mặt thường xuyên khi viết chương trình. Vậy lvalue và rvalue là gì?

Thật sự khái niệm lvalue và rvalue không có một định nghĩa rõ ràng, nhưng có một thủ thuật để nhận biết đó là hãy đặt giá trị cần kiểm tra ở bên trái dấu = (thực hiện gán giá trị cho nó), nếu nó thực hiện được mà không gây lỗi thì đó là lvalue.

Ngược lại, nếu có lỗi, chẳng hạn như "lvalue required as left operand of assignment" thì đó là rvalue. Vậy có thể hiểu lvalue là giá trị có thể gán được, còn rvalue là giá trị ta không thể gán được hoặc rvalue chỉ có thể nằm bên phải dấu =.

Có thể thực hiện lấy địa chỉ của lvalue sử dụng toán tử &, nhưng không thể lấy địa chỉ của rvalue.

// dream là một lvalue do phép gán bên dưới thành công
string dream = "Toi Se Viet Sach";

// doNothing * beRich là một rvalue vì compiler sẽ báo lỗi dòng này
int doNothing = 1, beRich = 2;
(doNothing * beRich) = 8;

Có 1 cách khác để định nghĩa lvalue và rvalue dễ hiểu hơn, đó là dựa vào vòng đời của giá trị. Vùng nhớ chứa giá trị của rvalue có vòng đời rất ngắn, nó sẽ bị hủy ngay sau khi biểu thức kết thúc. Còn vùng nhớ chứa giá trị của lvalue có vòng đời dài hơn, và nó chỉ bị hủy khi ra khỏi phạm vi định nghĩa nó.

int main()
{
	int b = 2;

	// Giả sử có 2 biến tạm temp = 3 * b và temp2 = 4 + temp được tạo ra
	// Hủy temp và temp2 vì đã kết thúc biểu thức "a = 4 + (3 * b)" chứa nó
	int a = 4 + (3 * b);
	a = 3;
}	// Hủy a, b

Khái quát hơn, ta có:

  • Các lvalue thường là những biến có tên, các rvalue thường là những biến không có tên.
  • Hầu hết các đối tượng tạm là rvalue, và hầu hết các rvalue là đối tượng tạm.
  • Các đối tượng tạm được tạo ra trong quá trình tính toán biểu thức là rvalue.
  • Khi một hàm trả về giá trị của một biến thì giá trị trả về đó là một rvalue (Vì chương trình phải tạo ra một đối tượng tạm để chứa giá trị của biến mà nó trả về).

lvalue references

lvalue reference là những reference thông thường (tham chiếu thông thường) mà ta hay dùng, khi "rvalue references" ra đời và để phân biệt với rvalue references người ta còn gọi nó là "lvalue reference".

lvalue reference có thể dùng để tham chiếu đến một lvalue, nhưng không thể dùng để tham chiếu đến một rvalue.

int a = 2;

// Biên dịch được vì lvalue reference tham chiếu đến lvalue
int &b = a;

// Biên dịch lỗi vì c là lvalue reference, nhưng 2 là một rvalue
int &c = 2;

Trong C++ lại cho phép dùng const lvalue reference để tham chiếu đến rvalue. Khi dùng const lvalue reference tham chiếu đến rvalue thì rvalue đó chỉ bị hủy khi const lvalue reference đó bị hủy, chứ không bị hủy ngay sau khi biểu thức kết thúc nữa.

// Không báo lỗi, vì dùng const lvalue reference để tham chiếu đến rvalue
const int &c = 2;

rvalue references

Có loại tham chiếu nào dùng để tham chiếu đến rvalue không? Trước C++11 thì loại tham chiếu này không tồn tại, nhưng từ C++11 đã có thêm một khái niệm mới "rvalue reference" – reference to rvalue. Kiểu dữ liệu rvalue reference có thể được khai báo bằng cách thêm dấu &&. Ví dụ string&& là một "rvalue reference to string".

&& có thể dễ gây nhầm lẫn đây là một "reference to reference", nhưng đây là kí hiệu của rvalue reference, trong C++ cũng không có khái niệm "reference to reference".

Dù tên gọi của "rvalue reference" có từ "tham chiếu", nhưng nó thường không được dùng vào mục đích để tham chiếu đến một rvalue. Mà mục đích chính của nó là để giúp ta nhận ra đâu là lvalue, và đâu là rvalue. Vì chỉ có rvalue reference mới có thể tham chiếu đến rvalue, nên nếu ta có 2 hàm có cùng tên và giá trị trả về, một hàm nhận đối số là string& và một hàm nhận đối số là string&& thì:

  • Nếu giá trị truyền vào là một lvalue thì hàm có đối số là string& sẽ được gọi.
  • Nếu giá trị truyền vào là một rvalue thì hàm có đối số là string&& sẽ được gọi.

Nói một cách ngắn gọn, rvalue là một loại tham chiếu mới mà nó chỉ có thể được dùng để tham chiếu tới rvalue. Và nhờ đó mà ta có thể định nghĩa được "move semantics" để giảm thiểu tối đa những chi phí copy dữ liệu vô ích (copy dữ liệu từ đối tượng tạm).

Semantics

Copy semantics

Copy semantics là thực hiện copy trạng thái (hay dữ liệu) của đối tượng này sang đối tượng khác, sau đó cả 2 đối tượng này sẽ có trạng thái (hay dữ liệu) như nhau. Đối với lập trình viên C++ khái niệm này khá quen thuộc, khi định nghĩa "copy constructor" hay copy assignment operator (toán tử gán) đã định nghĩa nó theo copy semantics.

Nhưng có một điểm của copy semantics mà ta luôn phải để ý, đó là chỉ thực hiện copy trạng thái (dữ liệu) của đối tượng nguồn sang đối tượng đích, còn trạng thái (dữ liệu) của đối tượng nguồn vẫn y nguyên như cũ. Đôi khi đó không phải là điều mà chúng ta muốn, mong đợi là "dịch chuyển" trạng thái (dữ liệu) của đối tượng nguồn sang đối tượng đích, trạng thái (dữ liệu) của đối tượng nguồn sẽ ở trạng thái default (vì dịch chuyển là ta thực hiện "lấy" chứ không phải copy, cũng giống như chúng ta thực hiện "cut file", sau khi copy xong file đó sang vị trí khác thì file gốc sẽ bị xóa).

Move semantics

Xét đoạn codes sau để tìm hiểu tại sao ta cần đến nó:

std::vector<int> CreateSortedArray(std::vector<int> source)
{
	std::vector<int> des;
	// [CODE] Thêm các phần tử của mảng "source" vào mảng "des"
	// [CODE] Thực hiện sắp xếp mảng "des"
	return des;
}

int main()
{
	std::vector<int> inputtedArray;

	// [CODE] Nhập dữ liệu cho mảng "inputtedArray"
	// Giả sử inputtedArray có 100 triệu phần tử

	std::vector<int> sortedArray = CreateSortedArray(inputtedArray);
}

Đầu tiên ta không thấy chương trình này có vấn đề gì, chỉ cảm thấy hơi tò mò ở mảng inputtedArray có tới 100 triệu phần tử.

Để tìm hiểu xem vấn đề gì sẽ xảy ra thì ta khảo sát:

  • Bước 1 - Chương trình tạo ra một mảng động kiểu int "inputtedArray" (vector<> là kiểu dữ liệu mảng tự co giãn được định nghĩa sẵn trong STL).
  • Bước 2 - Nó sẽ nhảy vào thực hiện đoạn code trong hàm CreateSortedArray.
  • Bước 3 - Nó sẽ tạo ra một đối tượng tạm để bằng cách copy dữ liệu từ mảng des.
  • Bước 4 - Đối tượng tạm sẽ được gán cho đối tượng sortedArray bằng cách copy dữ liệu từ mảng tạm.

Chưa xét đến chi phí thực hiện, ngay trước dòng return des; trong hàm CreateSortedArray đã có được mảng kết quả, sau đó chỉ vì trả về và gán kết quả vào biến sortedArray mà ta phải thực hiện copy mảng đó 2 lần. Dễ dàng thấy được việc này quá vô ích, tại sao không gán con trỏ mảng của đối tượng des (con trỏ bên trong nó, để trỏ tới mảng mà nó quản lý) cho con trỏ mảng của đối tượng sortedArray? Nếu làm được như vậy thì ta sẽ tránh được hoàn toàn việc copy dữ liệu tới 2 lần.

Tại sao không tạo đối tượng des trên heap, sau khi sắp xếp xong thì chỉ việc trả về địa chỉ của nó, như vậy ta cũng có thể hoàn toàn tránh được việc copy dữ liệu 2 lần? Mới nghe thì có vẻ đúng, nhưng khi đó lại có thêm vấn đề mới phát sinh: Client Code hay Server Code sẽ là đối tượng giải phóng tài nguyên đó? Khi một hàm cấp phát một tài nguyên và trả về tài nguyên đó, không có bất kì tiêu chuẩn lập trình nào đề cập việc hàm đó phải chịu trách nhiệm hủy tài nguyên, hay là người gọi hàm đó phải thực hiện việc hủy tài nguyên. Do vậy nó sẽ gây bối rối cho phần Client, nếu Client không phải là đối tượng định nghĩa ra nó.

Mặc khác, bản thân vector<> là một mảng, việc trả về vector<>* không khác nào việc một hàm thực hiện sắp xếp mảng 1 chiều, sau đó trả về con trỏ cấp 2, điều này không tối ưu và nó cũng gây bối rối cho Client.

Do vậy, để tối ưu trong trường hợp này, cần có một cách tiếp cận khác để gán trực tiếp con trỏ mảng bên dưới của đối tượng des sang đối tượng tạm, rồi đối tượng tạm sang đối tượng sortedArray mà không cần phải thực hiện việc tạo mảng mới và copy dữ liệu mảng. Và đó là khi ta cần tới khái niệm "Move semantics".

Move semantics

Vấn đề nảy sinh khái niệm move semantics

  1. Như đã được đề cập ở phần trước, đối tượng tạm được sử dụn thường xuyên trong chương trình để chứa các giá trị tạm thời, phần lớn các đối tượng tạm là do chương trình tạo ra (Rất hiếm khi cần sử dụng tới đối tượng tạm, thay vào đó ta sử dụng các "biến có tên" – named object). Đặc điểm của đối tượng tạm là nó có vòng đời rất ngắn, nó được tạo ra để chứa giá trị tạm, và bị hủy ngay sau đó (ngay sau khi sử dụng xong – sau khi kết thúc biểu thức). Do đó, khi thực hiện copy trạng thái (dữ liệu) của đối tượng tạm (giả sử gọi là đối tượng nguồn) cho một đối tượng khác (giả sử gọi là đối tượng đích), việc cấp phát thêm tài nguyên tương đồng – như của đối tượng tạm – cho đối tượng đích rất lãng phí, dù gì thì đối tượng tạm cũng bị giải phóng ngay sau đó, và tài nguyên của nó cũng mất theo, vậy tại sao đối tượng đích không lấy luôn tài nguyên của đối tượng tạm?
    • Giả sử đối tượng B có 1 mảng 100 phần tử, thực hiện copy dữ liệu của đối tượng B cho A (có thể bằng copy constructor hoặc copy assignment operator), phải cấp phát một mảng 100 phần tử cho A, sau đó gán giá trị của từng phần tử trong mảng của B cho mảng của A. Ngay sau đó thì cả B và mảng 100 phần tử của nó bị hủy.
  2. Giả sử có 2 đối tượng A, B. Ta muốn copy dữ liệu của đối tượng B cho đối tượng A, nhưng tài nguyên của đối tượng B rất nhiều, sau đó ta sẽ không đụng tới đối tượng B nữa, thì điều mà ta muốn thực hiện sẽ là "dịch chuyển" (move) tài nguyên từ B sang A, chứ không phải là "sao chép" (copy) tài nguyên từ B sang A (để giảm chi phí thực hiện). 1 số trường hợp khác, buộc phải thực hiện việc "move" tài nguyên thay vì chỉ "copy" tài nguyên, vì trong trường hợp đó thực hiện việc copy sẽ dẫn đến sai về mặt ngữ nghĩa, logic.
    • Giả sử như đối với smart pointer unique_ptr<> trong C++11. Vì tài nguyên mà unique_ptr nắm giữ chỉ được sở hữu bởi 1 tham chiếu, do đó khi áp dụng ngữ nghĩa copy (copy semantics) đối với unique_ptr sẽ gây ra sự sai sót về mặt ngữ nghĩa, logic. Vì khi thực hiện ngữ nghĩa copy trên unique_ptr đồng nghĩa với việc đối tượng nguồn và đối tượng đích cùng nắm giữ một tài nguyên, như vậy vi phạm nguyên tắc của unique_ptr. Do đó trong trường hợp này buộc phải áp dụng ngữ nghĩa move (move semantics) cho unique_ptr.

Move semantics

Tưởng tượng có 2 nhà A và B. Nhà B có 30 kg dưa hấu, nhưng số dưa hấu đó không bán được, và sẽ hỏng trong nay mai. Trong khi đó, nhà A lại đang rất muốn ăn dưa hấu. Lúc này nhà A sẽ có 2 sự lựa chọn: 1 là ra chợ mua dưa hấu về ăn, 2 là qua nhà B xin dưa hấu đem về ăn. Câu trả lời tránh hao phí nhất là phương án 2.

Tương tự như vậy, trong trường hợp đó, B là một đối tượng tạm, và 30 kg dưa hấu là tài nguyên trên bộ nhớ của nó. Có 2 lựa chọn, 1 là cấp phát 1 lượng tài nguyên tương tự cho A rồi thực hiện gán, 2 là lấy luôn tài nguyên của B. Phương án tối ưu nhất trong tình huống này là phương án 2, nghĩa là khi thực hiện copy dữ liệu từ đối tượng tạm thì trực tiếp chuyển quyền sở hữu tài nguyên đó cho đối tượng đích. Khi đó thì ta đã thực hiện việc copy dựa theo "move semantics".

Copy semantics trong C++.
Copy semantics
Move semantics trong C++.
Move semantics

Do vậy có thể định nghĩa move semantics như sau:

Move semantics nghĩa là ta thực hiện hành động copy bằng cách move tài nguyên (chuyển quyền sở hữu tài nguyên) từ đối tượng nguồn (đối tượng cần copy) sang đối tượng đích (đối tượng copy) thay vì phải tạo mới và copy lại tài nguyên đó.

Nếu ta đang sử dụng phiên bản C++03 thì sẽ gặp một vấn đề, làm cho ta không thể định nghĩa được ‘move semantics’. Đó là trong C++03, chúng ta không có cách nào để nhận biết được đâu là đối tượng lâu dài – lvalue – hay đâu là đối tượng tạm – rvalue. Nhưng may thay C++0x đã đưa ra một khái niệm mới là rvalue reference – như đã đề cập ở trên, giúp chúng ta có thể xác định đối số nào là lvalue (đối tượng lâu dài) và đối số nào là rvalue (đối tượng tạm).

Move semantics chỉ là một giải pháp giúp tối ưu việc copy trong một số trường hợp, nó không thể thay thế được copy semantics. Do đó, thông thường những hàm gán sẽ được định nghĩa cả 2 phiên bản, một phiên bản dựa theo copy semantics và một phiên bản dựa theo move semantics.

Hay nói cách khác - khi ta cần thực hiện gán một đối tượng cho một đối tượng khác, thì ta có 2 phương pháp thực hiện, 1 là thực hiện dựa theo copy semantics, 2 là dựa theo move semantics’. Mỗi phương pháp này không dùng được (hoặc không tối ưu) trong mọi trường hợp, do đó ta cần định nghĩa theo cả 2. Nghĩa là khi ta có một hàm để gán đối tượng tham số cho một đối tượng nào đó, thì ta sẽ phải định nghĩa hàm overload:

  • Hàm đầu tiên sẽ nhận đối số là ‘lvalue reference đến kiểu của tham số Type&, và trong hàm này ta sẽ định nghĩa việc copy theo copy semantics.
  • Hàm thứ hai sẽ nhận đối số là ‘rvalue reference đến kiểu của tham số Type&&, và trong hàm này ta sẽ định nghĩa việc copy theo move semantics.

Các định nghĩa Move semantics

Để dễ dàng nắm được cách định nghĩa move semantics thì ta sẽ so sánh nó với move semantics.

MOVE SEMANTICS COPY SEMANTICS
Tham số đầu vào là một rvalue reference Tham số đầu vào là một lvalue reference
Thực hiện "shallow copy" các trường của đối tượng nguồn qua đối tượng đích. Gán các con trỏ của đối tượng nguồn về nullptr. (Vì theo "move semantics" thì ta thực hiệnchuyển quyền sở hữu tài nguyên của đối tượng nguồn cho đối tượng đích, do đó đối tượng nguồn sẽ không còn sở hữu tài nguyên này nữa). Thực hiện "deep copy" các trường của đối tượng nguồn qua đối tượng đích.

Để dễ hiểu hơn, xét có ví dụ minh họa sau:

class Array
{
private:
	int* _pData;
	int _n;

public:
	Array(int n)
	{
		_pData = new int[n];
		_n = n;
	}

	/* Được gọi khi đối số truyền vào là lvalue
	=> Hàm này sẽ định nghĩa theo "copy semantics" */
	const Array& Assign(const Array& value)
	{
		this->_pData = new int[value._n];
		this->_n = value._n;
		for (int i = 0; i < value._n; i++)
			this->_pData[i] = value._pData[i]; 
		return *this;
	}

	/* Được gọi khi đối số truyền vào là rvalue
	=> Hàm này sẽ định nghĩa theo "move semantics" */
	const Array& Assign(Array&& value)
	{
		this->_pData = value._pData;
		value._pData = nullptr;
		this->_n = value._n; 
		return *this;
	}
};

int main()
{
	Array b(1000000);
	Array a(1);

	/* Vì b là lvalue => gọi hàm Assign(const Array&)
	để thực hiện copy dựa theo "copy semantics" */
	a.Assign(b);

	/* Array(1000000) tạo ra một đối tượng tạm, là một rvalue.
	Do đó hàm Assign(Array&&)
	được gọi để thực hiện copy dựa theo Move semantics */
	a.Assign(Array(1000000));
}

Move constructor và Move assignment

Move semantics thường được áp dụng để định nghĩa move constructor và move assignment operator, giúp loại bỏ những chi phí không cần thiết khi tạo một đối tượng mới dựa trên đối tượng tạm, hoặc gán một đối tượng tạm cho một đối tượng nào đó.

Move constructor chỉ khác copy constructor ở chổ đối số đầu vào của nó là rvalue reference (Data_type&&) thay vì lvalue reference (Data_type&), và bên trong nó sẽ định nghĩa việc copy dựa theo "move semantics", thay vì "copy semantics" như của copy constructor. Cũng tương tự như giữa move assignment operator và copy assignment operator.

Move constructor và move assignment operator sẽ chỉ được gọi khi đối số truyền vào là một rvalue, do đó, nó chỉ dùng được khi ta thực hiện việc copy từ đối tượng tạm. Vì vậy nên ta cũng cần phải định nghĩa thêm copy constructor và copy assignment operator để dùng trong trường hợp đối số truyền vào là một lvalue.

Để rõ hơn ta có ví dụ sau:

class Array
{
private:
	int* _pData;
	int _n;

public:
	Array(int n)
	{
		_pData = new int[n];
		_n = n;
	}

	/* Copy constructor, đối số là 1 lvalue reference
	=> Nó chỉ được gọi khi đối số truyền vào là lvalue
	(Xét trong TH move constructor được định nghĩa) */
	Array(const Array& value)
	{
		/* Định nghĩa việc copy dữ liệu từ đối tượng value
		dựa theo "copy semantics" */
		this->_n = value._n;
		this->_pData = new int[this->_n];
		for (int i = 0; i < this->_n; i++)
		{
			this->_pData[i] = value._pData[i];
		}
	}

	/* Move constructor, đối số là một rvalue reference
	=> Nó được gọi KHI VÀ CHỈ KHI đối số truyền vào là rvalue
	(temporary object) */
	Array(Array&& value)
	{
		/* Định nghĩa việc copy dữ liệu từ đối tượng value
		dựa vào "move semantics" */
		this->_n = value._n;
		this->_pData = value._pData;
		value._pData = nullptr;
	}
};

int main()
{
	Array b(1000000);

	// Vì b là một lvalue, do đó copy constructor sẽ được gọi
	Array a(b);

	// Vì Array(1000000) là một rvalue, do đó move constructor sẽ được gọi
	Array a(Array(1000000));
}

std::move

Đôi khi có những trường hợp, chúng ta nắm rõ được vòng đời của các đối tượng, các đối tượng nguồn không còn được sử dụng sau khi copy. Lúc này ta muốn thực hiện move semantics thay vì copy semantics để giảm chi phí.

Đó là lúc ta cần sử dụng đến std::move(), hàm std::move() sẽ trả về một rvalue reference đến đối tượng, do đó khi được truyền vào hàm, nếu có 1 nhận vào đối số là rvalue reference thì hàm này sẽ được gọi để thực hiện việc move tài nguyên,  thay cho copy tài nguyên.

Về cơ bản hàm std::move() hầu như chỉ thực hiện việc cast đối tượng của sang một rvalue reference, do đó, nếu muốn thủ công cũng có thể thực hiện được bằng cách sử dụng static_cast: static_cast<Type&&>(object).

Có một số trường hợp đặc biệt buộc phải sử dụng std::move, như đối với unique_ptr, unique_ptr không có copy constructor và copy assignment, vì việc gán một con trỏ cho một con trỏ khác đồng nghĩa việc hai con trỏ cùng trỏ tới một tài nguyên, vi phạm nguyên tắc của unique_ptr. Do đó ta chỉ thực hiện việc copy unique_ptr dựa theo move semantics (bằng cách sử dụng std::move để cast đối tượng nguồn cần copy thành rvalue reference rồi thực hiện gán).

#include <memory>
using namespace std;

int main()
{
	unique_ptr<int> p1(new int[1000000]);

	/* Bị lỗi, vì unique_ptr không có copy constructor và đoạn code này
	cũng gây ra sai lệch về mặt ngữ nghĩa của unique_ptr (nếu thực hiện
	được thì hai smart pointer p1 và p2 sẽ cùng trở tới một tài nguyên) */
	unique_ptr<int> p2(p1);

	/* Không lỗi, lúc này ta cố ý muốn chuyển tài nguyên mà p1 sở hữu
	cho p3, và sau đó thì p3 sẽ trỏ tới tài nguyên trước đó của p1, còn
	p1 trỏ tới null */
	unique_ptr<int> p3(std::move(p1));
}
IO Stream

IO Stream Co., Ltd

developer@iostream.co
383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024