Vietnamese Developers’ Blog

Lớp auto_ptr trong C++

Posted in C/C++ by kiennguyen on November 12, 2007

Nguồn: Tổng hợp từ Exceptional C++ của Herb Sutter và The C++ Standard Library của Nicolai M. Josuttis

auto_ptr là gì?

Các hàm trong các ngôn ngữ lập trình thường hoạt động theo quy trình sau đây

1- Cấp phát tài nguyên
2- Thực hiện các xử lí
3- Giải phóng tài nguyên

Nếu tài nguyên được cấp phát thông qua các đối tượng cục bộ, chúng sẽ được tự động giải phóng khi kết thúc hàm. Ngược lại, khi tài nguyên được cấp phát một cách tường minh và không gắn với một đối tượng nào, chúng phải được giải phóng một cách tường minh. Tài nguyên thường được cấp phát và giải phóng một cách tường minh thông qua các con trỏ. Cách sử dụng con trỏ phổ biến trong C++ là sử dụng toán tử new và delete như sau:

//Ví dụ 1(a): Đoạn mã không sử dụng auto_ptr
//
void f()
{
    T* pt( new T ); //Cấp phát tài nguyên một cách tường minh
    /*... các xử lí ...*/
    delete pt; //Giải phóng vùng nhớ pt trỏ tới một cách tường minh
} //Kết thúc hàm, biến cục bộ pt được hủy một cách tự động

Hàm f() mang trong mình một lỗi tiềm ẩn: Người lập trình có thể quên không viết câu lệnh delete. Kể cả trong trường hợp có lệnh delete, nếu có một lệnh return được viết trước đó hoặc xảy ra một exception thì hàm f sẽ thoát ngay lập tức mà không thực hiện lệnh delete. Các trường hợp này nếu xảy ra đều dẫn đến lỗi memory leak. Giải pháp thông thường là chúng phải bắt tất cả các exception có thể xảy ra.

void f()
{
    T* pt( new T ); //Cấp phát tài nguyên một cách tường minh
    try{
        /* ...Các xử lí, có thể xảy ra exception...*/
    } catch( ... ) { //Với mọi exception xảy ra:
        delete pt; //-giải phóng vùng nhớ pt trỏ tới
        throw; //-ném ra exception
    }

    delete pt; //Không xảy ra exception, giải phóng vùng nhớ pt trỏ tới

} //Kết thúc hàm, biến cục bộ pt được hủy một cách tự động

Giải pháp này sẽ trở nên phức tạp khi có nhiều tài nguyên được cấp phát một cách tường minh. Chúng ta cần một con trỏ “thông minh” (smart pointer) có khả năng tự giải phóng vùng nhớ mà nó đang trỏ đến bất cứ khi nào bản thân con trỏ đó bị hủy. Con trỏ cũng là một biến cục bộ nên nó sẽ bị hủy một cách tự động khi hàm thoát ra bất kể theo cách bình thường hay bất thường. Bởi vậy, khi kết thúc hàm hoặc ra khỏi phạm vi (scope) của con trỏ thông minh, vùng nhớ được cấp phát động trước đó sẽ tự động được giải phóng mà không cần đến câu lệnh delete nữa. Lớp auto_ptr ra đời nhằm đáp ứng nhu cầu này.

Một auto_ptr đóng vai trò như chủ sở hữu (owner) của một đối tượng được cấp phát động. Bất cứ khi nào một auto_ptr bị hủy, đối tượng mà nó đang sở hữu cũng bị hủy theo. Mỗi đối tượng chỉ được sở hữu bởi duy nhất bởi một auto_ptr. Đoạn mã trở nên đơn giản hơn nhiều nhờ sử dụng auto_ptr như sau:

//Ví dụ 1(b) Đoạn mã an toàn nhờ dùng auto_ptr
//

#include< memory > //Header file cho kiểu auto_ptr

void f()
{
    std::auto_ptr< T > pt( new T );

    /*...các xử lí khác...*/

} //Hết phạm vi của pt, destructor của pt được gọi. Vùng nhớ được cấp phát bởi new T
  //được tự động giải phóng theo

Đoạn mã trên không thể gây ra memory leak ở vùng nhớ cấp phát bởi lệnh new T dù hàm f kết thúc bình thường hay bất thường.

