Nested Functions

Bài viết trích từ  openandfree. Bài gốc tiếng Anh tại trang gotw


Bài viết này không trình bày chi tiết về nested class trong C++ mà chỉ tập trung vào các kĩ thuật sử dụng nested class và function object để mô phỏng các nested function, một yếu tố không có trong C++. Các chi tiết về nested class có thể tìm thấy trong nhiều cuốn sách C++ khác, ví dụ cuốn Thinking in C++, tập 1.

Bài viết đưa ra ba câu hỏi và sau đó lần lượt đi tìm các câu trả lời cho chúng. Ba câu hỏi là:

1- Nested class là gì? Tại sao chúng ta cần các nested class?
2- Local class là gì? Tại sao chúng ta cần các local class?
3- Trong C++ không có khái niệm nested function. Bởi vậy, chúng ta không thể viết một đoạn mã như sau


int f( int i )
{
int j = i*2;
int g( int k )
{
return j+k;
}
j += 4;
return g( 3 );
}

Hãy đưa ra một giải pháp mô phỏng các hàm f và g sao cho đạt được một “hiệu ứng” tương tự như đoạn mã trên.



Type rest of the post hereHãy đưa ra một giải pháp mô phỏng các hàm f và g sao cho đạt được một “hiệu ứng” tương tự như đoạn mã trên.

Trả lời

C++ có rất nhiều công cụ hữu ích dùng để ẩn thông tin (information hiding) và quản lí sự phụ thuộc mã nguồn (dependency management). Các đoạn mã sau đây có thể chưa hoàn toàn chính xác về mặt cú pháp, chúng được dùng để minh họa cho các kĩ thuật thiết kế mà thôi.

1- Nested class là gì? Tại sao chúng ta cần nested class?

Nested class là một class được “viết” (enclosed) bên trong phạm vi (scope) của một class khác.

// Ví dụ 1: Nested class
//
class OuterClass
{
class NestedClass
{
// ...
};
// ...
};

Trong đoạn mã trên, NestedClass là một nested class được “viết” bên trong class OuterClass. Các nested class rất hữu ích cho việc tổ chức mã nguồn, quản lí quyền truy nhập (access) và các phụ thuộc (dependencies). Các nested class tuân theo các quy tắc thông thường về quyền truy nhập giống như các dữ liệu thành phần và các hàm thành phần. Tức là, nếu NestedClass được khai báo là public thì chúng ta có thể sử dụng nó từ bất cứ đâu thông qua tên gọi OuterClass::NestedClass. Ngược lại, nếu NestedClass được khai báo là private thì chỉ có các thành phần và các hàm bạn (friends) của OuterClass là có quyền truy nhập đến nó. Thông thường, các nested class chứa các cài đặt riêng cho OuterClass, do đó thường được khai báo là private.

Chú ý rằng nested class khác với namespace. Các namespace chỉ thuần túy nhóm các tên lại với nhau chứ không mang lại khả năng quản lí quyền truy nhập. Nếu bạn muốn quản lí quyền truy nhập tới một lớp, một trong các giải pháp là viết nó thành nested class trong một class khác.

2- Local class là gì? Tại sao chúng ta cần các local class?

Local class là một class được định nghĩa bên trong một hàm thông thường hoặc một hàm thành phần (member function). Trong ví dụ sau đây, LocalClass là một local class được định nghĩa bên trong một hàm thông thường có tên là f.

// Ví dụ 2: Local class
//
int f()
{
class LocalClass
{
// ...
};
// ...
};

Giống như nested class, local class là một công cụ hữu ích phục vụ việc quản lí những sự phụ thuộc về mã nguồn (code dependencies). Trong ví dụ 2, chỉ có đoạn mã trong thân hàm f mới được phép sử dụng LocalClass. LocalClass thường chứa những cài đặt riêng cho hàm f nên không cần thiết phải có khả năng truy nhập được từ bên ngoài.

Bạn có thể sử dụng local class gần như trong mọi tình huống có thể sử dụng class thông thường. Một ngoại lệ quan trọng cần ghi nhớ là: Các local class không thể đóng vai trò tham số kiểu (template parameter). Ví dụ dưới đây trích từ tài liệu chuẩn C++:

A local type, a type with no linkage, an unnamed
type or a type compounded from any of these types
shall not be used as a template-argument for a
template type-parameter.
[Example:
template
class X { /* ... */ };

void f()
{
struct S { /* ... */ };
X<s> x3; // error: local type used as
// template-argument
X<s> x4; // error: pointer to local type
// used as template-argument
}
--end example]

Tóm lại, cả nested class lẫn local class đều là những công cụ hữu ích của C++ dùng để ẩn thông tin và quản lí quyền truy nhập và các phụ thuộc.

Nested Funtion

Một vài ngôn ngữ (không phải C++) cho phép chúng ta viết các nested function. Giống như các nested class, nested function là một function được viết bên trong một function khác. Những đặc điểm quan trọng của nested function là:

- Các nested function có quyền truy nhập đến các biến cục bộ của hàm chứa nó.
- Các nested function là “cục bộ”, nghĩa là không thể truy nhập tới chúng từ bên ngoài, trừ khi có một con trỏ trỏ đến nested function được cung cấp bởi hàm chứa.

Nếu như các nested class hữu ích bởi chúng cho phép điều kiển sự “ẩn hiện” (visibility) của một lớp thì các nested function hữu ích bởi chúng cho phép điều khiển sự “ẩn hiện” của một hàm.

Trả lời cho câu hỏi 3: Các giải pháp sử dụng class để mô phỏng nested function trong C++

Chú ý: “mô phỏng” ở đây được hiểu theo nghĩa là: Xây dựng một class g bên trong một hàm f, sao cho f có thể sử dụng g như một nested function

void f()
{
class g { … }; g();
}

Nói đến một class được sử dụng như một function, chúng ta nghĩ ngay đến các function object. Giải pháp đầu tiên mà hầu hết mọi người sẽ đưa ra là:

// Giải pháp 3(a): Một giải pháp thô sử dụng function object
//
//
int f( int i )
{
int j = i*2;
class g_
{
public:
int operator()( int k )
{
return j+k; // ERROR!!!: Không thể sử dụng j bên trong g_
}
} g;

j += 4;

return g( 3 ); // f sử dụng g như một nested function
}

Ý tưởng ở đây là mô phỏng nested function bởi một local class có tên là g_, sau đó gọi operator() của g_. (Xem lại bài viết: STL Function Object và các ứng dụng(1) để biết chi tiết về cách viết một function object). Đây là một ý tưởng hay nhưng đáng tiếc là đoạn mã trên không chạy được! Lí do là một local class không thể sử dụng các biến của hàm bên ngoài (ở đây là biến j). Một giải pháp cho vấn đề này là truyền các biến của hàm bên ngoài vào trong local class thông qua các thành phần dữ liệu của class đó.

// Giải pháp 3(b): Sử dụng function object.
// Thành phần dữ liệu của local class là các references trỏ tới
// các biến cục bộ của hàm bên ngoài (phức tạp, khó bảo trì)
//
int f( int i )
{
int j = i*2;
class g_
{
public:
g_( int &j ) : j_( j ) { }
int operator()( int k )
{
return j_+k; // sử dụng j thông qua reference
}
private:
int &j_;
} g( j );

j += 4;

return g( 3 );
}

Bây giờ thì local class g_ đã có thể sử dụng biến j của hàm bên ngoài thông qua thành phần dữ liệu j_ của nó và hàm f() ở trên đã chạy đúng. Tuy nhiên nhược điểm của giải pháp này là rất khó có thể mở rộng. Trong trường hợp class g_ cần sử dụng một biến khác nữa của hàm f() ngoài biến j (giả sử là biến k kiểu int). Khi đó chúng ta phải thực hiện những thao tác sau đây:

- Thêm vào class g_ một thành phần dữ liệu int &k_;
- Thêm tham số int k cho constructor của g_;
- Thêm một phép khởi tạo cho k_: k_( k );

Sô thao tác cần thực hiện sẽ tăng lên rất nhiều nếu có nhiều local class trong f(). Vậy, chúng ta cần tìm ra một giải pháp tốt hơn.

Một giải pháp tốt hơn

Hàm f được cải tiến bằng cách chuyển các biến cục bộ của nó thành các thành phần dữ liệu public của lớp g_

// Giải pháp 3(c): Một giải pháp tốt hơn
//
int f( int i )
{
class g_
{
public:
int j;
int operator()( int k )
{
return j+k;
}
} g;

g.j = i*2;
g.j += 4;

return g( 3 );
}

