Function Pointer
Bài viết về function pointer được trích dịch từ tài liệu này: http://www.newty.de/fpt/index.html
1 Giới thiệu
Function Pointer cung cấp một kỹ thuật lập trình cực kỳ thú vị, hiệu quả và “đầy màu sắc”. Chúng ta có thể sử dụng nó để thay thế câu lệnh switch/if, xây dựng quá trình late-binding hoặc implement hàm callback. Tiếc thay, có thể vì sự phức tạp của nó mà nó được đề cập rất ít trong hầu hết sách và tài liệu. Nếu có thì nó chỉ được trình bày một cách rất tóm tắt và sơ sài. Thực ra thì nó ít gây ra lỗi hơn so với pointer bình thường bởi vì chúng ta không bao giờ phải allocate hoặc de-allocate bộ nhớ cả. Tất cả việc chúng ta cần làm là hiểu nó làm gì và học cú pháp của nó. Nhưng hãy luôn tâm niệm rằng: hãy tự hỏi bạn có thực sự cần đến function pointer hay không? Rất tuyệt để thể hiện cách thức late-binding, thế nhưng sử dụng cấu trúc hiện tại của C++ làm cho đoạn mã trở nên dễ đọc và rõ ràng hơn. Một khía cạnh khác của late-binding là runtime: nếu bạn gọi một virtual function, chương trình sẽ xác định hàm nào được gọi. Nó làm điều đó bằng cách sử dụng V-Table mà chứa tất cả những hàm có thể gọi. Điều đó có vẻ hơi lãng phí mỗi lần gọi, và có thể bạn sẽ tiết kiệm một chút nếu sử dụng function pointer thay vì virtual function. Cũng có thể không …
1.1 Function Pointer là gì?
Function pointer là một pointer mà nó chỉ đến địa chỉ của một hàm. Bạn phải luôn giữ trong đầu rằng một chương trình chạy sẽ chiếm một không gian bộ nhớ xác định trong bộ nhớ chính. Cả đoạn chương trình thực thi đã được dịch từ mã mà bạn viết và các biến sử dụng đều được đưa vào trong không gian bộ nhớ này. Vì vậy một function trong chương trình của bạn không có gì khác hơn là một địa chỉ trong bộ nhớ.
1.2 Thay thế câu lệnh Switch như thế nào?
Khi chúng ta muốn gọi một hàm DoIt() ở một label xác định trong chương trình, chúng ta phải để lời gọi tới hàm DoIt() tại label đó. Sau đó biên dịch và mỗi khi chương trình chạy tới label đó thì hàm DoIt() sẽ được gọi. Mọi thứ đều ok, nhưng sẽ làm gì nếu giả sử chúng ta không biết tại thời điểm build-time (thời gian dịch) hàm nào sẽ được gọi? Nghĩa là chỉ đến lúc chạy ta mới biết ở label đó thì nên chạy DoIt() hay một hàm nào khác. Đó chính là lúc chúng ta muốn sử dụng đến callback-function hoặc là sử dụng kỹ thuật lấy ra từ một “pool” chứa các possible function. Tuy nhiên thì chúng ta có thể giải quyết vấn đề này bằng cách sử dụng lệnh switch, và lựa chọn lời gọi đến hàm thích hợp ở những nhánh khác nhau tùy theo giá trị biểu thức của switch. Nhưng vẫn có một cách khác là sử dụng function pointer. Trong ví dụ sau đây chúng ta thực hiện nhiệm vụ của bốn toán tử toán học cơ bản (+, -, *, /). Cách đầu tiên sử dụng switch và cách thứ hai sử dụng function pointer.
STL Function Object và các ứng dụng (1)
Function Object là gì?
Function object là một object được sử dụng như một function. Với một function object của lớp Foo, khi viết Foo() nghĩa là chúng ta đang gọi đến operator() của lớp Foo. Viết một function object nghĩa là viết operator() cho một lớp. Chúng ta đã biết operator của một lớp được viết như sau
class Foo
{
public:
return_type operator() ( parameter list ) {
statements;
}
/* Các public member khác */
private:
/* Các private member */
};
Cài đặt cụ thể cho operator() tùy thuộc vào ngữ cảnh sử dụng của function object. Qua cái nhìn đầu tiên, chúng ta thấy rằng cách viết này chính là sự phức tạp hóa của một hàm bình thường sau đây
return_type foo( parameter list ) {
statements;
}
Sự phức tạp hóa này mang lại ba lợi ích
1- Các function object là các object, bởi vậy chúng có trạng thái, còn các hàm bình thường thì không.
2- Các function object thuộc về một lớp nào đó. Bởi vậy, chúng ta có thể tham số hóa các kiểu dữ liệu bên trong function object thông qua template.
3- Các function object thường chạy nhanh hơn các hàm thông thường.
Không nên mất thời gian suy nghĩ về ba lợi ích này làm gì! Hãy nghiên cứu các ứng dụng của function object, chúng ta sẽ dễ dàng hiểu được những lợi ích của chúng.
Ứng dụng của function object
Các function object được sử dụng trong hai trường hợp sau đây
1- Làm tiêu chí sắp xếp cho các container
2- Làm tham số cho các STL algorithm
Việc một function object được sử dụng ở đâu sẽ quyết định cách viết operator() của lớp đó.
Function Object làm tiêu chí sắp xếp cho các container
Trong phần này, chúng ta sẽ xem xét các ứng dụng của function object trong việc tạo ra các tiêu chí sắp xếp cho các STL set. Các ví dụ này có thể mở rộng cho các STL associative container khác như multiset, map, multimap.
Khi chúng ta đưa các phần tử vào một set, chúng sẽ được sắp xếp sao cho hai phần tử liên tiếp phải thỏa mãn tiêu chí sắp xếp dành cho set đó. Nếu các phần tử của set là các kiểu cơ bản như int hay string, chúng ta có thể sử dụng các tiêu chí sắp xếp sẵn có như greater hay less. Ví dụ dòng khai báo dưới đây
std::set< std::string, greater > strSet;
khai báo một set với các phần tử là các STL string được sắp xếp theo thứ tự tăng dần. Tuy nhiên, nếu các phần tử cần đưa vào set có kiểu do người dùng định nghĩa, ví dụ là các đối tượng của một lớp, thì làm sao để xác định thứ tự của chúng trong set? Có hai cách thực hiện: Một là vẫn sử dụng các tiêu chí sẵn có là less và greater. Tuy nhiên, cách này chỉ thực hiện được nếu lớp đã định nghĩa sẵn operator < (cho tiêu chí less) hoặc operator > (cho tiêu chí greater). Không phải lớp nào cũng cung cấp sẵn các operator này, mà không phải lúc nào chúng ta cũng có quyền “nhảy” vào để thêm mã cho lớp, mà giả sử chúng ta có quyền đi nữa thì cũng không nên làm phức tạp hóa một lớp sẵn có. Cách thứ hai là chúng ta tự định nghĩa một tiêu chí sắp xếp mới, đây chính là lúc cần đến function object. Xem ví dụ sau đây: Giả sử chúng ta cần lưu các đối tượng của lớp Person vào một set. Định nghĩa của lớp Person như sau:
Phụ lục cho bài viết về auto_ptr: Tìm hiểu thêm về lệnh delete
Nhân tiện bài viết trước về auto_ptr, chúng ta ngó qua một chút về lệnh delete thông qua một số Q&A.
Nguồn: http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.11
Q: Lệnh delete p xóa con trỏ p hay xóa vùng nhớ trỏ đến bởi p?
A: Vùng nhớ được trỏ đến bởi p.
Khi viết delete p có nghĩa là delete_the_thing_pointed_to_by p. Chúng ta có tình huống tương tự khi giải phóng bộ nhớ bởi lệnh free. free( p ) thực chất là free_the_stuff_pointed_to_by( p ).
Q: Có an toàn không nếu gọi delete hai lần cho cùng một con trỏ?
A: Không, nếu không có lệnh new nào cho con trỏ đó xen vào giữa.
Đoạn mã sau có thể gây ra thảm họa
class Foo { ... };
void yourCode()
{
Foo* p = new Foo();
delete p;
delete p; //← thảm họa!
...
}
Lệnh delete thứ hai có thể gây ra những thảm họa như làm hỏng vùng nhớ heap, làm đổ vỡ chương trình, làm thay đổi một cách tùy ý các object đang tồn tại trong heap. Thật không may, những hậu quả này xảy ra một cách khá ngẫu nhiên. Một số môi trường chạy (runtime environment) có thể giúp bạn tránh được những hậu quả của việc delete hai lần trong một số trường hợp đơn giản. Tuy nhiên, delete một con trỏ hai lần vẫn là một việc làm tồi tệ.
Q: Có cần kiểm tra con trỏ NULL trước khi delete hay không?
A: Không
C++ đảm bảo rằng lệnh delete p sẽ không làm gì cả nếu p là một con trỏ NULL. Bởi vậy, lệnh if trong đoạn mã sau đây là thừa
if( p != NULL )
delete p;
Q: Điều gì thật sự xảy ra khi viết delete p?
A: delete p thực hiện hai việc: Gọi destructor của đối tượng được trỏ tới bởi p và giải phóng vùng nhớ của đối tượng đó. delete p có chức năng tương tự như đoạn mã sau đây, với p là con trỏ kiểu Fred*
Lớp auto_ptr trong C++
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: (more…)
POSIX Thread (6) – Mutex
Chúng ta đã hiểu vấn đề nảy sinh ở chương trình trước. Để giải quyết bài toán xung đột đó chũng ta hãy xem đoạn mã đúng sử dụng mutex:
thread3.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg)
{
int i,j;
for ( i=0; i<20; i++ ) {
pthread_mutex_lock(&mymutex);
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
pthread_mutex_unlock(&mymutex);
}
return NULL;
}
int main(void)
{
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
for ( i=0; i<20; i++) {
pthread_mutex_lock(&mymutex);
myglobal=myglobal+1;
pthread_mutex_unlock(&mymutex);
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}
POSIX Thread (5) – Synchronization
Bây giờ hãy xem xét một chương trình mà nó có kết quả không như chúng ta mong đợi.
thread2.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
void *thread_function(void *arg) {
int i,j;
for ( i=0; i<20; i++ ) {
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
}
return NULL;
}
int main(void) {
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
for ( i=0; i<20; i++) {
myglobal=myglobal+1;
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}
POSIX Thread (4) – No parents, no children
Nguồn: http://www.ibm.com/developerworks/linux/library/l-posix1.html?
Nếu bạn đã từng sử dụng system call fork(), bạn chắc chắn đã quen với khái niệm về parent và child processes. Khi một process tạo ra một process mới bằng cách sử dụng fork() thì process mới được xem như là child process và process nguyên thủy của nó là parent. Nó sẽ tạo ra một cây process có mối quan hệ với nhau và có thể tương tác với nhau; như chờ đợi cho child process kết thúc. Ví dụ hàm waitpid() sẽ nói cho process hiện tại chờ tất cả child processes kết thúc. Waitpid được sử dụng để implement một hàm “dọn dẹp” cho parent process.
Có một chút thú vị khi sử dụng POSIX threads. Những thuật ngữ như parent thread hay child thread không có bởi vì thực sự với POSIX thread thì một mô hình các mối quan hệ giữa các thread là không tồn tại. Khi một thread chính có thể tạo ra một thread mới và thread mới này cũng có thể tạo thêm một thread mới khác. Tuy nhiên thì không có gì nói rằng thread mới đó là “cháu” của main thread. POSIX thread sẽ coi tất cả các thread như là một cái pool (giống như queue) đơn lẻ mà mỗi phần tử là một thread ngang hàng nhau. Do đó khái niệm đợi một child thread không có ý nghĩa. Việc thiếu tính chất thế hệ này ngụ ý một điểm chính: nếu bạn muốn đợi một thread kết thúc thì bạn cần phải xác định thread nào bạn đợi bằng truyền chính xác tid vào pthread_join. Thư viện thread không thể làm điều này hộ chúng ta.
Đối với nhiều người thì điều này không phải là tin tốt vì nó có thể làm phức tạp chương trình có nhiều hơn hai threads. Đừng để điều đó là phiền chúng ta. Chuẩn POSIX thread cung cấp công cụ để chúng ta có thể quản lý nhiều thread một cách dễ dàng. Sự thực thì việc không có mối quan hệ parent/children mở ra một cách nghĩ sáng tạo. Ví dụ, nếu chúng ta có một thread gọi là thread 1 và thread 1 này tạo ra một thread khác là thread 2, thì không nhất thiết là bản thân thread 1 phải gọi pthread_join cho thread 2. Bất kỳ thread nào khác trong chương trình cũng có thể làm điều này. Nó cho phép những khả năng thú vị khác khi bạn viết những đoạn mã multithread khổng lồ. Ví dụ bạn có thể tạo một “dead list” toàn cục bao gồm tất cả những thread đã bị dừng lại và sau đó có một thread đặc biệt để “dọn dẹp” chúng, đơn giản chỉ là đợi một item được thêm vào trong list. Thread có nhiệm vụ dọn dẹp đó sẽ gọi pthread_join() để merge với chính nó. Bây giờ thì quá trình “dọn dẹp” có thể được kiểm soát gọn gằng và hiệu quả trong một thread đơn.
(Còn tiếp)
Prototype Pattern
Trong quyển GoF Design Pattern có 23 mẫu thiết kế và khi được phân chia theo mục đích sử dụng thì có 5 mẫu kiến tạo (creational pattern) bao gồm: Abstract factory, Builder, Factory method, Prototype và Singleton. Creational Pattern trừu tượng hóa quá trình khởi tạo object. Các mẫu creational giúp hệ thống không phải phụ thuộc vào cách một object được tạo ra, xây dựng và thể hiện.
1. Mục đích và ý nghĩa
Prototype Pattern giúp cho việc khởi tạo object bằng một object nguyên mẫu (prototype), là copy của object “mẫu” đó. Ý tưởng của mẫu là chúng ta được cung cấp một object và sẽ dùng chính object này để như là một hình mẫu (template) khi cần tạo lập một đối tượng mới. Việc tạo lập object mới sẽ dựa trên object mẫu mà không sử dụng toán tử new hoặc constructor … được cụng cấp bởi ngôn ngữ lập trình. Lý do là chúng ta không biết được thông tin nội tại chi tiết bên trong object và object có thể che dấu và chỉ cung cấp ra bên ngoài một lượng thông tin giới hạn. Do vậy ta không thể dùng toán tử new và sao chép những dữ liệu được object cung cấp (vốn không đầy đủ) cho một object mới. Cách tốt nhất là để cho chính object “mẫu” tự xác định thông tin và dữ liệu sao chép.
2. Cấu trúc

- Prototype: Cung cấp interface để copy chính bản thân nó (phương thức clone())
- ConcreatePrototype: Implement interface được cung cấp bởi Prototype để copy chính bản thân nó. (thể hiện cụ thể phương thức clone())
- Client: Tạo một object mới bằng cách yêu cầu prototype copy chính bản thân nó.
3 comments