close up photo of programming of codes

codecungnhau.com

Một trang web về kỹ thuật lập trình

Hàm tạo Move và tham chiếu rvalue trong C++11

Trong bài này, chúng ta sẽ thảo luận về việc sử dụng các tham chiếu rvalue trong ngữ cảnh di chuyển các đối tượng của C++11.

Vấn đề về các đối tượng tạm thời

Ý tưởng đằng sau ngữ cảnh di chuyển này là giảm tải các đối tượng tạm thời này trên bộ nhớ. Mỗi khi chúng ta trả về một đối tượng từ một hàm thì một đối tượng tạm thời được tạo ra, đối tượng này sau đó sẽ được sao chép. Cuối cùng, chúng ta tạo ra 2 bản sao của một đối tượng trong khi chúng ta chỉ cần một. Hãy xem xét một ví dụ sau,

class Container {
    int * m_Data;
public:
    Container() {
        //Allocate an array of 20 int on heap
        m_Data = new int[20];
        std::cout << "Constructor: Allocation 20 int" << std::endl;
    }
    ~Container() {
        if (m_Data) {
            delete[] m_Data;
            m_Data = NULL;
        }
    }
    Container(const Container & obj) {
        //Allocate an array of 20 int on heap
        m_Data = new int[20];
        //Copy the data from passed object
        for (int i = 0; i < 20; i++)
            m_Data[i] = obj.m_Data[i];
        std::cout << "Copy Constructor: Allocation 20 int" << std::endl;
    }
};

Giả sử chúng ta có một lớp Container chứa một con trỏ nguyên là biến thành viên,

Khi chúng ta tạo một đối tượng của lớp Container, thì phương thức khởi tạo mặc định của nó sẽ phân bổ nội bộ một mảng 20 số nguyên trên heap và gán cho biến thành viên của nó. Tương tự, hàm xây dựng sao chép của lớp Container phân bổ một mảng gồm 20 số nguyên trên heap, sau đó sao chép nội dung của mảng đối tượng được truyền vào mảng đó rồi gán cho biến thành viên của nó. Nói chung, chúng ta sử dụng các lớp Factory để tạo đối tượng các lớp của chúng ta. Trên các dòng tương tự, hãy tạo một hàm đơn giản để tạo một đối tượng của lớp Container, và trả về đối tượng đó.

// Create an object of Container and return
Container getContainer() 
{
    Container obj;
    return obj;
}

Bây giờ trong hàm main, chúng ta đã tạo một vector kiểu Container và chèn một đối tượng được trả về bởi hàm getContainer (), tức là

int main() {
    // Create a vector of Container Type
    std::vector<Container> vecOfContainers;
    //Add object returned by function into the vector
    vecOfContainers.push_back(getContainer());
    return 0;
}

Cuối cùng ta có một đối tượng trong vecOfContainers. Nhưng chúng ta thực sự đã tạo 2 đối tượng cho nó vì hàm getContainer() trả về một đối tượng tạm thời được sao chép vào một đối tượng mới và sau đó bị hủy. Đối tượng thứ 2 này đã được chèn vào vector. Vì vậy, 2 đối tượng của lớp Container được tạo ở bước sau trong đoạn mã trên,

  • Một đối tượng bên trong hàm getContainer() sử dụng hàm tạo mặc định của lớp Container.
  • Đối thượng thứ hai khi được thêm vào trong vector bằng cách sử dụng hàm tạo sao chép của lớp Container.

Để tạo từng đối tượng trong số 2 đối tượng này, chương trình đã phân bổ một mảng 20 số nguyên trên heap 2 lần và cuối cùng chỉ có một đối tượng được sử dụng. Vì vậy, rõ ràng là một sự lãng phí tài nguyên và công sức.

Làm thế nào để giải quyết vấn đề lãng phí tài nguyên và công sức này do các đối tượng tạm thời? Có cách nào để di chuyển đối tượng thứ nhất thay vì tạo đối tượng thứ hai và sao chép nội dung vào nó không?

Câu trả lời là có. Đó là lúc các tham chiếu rvalue và hàm tạo di chuyển (Move) sinh ra.

Hàm tạo di chuyển (Move)

Hàm getContainer() ở đây là một rvalue, vì vậy nó có thể được tham chiếu bởi một tham chiếu rvalue. Ngoài sử dụng tham chiếu rvalue, chúng ta cũng có thể nạp chồng các hàm. Lần này, chúng ta sẽ nạp chồng cho Hàm tạo của lớp Container và Hàm tạo mới này sẽ được gọi là hàm tạo di chuyển (move constructor).