Sử dụng auto_ptr cũng dễ dàng như sử dụng con trỏ thông thường nhờ các hàm thành phần mà lớp auto_ptr cũng cấp. Toán tử * dùng để truy nhập đối tượng đang được trỏ tới, toán tử -> dùng để truy nhập các thành phần của đối tượng đang được trỏ tới nếu nó là một cấu trúc hay một lớp. Tuy nhiên, kiểu auto_ptr không định nghĩa các phép toán số học (như ++ chẳng hạn), đây có thể là một điều tốt vì các phép toán số học trên con trỏ thường gây ra nhiều phiền toái.

Chú ý rằng lớp auto_ptr không cho phép khởi tạo đối tượng từ một con trỏ thông thường nhờ phép gán:

std::auto_ptr< ClassA > ptr1( new ClassA ); //Khởi tạo đúng
std::auto_ptr< ClassA > ptr2 = new ClassA; //Khởi tạo SAI

Ví dụ sau đây minh họa cách sử dụng các toán tử *, -> và các hàm thành phần get(), release() của lớp auto_ptr

//Ví dụ 2: Sử dụng các hàm thành phần của lớp auto_ptr
//
void g()
{

    T* pt1 = new T; //Cấp phát một vùng nhớ thông qua con trỏ pt1
    std::auto_ptr< T > pt2( pt1 ); //pt2 sở hữu vùng nhớ được cấp phát

    *pt2 = 12; //Tương đương “*pt1 = 12;”
    pt2->someFunc(); //Tương đương “pt1->someFunc();”

    //Dùng hàm get() để kiểm tra đối tượng pt2 đang sở hữu
    assert( pt1 == pt2.get() );

    //Dùng hàm release() lấy đi quyền sở hữu của pt2
    T* pt3 = pt2.release();

    //pt2 không còn sở hữu vùng nhớ được cấp phát động nữa
    //Phải giải phóng vùng nhớ bằng lệnh delete
    delete pt3;

} //Kết thúc hàm, biến cục bộ ptr bị hủy. pt2 không sở hữu vùng nhớ nào cả
  //nên không xảy ra hủy một vùng nhớ hai lần

Hàm thành phần reset() dùng để xóa bỏ quyền sở hữu hiện thời của một auto_ptr để auto_ptr này chuyển sang sở hữu một đối tượng mới. Xem đoạn mã sau đây

//Ví dụ 3: Sử dụng hàm thành phần reset
//
void h()
{
    std::auto_ptr< T > pt( new T( 1 ) );

    pt.reset( new T( 2 ) ); //Hủy đối tượng được cấp phát bởi “new T( 1 );”
    //pt chuyển sang sở hữu đối tượng cấp phát bởi “new T( 2 );”

} //Kết thúc hàm, pt2 và đối tượng cấp phát bởi new T( 2 ) bị hủy

Các thành phần dữ liệu ( data member ) kiểu auto_ptr

Đoạn mã sau đây sử dụng Pimpl với các thành phần dữ liệu là con trỏ thông thường

//Ví dụ 4( a ): Một Pimpl phổ biến
//

//file c.h
//
class C
{
  public:
    C();
    ~C();
    /*...*/

  private:
    class CImpl; //forward declaration
    CImpl* pimpl_;
};

//file c.cpp
//
class C::CImpl { /*...*/ };

C::C() : pimpl_( new CImpl ) { }
C::~C() { delete pimpl_; }

Constructor của lớp C làm nhiệm vụ khởi tạo đối tượng “Pimpl” chứa các xử lí bên trong, destructor của lớp C giải phóng vùng nhớ cấp phát cho “Pimpl” một cách tường minh bằng câu lệnh delete. Đoạn mã sẽ trở nên đơn giản hơn nhờ sử dụng auto_ptr

//Ví dụ 4( b ): Pimpl an toàn hơn nhờ auto_ptr
//

//file c.h
//
class C
{
  public:
    C();
    /*...*/

  private:
    class CImpl; //forward declaration
    std::auto_ptr< CImpl > pimpl_;
};

//file c.cpp
//
class C::CImpl { /*…*/ };
class C::C() : pimpl_( new CImpl ) { }

Chúng ta không cần phải viết destructor cho lớp C nữa bới auto_ptr sẽ tự động giải phóng vùng nhớ cấp phát bởi lệnh new.

Các vấn đề về quyền sở hữu (ownership)

Chuyển quyền sở hữu khi sao chép hay gán các auto_ptr

Một đối tượng chỉ được sở hữu bởi duy nhất một auto_ptr tại một thời điểm. Khi sao chép một auto_ptr, quyền sở hữu được chuyển từ auto_ptr nguồn sang auto_ptr đích. Nếu auto_ptr đích đang sở hữu một đối tượng khác thì đối tượng đó sẽ bị giải phóng. auto_ptr đích chuyển sang sở hữu đối tượng của auto_ptr nguồn còn auto_ptr nguồn sẽ không sở hữu cái gì cả và do đó không thể dùng toán tử * để truy nhập đối tượng mà nó đang sở hữu nữa. Xem ví dụ sau đây

//Ví dụ 5: Chuyển quyền sở hữu giữa các auto_ptr
//
void f()
{
    std::auto_ptr< T > pt1( new T );
    std::auto_ptr< T > pt2;

    pt1->doSomething(); //OK

    pt2 = pt1; //pt2 sở hữu con trỏ tạo ra bởi “new T;”
    //pt1 không sở hữu gì cả

    pt2->doSomething(); //OK
} //Kết thúc hàm, biến pt2 và con trỏ bị hủy.

Sử dụng một auto_ptr không sở hữu gì cả sẽ gây lỗi

//Ví dụ 6: Lỗi khi làm việc với auto_ptr không sở hữu đối tượng nào
//
void f()
{
    std::auto_ptr< T > pt1( new T );
    std::auto_ptr< T > pt2;

    pt2 = pt1; //pt2 sở hữu con trỏ T, pt1 không sở hữu gì cả

    pt1->doSomething(); //ERROR!!!
}

Chuyển quyền sở hữu thông qua hàm

Một hàm có thể đóng vai trò nguồn cung cấp quyền sở hữu (source) hay đích nhận quyền sở hữu (sink). Hàm đóng vai trò source khi nó tạo ra một tài nguyên và chuyển quyền sở hữu tài nguyên đó cho một bên nhận. Các hàm thường đóng vai trò source khi chúng trả về một auto_ptr bằng lệnh return như sau

//Một hàm tạo ra một tài nguyên và chuyển đi quyền sở hữu tài nguyên đó
//
std::auto_ptr< T > source()
{
    return std::auto_ptr< T >( new T );
}

Hàm đóng vai trò sink khi nó nhận về quyền sở hữu một đối tượng sẵn có (thường là thực hiện các xử lí trên đối tượng này và sau đó giải phóng nó). Các hàm đóng vai trò sink khi chúng có một tham số kiểu auto_ptr

//Một hàm nhận quyền sở hữu đối tượng và giải phóng đối tượng đó
//
void sink( std::auto_ptr< T > pt )
{
}

Hàm source() chuyển quyền sở hữu con trỏ T* cho môi trường gọi (calling environment). Dù môi trường gọi có sử dụng giá trị trả về của source() hay không thì tài nguyên được cấp phát trong hàm cũng sẽ được giải phóng một cách an toàn.
Hàm sink() nhận một tham số auto_ptr truyền bởi giá trị nên lấy được quyền sở hữu đối với đối tượng. Đối tượng sẽ bị hủy khi hàm sink() kết thúc. Trong ví dụ trên, sink() không hề xử lí tham số nhận vào nên viết sink( pt ) chính là một cách viết “lạ mắt” của lệnh “pt.reset( 0 );” Trong thực tế, các hàm sink thường thực hiện một số xử lí trên tham số nhận vào trước khi giải phóng nó.

Những sai lầm khi sử dụng auto_ptr

Đừng bao giờ sử dụng auto_ptr ngoài những cách được trình bày ở phần trên. Rất nhiều người sử dụng sai auto_ptr sai mục đích. Họ quên mất rằng auto_ptr có một điểm khác biệt cơ bản so với các kiểu dữ liệu khác. Điểm khác biệt đó là: Đối với các đối tượng kiểu auto_ptr, các bản sao là KHÔNG giống nhau. Đặc điểm này khiến cho chúng ta không thể sử dụng auto_ptr trong các lớp container bởi hàm thành phần của các lớp này thường sao chép giá trị gốc và sau đó làm việc với các bản sao. Xem đoạn mã rất hay gặp sau đây

//Ví dụ 8: Sai lầm nguy hiểm khi sử dụng sai auto_ptr
//
std::vector< std::auto_ptr< T > > v;

/* ... */

sort( v.begin(), v.end() );

Nhiều người nói rằng trình dịch mà họ sử dụng không hề báo lỗi trong trường hợp này, một số còn khẳng định đã nhìn thấy vị dụ này trong tài liệu hướng dẫn của một trình dịch đang được sử dụng rộng rãi. Đừng bao giờ tin họ!!!

Hãy nhớ rằng, auto_ptr không thỏa mãn một điều kiện cơ bản đối với các kiểu dữ liệu có thể lưu trữ trong container, theo đó các bản sao của cùng một đối tượng phải giống nhau. Để phục vụ mục đích sắp xếp, các hàm sort thường lựa chọn một phần tử trung tâm (pivot element) và sao chép phần tử đó nhiều lần. Nếu phần tử trung tâm là một auto_ptr thì ngay trong lần sao chép đầu tiên, quyền sở hữu của nó đã bị chuyển sang một auto_ptr tạm. Do đó, những lần sao chép tiếp thep đều được thực hiện trên một auto_ptr không sở hữu cái gì cả! Hơn nữa, khi kết thúc hàm sort, auto_ptr tạm sẽ bị hủy, nghĩa là vùng nhớ mà giá trị trung tâm từng sở hữu sẽ bị hủy!

Ủy ban chuẩn hóa (standard committee) đã dùng một “mẹo” (trick) để giải quyết vấn đề này. Theo đó, các copy constructor và assignment operator của lớp auto_ptr sẽ nhận vào tham số cho đối tượng bên về phải (rhs) là các reference to non-const. Trong khi đó, hàm thành phần của các container lại nhận vào tham số là các reference to const, do đó không làm việc được với các auto_ptr.

const auto_ptr

const auto_ptr là các auto_ptr không bao giờ bị mất quyền sở hữu hiện có. Sao chép một const auto_ptr là không hợp lệ. Với const auto_ptr, chúng ta chỉ có thể gọi các toán tử operator*(), operator->() và hàm thành phần get(). (các hàm thành phần còn lại là reset() và release() đều làm thay đổi quyền sở hữu của một auto_ptr ). Xem ví dụ sau

//Ví dụ 9: const auto_ptr idiom
//
const std::auto_ptr< T > pt1( new T ); //pt1 không thể được sao chép sang một auto_ptr khác
//bởi vậy nó không bao giờ bị mất quyền sở hữu

std::auto_ptr< T > pt2( pt1 ); //Không hợp lệ!!!

std::auto_ptr< T > pt3;
pt3 = pt1; //Không hợp lệ

pt1.release(); //Không hợp lệ
pt1.reset( new T ); //Không hợp lệ

Tóm lại, nếu bạn muốn một auto_ptr không bao giờ bị mất quyền sở hữu, hãy khai báo nó là một const auto_ptr.

auto_ptr và exception-safety

Trong một số trường hợp, sử dụng auto_ptr là yêu cầu bắt buộc để viết được những đoạn mã có tính exception-safe. Excepion-safe cũng là một chủ đề rất thú vị và sẽ được trình bày trong một bài viết sau.

Tài liệu tham khảo

Sách:

Item 37, Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Herb Sutter

Section 4.2, The C++ Standard Libarary by Nicolai M.Josuttis

More Effective C++: 35 New Ways to Improve Your Programs and Designs by Scott Meyers

Website:

Những bài báo dưới đây được trích ra từ những cuốn sách trên

http://www.gotw.ca/publications/using_auto_ptr_effectively.htm#2

http://www.informit.com/content/images/020163371X/autoptrupdate/auto_ptr_update.html

6 Responses

Subscribe to comments with RSS.

  1. kiennguyen said, on November 12, 2007 at 11:57 am

    Anh Hoàng format code giúp em với:-D Thanks a lot!

  2. Hoang Tran said, on November 12, 2007 at 4:21 pm

    Nice.

  3. Hung Tuan said, on November 23, 2007 at 10:05 pm

    Cac bai viet cua anh cong nhan rat hay va bo ich! Hy vong anh se co nhieu bai viet nhu vay nua!

  4. trungpn said, on December 4, 2007 at 4:46 pm

    cam on ban, bai viet rat de hieu va huu ich

  5. Van Tai said, on March 30, 2008 at 6:08 am

    Bai viet rat bo ich, truoc day toi rat mu mo ve con tro trong C++. Cam on anh rat nhieu

  6. trannam said, on August 26, 2008 at 3:20 pm

    cảm ơn anh


Leave a Reply