Từ khóa typename trong C++
So với các từ khóa phổ biến của C++ mà chúng ta hay gặp thì typename là một từ khóa mới, ra đời cùng với sự ra đời của khái niệm template. Mục đích của typename là để thông báo rằng tên (identifier) được viết ngay sau từ khóa là một tên kiểu (type). Cụ thể, typename được sử dụng trong hai trường hợp sau đây
1 – Từ khóa typename có thể được sử dụng thay cho từ khóa class trong các định nghĩa template:
Ví dụ, thay vì viết:
template < class T >
class MyClass {...};
Chúng ta có thể viết
template < typename T >
class MyClass {...};
Hai từ khóa typename và class được sử dụng đồng thời trong trường hợp này là do vấn đề lịch sử. Khi mới bắt tay vào viết đặc tả cho các template, ngài Stroustrup đã quyết định tái sử dụng từ khóa class thay vì giới thiệu một từ khóa mới. Một số người thích sử dụng typename hơn vì nó có vẻ mang tính đại diện hơn so với class (kiểu T không chỉ là class mà còn có thể là các kiểu dữ liệu cơ bản nữa). Trái lại, một số người thích dùng class hơn vì sẽ tốn ít thời gian gõ phím hơn
(để gõ từ typename chúng ta phải di chuyển ngón tay khắp bàn phím!). Một số khác thì thích sử dụng class hơn bởi họ muốn để dành typename cho trường hợp hai dưới đây. (more…)
Programming Style (2)
Các idiom
Không biết dịch từ “idiom” thế nào cho chính xác. Tạm hiểu idiom là các chuẩn không bắt buộc nhưng được đa số người dùng tuân theo. Sử dụng các idiom giúp giảm bớt khả năng mắc lỗi đồng thời làm chương trình dễ đọc hơn và nhất là có vẻ “chuyên nghiệp” hơn
Sau đây là một số idiom phổ biến:
Các idiom cho mảng
Để duyệt qua n phần tử của một mảng và khởi tạo chúng, có các cách viết sau đây:
i = 0;
while ( i <= n – 1 )
array[ i++ ] = 1.0;
hoặc
for( i = 0; i < n; )
array[ i++ ] = 1.0;
hoặc
for( i = n; –i >= 0; )
array[ i ] = 1.0;
Tất cả những cách viết trên đều đúng, tuy nhiên idioms cho trường hợp này là:
for( i = 0; i < n; i++ )
array[ i ] = 1.0;
Idiom của vòng lặp duyệt qua các phần tử của một danh sách (list) là
for( p = list; p != NULL; p = p->next )
...
Đối với các vòng lặp vô hạn, idiom là
for ( ; ; ) ...
hoặc
while( 1 )
...
Các idiom cho xâu và kí tự
Xem đoạn mã sau đây:
Những hàm được viết và gọi “thầm lặng”
Tham khảo: Item 45 – Effective C++ by Scott Meyers
Để lập trình C++ một cách hiệu quả chúng ta nên hiểu compiler (trình biên dịch) đã làm những gì, biên dịch thế nào cho chúng ta.
Điều gì đã xảy ra nếu chúng ta khai báo một class thế này:
class Empty{};
Lớp đó có thực sự là chẳng có gì không? Thực ra có một số member function mà trình biên dịch “quá thông minh” đã thêm vào class đó rồi. Nghĩa là vì chúng ta không khai báo copy constructor, assignment operator, destructor, và một cặp toán tử lấy địa chỉ (address-of operators) thì trình biên dịch đã tự động thêm những hàm mặc định cho chúng ta rồi. Tất cả những hàm này đều là public. Nói một cách khác thì lớp trên sẽ giống hệt khi chúng ta viết như sau:
class Empty
{
public:
Empty(); // default constructor
Empty(const Empty& rhs); // copy constructor
~Empty(); // destructor - it's nonvirtual
Empty& operator=(const Empty& rhs); // assignment operator
Empty* operator&(); // address-of operators
const Empty* operator&() const;
};
Những hàm được viết và gọi “thầm lặng”
Tham khảo: Item 45 – Effective C++ by Scott Meyers
Để lập trình C++ một cách hiệu quả chúng ta nên hiểu compiler (trình biên dịch) đã làm những gì, biên dịch thế nào cho chúng ta.
Điều gì đã xảy ra nếu chúng ta khai báo một class thế này:
class Empty{};
Lớp đó có thực sự là chẳng có gì không? Thực ra có một số member function mà trình biên dịch “quá thông minh” đã thêm vào class đó rồi. Nghĩa là vì chúng ta không khai báo copy constructor, assignment operator, destructor, và một cặp toán tử lấy địa chỉ (address-of operators) thì trình biên dịch đã tự động thêm những hàm mặc định cho chúng ta rồi. Tất cả những hàm này đều là public. Nói một cách khác thì lớp trên sẽ giống hệt khi chúng ta viết như sau:
class Empty
{
public:
Empty(); // default constructor
Empty(const Empty& rhs); // copy constructor
~Empty(); // destructor - it's nonvirtual
Empty& operator=(const Empty& rhs); // assignment operator
Empty* operator&(); // address-of operators
const Empty* operator&() const;
};
POSIX Thread (3)
Vậy là chúng ta đã quen với một số hàm cơ bản trong việc tạo và kết thúc thread.
#include <pthread.h>
int pthread_create( pthread_t *restrict thread, const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
void pthread_exit(void *value_ptr);
int pthread_join(pthread_t thread, void **value_ptr);
Một thread được tạo ra sẽ bắt đầu thực hiện đoạn mã trong hàm start_routine() (trong ví dụ trước là hàm thread_function) và kết thúc khi trả về hàm đó. Có thể thấy chúng ta truyền con trỏ hàm (function pointer) tới start_routine vào pthread_create. Tuy nhiên tư tưởng lập trình hướng đối tượng không thích hợp cho những hàm kiểu vậy. Chúng ta thích khởi tạo một thread mới bằng cách tạo ra một instance hay một object của một class nào đó và thực thi bằng cách gọi một member function của object đó. Ví dụ như ta có một class Task, và muốn mỗi đối tượng Task chạy trong một thread mới và thread đó sẽ tự động thực thi member function execute() của đối tượng đó. Nói một mặt nào đó gần như chúng ta muốn tạo ra một function object.
Phân biệt pointer và reference
(Nguồn: Item 1 – More Effective C++ – Scott Meyers)
Một câu hỏi interview khá phổ biến trong C++ là phân biệt giữa pointer và reference. Khi nào thì sử dụng pointer và khi nào thì sử dụng reference?. Pointer và reference thoạt nhìn thì thấy nó khác nhau nhưng dường như chúng đều làm những công việc giống nhau. Chúng đều cho phép chúng ta truy nhập gián tiếp vào một đối tượng khác.
Điểm đầu tiên có thể nhận ra rằng không có một cái nào được gọi là null reference (trong khi có null pointer). Có nghĩa là một reference phải luôn luôn refer đến một object nào đó. Do đó, nếu bạn có một biến mà mục đích của nó là refer đến một object nào đó nhưng nó có thể refer đến không object nào cả, thì bạn nên sử dụng pointer bởi vì sau đó bạn có thể cho giá trị của nó là null. Mặt khác, nếu biến đó phải luôn luôn refer đến một object (design không cho phép có khả năng biến đó là null) thì bạn nên để biến đó là reference.
Thế nhưng, trường hợp này thì sao
char *pc = 0; // set pointer to null char& rc = *pc; // make reference refer to dereferenced null pointer
Programming Style (1)
Sau một thời gian ra trường và đi làm với công việc chính là lập trình, nhiều khi tôi nhìn lại thời đại học và giật mình vì thấy mình đã học lập trình một cách quá hời hợt. Có rất nhiều kiến thức cơ bản mà tôi đã không học (hay không được dạy?). Nhớ lại hồi năm thứ nhất học Pascal, chúng tôi được học các lệnh if the, while, for…với các ví dụ kiểu như tìm tất cả các ước số của một số nguyên, hay quản lí sắp xếp sinh viên…Đến năm thứ 3 học C, chúng tôi vẫn học từng ý lệnh với từng ý kiểu bài tập. Có quá nhiều những kiến thức vô cùng quan trọng mà các thầy không hề nhắc đến (chứ chưa nói gì đến dạy), chẳng hạn như: Phong cách lập trình (Programming Style), kĩ thuật gỡ lỗi (debug), kiểm thử (test), hiệu năng của chương trình (performance), tính khả chuyển (portable)…Đáng tiếc là sự tồn tại của những kiến thức này lại không hiển nhiên cho lắm để sinh viên có thể tự tìm sách để đọc.
Một trong những cuốn sách tuyệt vời viết về những chủ đề nói trên là cuốn “The practice of Programming” của Brian W. Kernighan và Rob Pike (tôi ước là mình biết đến cuốn sách này sớm hơn). Đây là một cuốn sách được “recommend” cho tất cả các lập trình viên của tất cả các ngôn ngữ lập trình. Trong bài viết này tôi muốn giới thiệu chương đầu tiên của cuốn sách với tiêu đề “Style”. Những chương tiếp theo tôi sẽ giới thiệu khi có thời gian, hoặc tốt nhất là mọi người tự tìm đọc bản tiếng Anh để hấp thu được toàn bộ sự sâu sắc của cuốn sách. (Tôi có bản mềm của cuốn sách này, ai cần có thể PM)
Phong cách lập trình (programming style)
Mở đầu
Hãy xem đoạn mã sau đây:
if ( ( country == SING ) || ( country == BRNI ) ||
( country == POL ) || ( country == ITALY ) ) {
/*
* If the country is Singapore, Brunei or Poland
* then the current time is the answer time
* rather than the off hook time
* Reset answer time and set day of week
*/
...
}
Đoạn mã trên được viết một cách rất đẹp đẽ, được comment cẩn thận và là một phần trong một chương trình chạy hoàn toàn đúng. Tuy nhiên đoạn mã này vẫn gây ra một chút băn khoăn cho người đọc: Những đất nước Singapore, Brunei, Poland và Italy có mối liên hệ gì với nhau? Tại sao Italy lại không được nói đến trong phần comment? Vì đoạn mã và phần comment có sự khác nhau nên một trong hai thứ phải sai mà cũng có thể là cả hai đều sai. Đoạn mã có nhiều khả năng đúng hơn vì nó đã được test; có thể phần comment đã không được cập nhật theo sự thay đổi của đoạn mã. Nếu là người bảo trì cho đoạn chương trình này, rất có thể bạn cần phải biết mối quan hệ giữa các quốc gia được nhắc đến.
Đoạn chương trình nói trên đại diện cho phần lớn các chương trình trong thực tế: Hầu hết chạy đúng nhưng vẫn cần phải cải tiến.
Mục đích của style là làm cho chương trình trở nên dễ đọc đối với người viết và những người khác, một style tốt là một phần thiết yếu của việc lập trình tốt. Viết một chương trình chạy đúng là chưa đủ bởi chương trình không chỉ để cho máy tính đọc mà còn để các lập trình viên khác đọc. Hơn nữa, một chương trình có style tốt luôn có nhiều khả năng chạy đúng hơn một chương trình có style tồi.
Vậy thế nào là một style tốt? Điều đó tùy thuộc vào quy định của từng công ty, tổ chức, dự án…Phần sau đây giới thiệu những style cơ bản nhất.
POSIX Thread (2)
Chúng ta hãy bắt đầu bằng một ví dụ đơn giản.
thread1.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
void *thread_function(void *arg)
{
int i;
for ( i=0; i<20; i++ ) {
printf("Thread says hi!\n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t mythread;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
printf(“Waiting for thread to finish...\n”);
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
exit(0);
}
Biên dịch chương trình
$ gcc thread1.c -o thread1 -lpthread (more…)
POSIX Thread (1)
Biết cách lập trình với thread và multithread là một trong những kỹ năng cần thiết của một programmer tốt. Trong bài viết này sẽ đề cập về POSIX (Portable Operating System Interface) threads. Như bạn đã biết POSIX (chính xác hơn là chuẩn IEEE 1003.1c của tổ chức IEEE đưa ra) bao gồm những định nghĩa “giao diện” chung cho các hệ điều hành. Điều đó có nghĩa là những hệ điều hành nào support POSIX (GNU/Linux, BSD, Sun Solaris, Unix, …) thì đều có những system call có prototype giống như trong tài liệu về POSIX đưa ra, mặc dù đối với mỗi hệ điều hành có cách implement khác nhau. POSIX threads (Pthreads) là một cách rất tốt để làm tăng độ tin cậy và performance cho chương trình.
Threads cũng tương tự như processes, đều được phân chia thời gian bởi kernel. Với hệ thống chỉ có một bộ vi xử lý thì kernel sử dụng cách phân chia thời gian để “làm cho” các threads như là chạy đồng thời theo cùng cách thức kernel thực hiện với processes. Và với các hệ thống đa nhân thì các threads thực sự có thể chay đồng thời giống như là nhiều processes.
Thế thì tại sao multithread lại được ưa chuộng hơn là nhiều process độc lập đối với các task có mối quan hệ với nhau? Đó là bởi vì các threads sử dụng chung cùng một không gian bộ nhớ. Mỗi thread độc lập đều có thể truy nhập vào cùng một biến toàn cục trong bộ nhớ. Trong khi fork() cho phép tạo ra nhiều process nhưng rất khó khăn trong việc trao đổi thông tin giữa process với nhau vì mỗi process có một không gian vùng nhớ riêng. Không có một câu trả lời đơn giản cho việc trao đổi giữa các process (IPC). Do vậy mà multiprocess programming sẽ phải chịu 2 trở ngại lớn:
- Perforamance thấp vì khi tạo một process mới đòi hỏi kernel thực thi nhiều phép tính toán để cấp phát bộ nhớ.
- Trong hầu hết các trường hợp thì IPC làm chương trình trở nên phức tạp hơn rất nhiều.
Processes and Multi-process Programming (1)
Thực sự khi đọc đi đọc lại bài viết về fork() tôi cảm thấy nó rất là tệ vì quá sơ sài, không làm nổi bật được vai trò của process trong operating system, sự phức tạp của nó cũng như ưu điểm của multi-process programming. Nó đơn giản chỉ là viết về một system call fork() mà thôi, và điểm cơ bản là không thấy được multi-process programming có thể giúp cho hệ thống trở nên mạnh mẽ như thế nào.
Khái niệm trọng tâm trong tất cả các hệ điều hành (operating system) là process (tiến trình). Một process về cơ bản chỉ là một chương trình có thể thực thi. Đi cùng với process là một không gian địa chỉ (address space) – vùng nhớ (từ tối thiểu, thông thường là 0, đến cực đại) mà process có thể đọc và ghi. Không gian địa chỉ này bao gồm đoạn mã thực thi, dữ liệu và stack của nó. Cũng đi cùng với mỗi process là một tập các thanh ghi, bao gồm program counter, stack pointer, các thanh ghi khác và tất cả các thông tin cần thiết để chạy chương trình.
Tất cả các hệ điều hành tiên tiến chạy trên các máy tính cá nhân bây giờ đều là multi-process, có nghĩa là cho phép chạy nhiều process “cùng lúc”. Hệ điều hành sẽ là trung tâm quản lý các process, nó sẽ quyết định khi nào thì dừng một process và start hay tiếp tục một process khác. Khi một process được tạm dừng kiểu này, thì nó sau đó phải được restart tại chính trạng thái mà nó bị dừng. Điều đó có nghĩa là tất cả các thông tin của process phải được lưu ở đâu đó bên ngoài trong lúc tạm dừng. Ví dụ một process có thể đang mở một vài file để đọc. Với mỗi file này thì có một con trỏ chỉ đến vị trí đang đọc trong file. Khi một process bị tạm thời dừng lại, thì tất cả các con trỏ này phải được ghi lại để sau đó các lệnh tiếp theo đối với file đang mở sẽ có được dữ liệu chính xác khi process được tiếp tục. Trong rất nhiều hệ điều hành, các thông tin về mỗi process được lưu vào trong một bảng được gọi là process table (là một mảng hay link-list mà mỗi phần tử là process đang tồn tại). Vì vậy mỗi process bao gồm không gian địa chỉ của nó, thường được gọi là core image, và một entry trong process table mà chứa đựng thanh ghi của nó và những thứ khác.
Những system calls quan trọng nhất trong process management là những lệnh liên quan đến quá trình tạo và huỷ process. Hãy xem xét một ví dụ điển hình: một process gọi command interpreter hay shell để đọc lệnh từ terminal. User gõ lệnh yêu cầu chương trình được dịch, và shell phải tạo ra một process mới mà chạy trình biên dịch. Khi process kết thúc quá trình biên dịch, nó thực hiện một system call để huỷ chính nó.
Nếu một process tạo ra một hay nhiều process khác (thường được gọi là child processes) và những process đó lại có thể tạo ra những process con, chúng ta sẽ có một cấu trúc cây process (process tree structure). Việc trao đổi thông tin giữa các process để đồng bộ các hành động giữa các process được gọi là interprocess communication (IPC) cũng là một vấn đề lớn trong multi-process programming.
Xây dựng ứng dụng multi-process là một công việc khó khăn. Process khi hoạt động phải luôn ở trạng thái tôn trọng và sẵn sàng nhường quyền xử lý CPU cho các process khác ở bất kỳ thời điểm nào, khi hệ thống yêu cầu. Nếu process xây dựng không tốt, thì khi nó đổ vỡ và gây ra lỗi thì có thể làm treo các process khác hay thậm chí phá vỡ hệ điều hành (treo).
leave a comment