Hàm tạo di chuyển lấy tham chiếu rvalue làm đối số và nạp chồng vì hàm tạo sao chép lấy tham chiếu const lvalue làm đối số. Trong hàm tạo di chuyển, chúng ta chỉ di chuyển các biến thành viên của đối tượng được truyền vào các biến thành viên của đối tượng mới, thay vì cấp phát bộ nhớ mới cho chúng.

Hãy xem hàm khởi tạo di chuyển cho lớp Container như sau,

Container(Container && obj)
{
    // Just copy the pointer
    m_Data = obj.m_Data;
    // Set the passed object's member to NULL
    obj.m_Data = NULL;
    std::cout<<"Move Constructor"<<std::endl;
}

Trong hàm tạo di chuyển, chúng ta chỉ sao chép con trỏ. Bây giờ biến thành viên m_Data trỏ đến cùng một bộ nhớ trên heap. Sau đó, ta đặt m_Data của đối tượng được truyền thành NULL. Vì vậy, chúng ta đã không phân bổ bất kỳ bộ nhớ nào trên heap trong hàm tạo di chuyển mà chỉ chuyển quyền kiểm soát bộ nhớ.

Bây giờ nếu chúng ta tạo vector lớp Container và đẩy một đối tượng trả về từ getContainer() vào đó. Sau đó, một đối tượng mới sẽ được tạo từ đối tượng tạm thời này nhưng vì getContainer() là một rvalue, vì vậy hàm tạo di chuyển của đối tượng của lớp Container mới này sẽ được gọi và trong bộ nhớ đó sẽ chỉ được dịch chuyển. Vì vậy, thực tế trên heap, chúng ta sẽ chỉ tạo một mảng các số nguyên.

Tương tự như hàm tạo di chuyển (Move Constructor), chúng ta có thể có toán tử gán di chuyển (Move Assignment). Hãy xem ví dụ đầy đủ như sau,

#include <iostream>
#include <vector>
class Container {
    int * m_Data;
public:
    Container() {
        //Allocate an array of 20 int on heap
        m_Data = new int[20];
        std::cout << "Constructor: Allocation 20 int" << std::endl;
    }
    ~Container() {
        if (m_Data) {
            delete[] m_Data;
            m_Data = NULL;
        }
    }
    //Copy Constructor
    Container(const Container & obj) {
        //Allocate an array of 20 int on heap
        m_Data = new int[20];
        //Copy the data from passed object
        for (int i = 0; i < 20; i++)
            m_Data[i] = obj.m_Data[i];
        std::cout << "Copy Constructor: Allocation 20 int" << std::endl;
    }
    //Assignment Operator
    Container & operator=(const Container & obj) {
        if(this != &obj)
        {
            //Allocate an array of 20 int on heap
            m_Data = new int[20];
            //Copy the data from passed object
            for (int i = 0; i < 20; i++)
                m_Data[i] = obj.m_Data[i];
            std::cout << "Assigment Operator: Allocation 20 int" << std::endl;
        }
    }
    // Move Constructor
    Container(Container && obj)
    {
        // Just copy the pointer
        m_Data = obj.m_Data;
        // Set the passed object's member to NULL
        obj.m_Data = NULL;
        std::cout<<"Move Constructor"<<std::endl;
    }
    // Move Assignment Operator
    Container& operator=(Container && obj)
    {
        if(this != &obj)
        {
            // Just copy the pointer
            m_Data = obj.m_Data;
            // Set the passed object's member to NULL
            obj.m_Data = NULL;
            std::cout<<"Move Assignment Operator"<<std::endl;
        }
    }
};
// Create am object of Container and return
Container getContainer()
{
    Container obj;
    return obj;
}
int main() {
    // Create a vector of Container Type
    std::vector<Container> vecOfContainers;
    //Add object returned by function into the vector
    vecOfContainers.push_back(getContainer());
    Container obj;
    obj = getContainer();
    return 0;
}

Kết quả:

Constructor: Allocation 20 int
Move Constructor
Constructor: Allocation 20 int
Constructor: Allocation 20 int
Move Assignment Operator

Trong ví dụ trên, hàm tạo di chuyển của lớp Container sẽ được gọi vì getContainer() trả về rvalue và lớp Container có hàm nạp chồng của hàm tạo chấp nhận rvalue trong tham số. Trong bộ nhớ hàm tạo di chuyển này chỉ được dịch chuyển con trỏ đến đối tượng được truyền. Tương tự, khi ta gán một đối tượng, toán tử gán di chuyển được sử dụng thay vì phép gán thông thường (dòng 71 và 72).


Đã đăng vào

trong

bởi

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *