Làm thế nào để đẩy dữ liệu ra một hàm xử lý nằm bên ngoài đối tượng trong C++

Mình có 1 yêu cầu thế này:

Có một đối tượng A sẽ tự đẩy một số dữ liệu ra một hàm nằm bên ngoài nó và chạy hàm đó để xử lý.
Tuy nhiên nó không thể biết hàm đó là hàm nào, tên hàm là gì, nằm trong đối tượng nào hoặc nằm ở đâu.

Mình sẽ ví dụ với ngôn ngữ rất gần gũi với C++ là C# và C.

C# có delegate:

public delegate Callback(int data); // kiểu delegate

class A{
    private data = 0;
    public Callback Called; // chỗ này khai báo delegate tên là Called;
    public void Call(){
        if(Called != null) Call(a);
    }
}

void CustomMethod(int data){
}

void Main(){
    A a = new A(); // khai báo một đối tượng từ class A.
    a.Called = CustomMethod; // Sẽ gán delegate trỏ đến một method nằm ngoài đối tượng a.
    a.Call(); // Gọi a.Call thì dữ liệu data trong đối tượng a sẽ được đẩy qua CustomMethod và hàm CustomMethod sẽ chạy để xử lý.
}

Ở trong C, ta có function pointer.

typedef void(*Callback)(int data); // khai báo 1 kiểu function pointer

Callback Called; // Khai báo một function pointer

void CustomMethod(int data){
}

int main(){
    Called = &CustomMethod; // chúng ta sẽ cho Called trỏ đến CustomMethod;
    Called(0); // và ta sẽ đẩy data vào CustomMethod thông qua con trỏ hàm Called. Và CustomMethod cũng sẽ chạy.
}

Đó, và bây giờ trong C++ chúng ta sẽ làm thế nào khi con trỏ hàm nằm trong A:

class A{
public:
    Callback Called;
    void Call(){
        // sẽ làm gì ở đây ?
    }
}

Và CustomMethod nằm bên ngoài A, ví dụ trong class B:

class B{
public:
    void CustomMethod(int data){
    }
}
int main(){
    A *a = new A();
    B *b = new B();
    C *c = new C();
    // chúng ta sẽ làm gì  tiếp theo ở đây để khi gọi a.Call(data) thì B->CustomMethod(data) sẽ chạy. 
    // hoặc là c->OrtherMothod(data) sẽ chạy.
    // chú ý là a hoàn toàn không biết về b và b->CustomMethod hoặc c/ c->OrtherMothod.
    // tất cả quá trình gán nằm ngoài sự kiểm soát của các đối tượng chứa function pointer và method.
}

Kính mời các cao nhân và các bạn.

Anh thử đọc về Command Pattern.

3 Likes

Anh có thể tạo class hoặc interface dạng:

public interface Function1<T, R> {
    public R call(T param);
}
public interface Function2<T1, T2, R> {
    public R call(T1 param1, T1 param2);
}

Khi sử dụng thì tạo anonymous class từ Function1 hoặc Function2.
Compiler hay dùng cách này để thực hiện anonymous function, lambla expression

1 Like

Ý bác muốn như thế này hả?

#include <iostream>
#include <cstdlib>
#include <functional>

class A
{
  public:
    using CallBack = std::function<void(int)>;
    CallBack Called;
  public:
    void Call()
    {
      this->Called(0);
    }
};

class B
{
  public:
    void CustomMethod(int data)
    {
      std::cout << data << std::endl;
    }
};

void CustomMethod(int data)
{
  std::cout << data << std::endl;
}

int main()
{
  A a;
  a.Called = CustomMethod;
  a.Call();
  
  B b;
  a.Called = std::bind(&B::CustomMethod, &b, 1);
  a.Call();
}

5 Likes

Hi @drgnz, @hungaya :
Theo mình hiểu thì ta sẽ khai báo 1 interface.
A sẽ có 1 vector hoặc 1 đối tượng loại interface.
B sẽ kế thừa interface này
Và ta sẽ gán đối tượng loại B vào đối tượng của a hoặc push b vào vector của a. Sau đó gọi ?

Nếu đúng là như vậy thì mình vừa test nó đã chạy nhưng sẽ phát sinh ra 2 vấn đề.
1, Nếu mình có nhiều loại Callback thì tương ứng sẽ có từng đó interface phải viết và có từng đó vector hoặc đối tượng với mỗi loại interface. Khi đó B cũng sẽ phải kế thừa rất nhiều interface tương ứng với các chức năng cần sử dung.
2, Tên hàm fix cứng không thay đổi được.

Có vẻ như nó đang khá going với yêu cầu của mình. Để mình kiểm tra thử.

Cụ thể mình chỉ muốn thằng a->Called trỏ tới thằng b->CustomMethod để từ trong a đẩy dữ liệu vào b->CustomMethod và gọi nó chạy.

Vậy thì std::bind là lựa chọn tốt rồi bác.
Tuy nhiên nó không tốt cho performance so với call qua lambda.

Hi Văn Dương.

  1. Đúng là như vậy. Bạn có thể thấy C# khai báo 1 cái delegate rồi khai báo 1 biến kiểu delegate, C thì khai báo 1 kiểu con trỏ hàm rồi khai báo 1 biến như vậy v.v.v…
  2. Tên hàm fix cứng nhưng code của các hàm là bạn tự viết lại và nó cũng tạo ra sự thống nhất cho code. VD bạn có các đối tượng gui kết thừa từ guiInterface bạn sẽ biết nó đều có phương thức enable và disable chứ không phải là buttonEnable, textViewEnable, editTextEnable v.v.v…

P/S Với yêu cầu của bạn thì bạn xem tính đa hình của OOP tuy nhiên tốt nhất bạn nên nêu rõ hơn yêu cầu vì trong OOP tính đa hình có rất nhiều biến thể.

Ví dụ về Android:

  • Android đều khai báo đầy đủ callback interface cho tất cả event: button touch, button long touch, edit text changed,… Chỉ cần nhìn vào tên interface là biết event thuộc đối tượng nào, loại event là gì (touch, changed, focus,…).
  • Với Android app đơn giản, có thể dùng Activity (Controller trong các Web Framework) implement tất cả interface, sinh ra vấn đề hay gặp trong Android là Massive Controller. Cách khắc phục là tạo class riêng, mỗi class implement 1 interface, các class đó được khởi tạo trong Activity. Sau này sửa logic 1 callback nào đó thì vào class tương ứng sửa, thay vì Activity class chung.
  • Về tên hàm, theo ý mình, tạo tên hàm tường minh, dễ đọc, chỉ làm 1 thao tác duy nhất, hơn là tạo 1 tên hàm chung xài cho nhiều callback khác nhau.
1 Like

Với tính đa hình thì C++ đáp ứng được việc chạy như mình đã trình bày.

Nhưng nếu mình có 50 loại sẽ rất vất vả (thực tế thì mình sẽ sử dung it nhất là hơn 10 loại).
Khi đó cả A phải kế thừa tới 50 loại interface khác nhau.
B thì sẽ phải kế thừa cái nào mà muốn dung tương ứng bởi vì không thể gộp chung nó vào một interface (nó sẽ sinh ra vấn đề khác).

Bạn có thể thấy bên C# thì lớp Window hoặc Form có đến hang chục loại event ( tương tứng với vector<interface_type>) tuy nhiên thì để sử dung nó các lớp khác không phải kế thừa bất cứ thứ gì từ Window và Form cả.

Việc fix tên thì cũng có thể bỏ qua vì nó không quan trọng lắm.

Vì vậy mình vẫn muốn đi tìm một cái gì đó đơn giản nhất có thể.

Hi Văn Dương.

  1. “Nhưng nếu mình có 50 loại sẽ rất vất vả (thực tế thì mình sẽ sử dung it nhất là hơn 10 loại).” là những loại gì ? bạn có thể nói rõ được không ?
  2. Khi xây dựng các đối tượng ui người ta cũng tạo sãn các interface để kết thừa rôi.
  3. Liên kết sự kiên thì mỗi fw có cách giải quyết khác nhau Android java thì dùng interface như bình thường, QT thì họ dùng macro để convert lại hoặc dùng contrỏ hàm
    http://www.dreamincode.net/forums/topic/123080-c-fltk-buttons-tutorial/

P/S Bạn có thể nêu rõ hơn yêu cầu được không ?

Đây là ví dụ với 1 window wpf. Nó sẽ có khoảng 120 event (sinh ra từ 120 loại delegate).
Ví dụ mình thiết kế class Window trong C++ mình cũng sẽ phải cần lượng callback tương ứng hoặc it bang 1 nửa thì cũng 50-60 loại rồi. Nếu mình sử dung đa hình, mình sẽ phải viết 50-60 interface và cho kế thừa hết toàn bộ.

Còn thực tế mình đang làm một class mô tả một thiết bị. Nó cũng sẽ có như:
Disconnect, Connected, Stopped, Release, Moving, Moved, PowerOff, PowerFail, Collision, OverHeat…
Nó sẽ chạy trên một thread riêng và chỉ thong báo trạng thái thay đổi ra bên ngoài qua các callback trỏ đến các hàm nằm tại nơi nào đó.

Về 2 chấm trên thì mình chưa hiểu ý bạn nói.

Còn chấm thứ 3 thì kiểu dung kế thừa này đang không giải quyết được này. Ví dụ:

class Program: public IMoveable{ // kế thừa IMoveable để sử dụng callback Moved của class A.
    A *a1 = new A(); // mình có 1 đối tượng loại A
    A *a2 = new A(); // và một đối tượng nữa cũng loại A.
    
    void Moved(){  // callback moved của đối tượng lớp A sẽ trỏ v
    }

    Program(){
        // bây giờ mình sẽ gán callback cho a1 và a2.
        // nhưng nó cùng sinh ra từ class A nên nó sẽ trỏ cùng đến 1 hàm vì tên hàm đã bị fix trong interface ???
        a1->Moved->push_back((IMoveable)this);
        a2->Moved->push_back((IMoveable)this);
    } 
}

Trong QT giải quyết việc này sẽ dung connect:

connect(object1, callbackname, object2, methodname);
// nhưng methodname của object2 có thể đặt tên bất kỳ để phân biệt chức năng như
// Image_Trigger
// Button_Trigger
// Label_Trigger

Anh có thể dùng kiểu này để giảm delegate, chỉ override các method cần thiết.

public abstract class ComponentDelegate {
     public boolean shouldConnect() { return true; }
     public void onPreConnect() { /* default impl */ }
     public void onConnect() { /* default impl */ }

     public void onStop() { /* default impl */ }
}

Tính năng thêm hay xoá delegate bằng += hay -= chỉ có C#
Ngôn ngữ khác chỉ có thay thế và delegate chỉ có 1 method duy nhất. Nên khi event được gọi, C# có thể chạy nhiều callback, nhưng các ngôn ngữ khác chỉ có 1method hoặc function được gọi.

Delegate trong C# có thể tương đương với như thế này trong C++

class A{
    IInterface *Moved;
}

Khi đó Moved chỉ lưu được 1 đối tượng, nó sẽ going delegate của C# chỉ gọi được một hàm.

C++ cũng có thể gọi được nhiều hàm nếu dung vector

class A{
    std::vector<IInterface*> Moved;
}

Khi đó thay vì +=/-= của C# thì chắc là push_back với pop_back của hoặc lệnh remove một element khỏi vector.

Nhưng mà vấn đề đó mình không nói. Chỉ nói về việc dung đa hình và kế thừa sẽ dính 2 vấn đề:

Lượng Interface phải viết và kế thừa tăng tỷ lệ với loại callback.
Nếu có 2 hoặc hiều object cùng loại sẽ đều phải dung chung 1 hàm vì tên hàm bị fix cứng trong interface.

Trong trường hợp bí quá chắc vẫn phải dung thôi nhưng không thích lắm vì những vấn đề phía trên.

Hi Văn Dương.

  1. Thực ra nó chỉ có vài loại thôi chứ không phải 50-60 loại
    “void Function(object sender, RoutedEventArgs e)” Còn cái nó gợi ý cho bạn đó là các biến được khai báo với kiểu đó.
  2. Về bài toán của bạn thì nên học oop và thiết kế cây kế thừa và cài đặt contrỏ hàm.
    VD với Disconnect, Connected thuộc class socket
class Socket {
public:
    using ConnecEvent = std::function<void(Socket *)>; //Dùng con trỏ hàm nếu muốn.
    ConnecEvent ConnecCallBack;
    ConnecEvent DisconnecCallBack;
public:
   ... Connect(...) {
       ConnecCallBack(this)
   }
   ... Disconnect(...) {
       DisconnecCallBack(this)
   }
};

Hi Văn Dương.

  1. Interface là kiểu dữ liệu chứ không phải biến. Bạn có thể viết 1 interface duy nhất sau đó khi goi thì ép kiểu.
  2. Chung một hàm -> có quan hệ với nhau vậy bạn có thể kế thừa code hoặc xem lại thiết kế.

P/S Bạn có thể tham khảo các thư viện GUI để biết thêm về các kĩ thuật này.

Em cũng không hiểu rõ ý anh nữa không.

Có 2 trường hợp cực hạn:

  • Interface chỉ có 1 callback, anh có thể tìm thấy trong Command Pattern, gặp vấn đề tăng tỉ lệ interface
  • 1 interface cho tất cả callback, giống std::function, gần C# vì C# so sánh theo return type và argument type

Nhưng kinh nghiệm em thì em ít dùng theo kiểu cực hạn cả. Ví dụ em đang làm lib bằng C++, em chỉ dùng abstract class để tạo interface, nhưng có hiện thực tất cả các method ( như ví dụ trên ), client chỉ extend các class cần thiết ( thông qua multiple inheritance của C++ ), và override các method cần thiết.

  1. Đó mình chỉ lấy ví dụ với Window C# thôi. Còn thực tế mình thiết kế một class mới thì sẽ có nhiều loại callback mình có thể nghĩ ra cho phù hợp với yêu cầu.

  2. Về OOP mình đã làm bên C# khá lâu (it nhất là 2 năm liên tục). Có thể C++ có phần khác nhưng không nhiều. Như vậy là mình đang nói về vấn đề gặp phải nếu dung interface để giải quyết vấn đề của mình. Mặc dù nó vẫn hoạt động tốt.

  3. Mình ví dụ luôn với phần GUI, hình như bạn chưa hiểu ý mình nói về vấn đề dung chung hàm.
    Mình sẽ có 1 cái nút nhấn nằm trên 1 Window, tất nhiên nó nằm trong 1 class rồi.
    Và 10 cái nút đó có Click thực hiện 10 việc khác nhau, việc đó được viết trong class chứa 10 button luôn.
    Do 10 button và cả Window đó cùng kế thừa 1 interface nên Click của nó đều phải gọi đến method Button_Click vì tên Button_Click đã được cố định trong interface mà chúng đã kế thừa.

Ví dụ với C#:
Tuy cùng sinh ra từ class Button nhưng mỗi button có thể gọi các hàm tên khác nhau để xử lý riêng.

button1.Click = Button1_Click;
button2.Click = Button2_Click;
button3.Click = Button3_Click;
//...
button10.Click = Button10_Click;

C++ dung interface hay abstractclass (sử dụng kế thừa và đa hình):
Nó sẽ phải gọi cùng một hàm mà interface hay abstract class chỉ định
Sau đó mới else if để xử lý từng cho đối tượng khác nhau.

button1...Click = ...Button_Click;
button2...Click = ...Button_Click;
button3...Click = ..Button_Click;
//...
button10...Click =.. Button_Click;

void Button_Click(...){
    if(button1) {}
    else if(button2){}
    .....
}

Mình nghĩ cái này sẽ hay hơn interface vì sẽ không phải viết nhiều interface. Còn lại phần tên hàm vẫn bị fix cứng. Bạn xem ví dụ trên nhé.

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?