Bây giờ thì hàm f đã trở nên đẹp đẽ và dễ bảo trì, phát triển. Giải pháp này gợi cho chúng ta suy nghĩ: Tại sao không “mô phỏng” các nested function của f() bởi các hàm thành phần x(), y(), z() của lớp g_? Đoạn mã sau đây hiện thực hóa ý tưởng này. Lớp g_ được đổi tên thành Local_ để mang tính mô tả tốt hơn

// Giải pháp 3(d): Mô phỏng nested function bằng cách hàm thành phần
//
int f( int i )
{

class Local_
{
public:
int j;
int g( int k )
{
return j+k;
}
void x() { /* ... */ }
void y() { /* ... */ }
void z() { /* ... */ }
} local;

local.j = i*2;
local.j += 4;
local.x();
local.y();
local.z();

return local.g( 3 );
}

Giải pháp này không sử dụng function object. Nhược điểm của nó là j không được khởi tạo một cách linh hoạt do lớp Local_ không có constructor.

Giải pháp hoàn thiện

Nếu f không nhất thiết phải là một hàm thông thường mà chỉ cần được sử dụng như một hàm thông thường, chúng ta có thể xây dựng f dưới dạng một function object và xây dựng các nested function dưới dạng các hàm thành phần như sau:

// Giải pháp 3(e): Một giải pháp hoàn thiện. Dễ bảo trì, phát triển
//
//
class f
{
int retval; // Giá trị trả về của “hàm” f
int j;

//Các hàm thành phần của lớp f
//g(), x(), y(), z() có vai trò tương tự như các nested function
int g( int k ) { return j + k; };
void x() { /* ... */ }
void y() { /* ... */ }
void z() { /* ... */ }

public:
f( int i ) // constructor
: j( i*2 )
{
j += 4;
x();
y();
z();
retval = g( 3 );
}
operator int() // operator() trả về giá trị cho “hàm” f
{
return retval;
}
};

Giải pháp 3(e) là một giải pháp hoàn thiện cho việc mô phỏng nested function g cho một hàm f thông thường. Nếu f không phải là một hàm thông thường mà là một hàm thành phần của một lớp C nào đó thì sao? Khi đó, chúng ta mong muốn mô phỏng được đoạn mã sau đây, trong đó g là một nested function của hàm thành phần f của lớp C (tất nhiên đoạn mã này là không hợp lệ trong C++, vì C++ không cho phép nested function)

// Đoạn mã cần mô phỏng: Một nested function bên trong một hàm thành phần
// Chú ý: Đoạn mã chỉ có tính minh họa, không hợp lệ trong C++
//
//
class C
{
int data_;
public:
/* f là hàm thành phần của lớp C */
int f( int i )
{
// g là một nested function của f
int g( int i ) { return data_ + i; }
return g( data_ + i*2 );
}
};

“Bắt chước” giải pháp 3(e), cộng với một chút điều chỉnh, chúng ta có giải pháp 4(a) sau đây:

- Xây dựng C_f như một function object và là một nested class bên trong lớp C
- Xây dựng g như một hàm thành phần của C_f
- Hàm thành phần f của C gọi đến C_f
// Giải pháp 4(a): Giải pháp hoàn thiện, giống 3(e) nhưng áp dụng cho hàm thành phần
//
//
class C
{
int data_;
friend class C_f;

public:
int f( int i );
};

class C_f
{
C* self;
int retval;
int g( int i ) { return self->data_ + i; }

public:
C_f( C* c, int i ) : self( c )
{
retval = g( self->data_ + i*2 );
}
operator int() { return retval; }
};

int C::f( int i ) { return C_f( this, i ); }

Kết luận

Cũng giống như những bài viết khác trong cùng loạt bài GotW của Herb Sutter, bài viết này đã đưa ra một yêu cầu thiết kế thú vị, sau đó tiến hành xem xét đánh giá những giải pháp khác nhau để cuối cùng chọn ra một giải pháp tốt nhất. Bài viết cung cấp nhiều kinh nghiệm lập trình C++, cũng như cho thấy một ứng dụng thú vị nữa của các function object.

Nhận xét

Bài đăng phổ biến từ blog này

Kinh nghiệm tạo biểu đồ Use Case

PHÉP TOÁN XOR

Phần mềm hỗ trợ vẽ bản đồ tư duy trên máy tính

Power Designer 12.5