Đại biểu C ++ là gì?


147

Ý tưởng chung của một đại biểu trong C ++ là gì? Chúng là gì, chúng được sử dụng như thế nào và chúng được sử dụng để làm gì?

Trước tiên tôi muốn tìm hiểu về chúng theo cách 'hộp đen', nhưng một chút thông tin về sự can đảm của những điều này cũng sẽ rất tuyệt.

Đây không phải là C ++ ở mức tinh khiết nhất hoặc sạch nhất, nhưng tôi nhận thấy rằng cơ sở mã nơi tôi làm việc có rất nhiều. Tôi hy vọng hiểu đủ về chúng, vì vậy tôi chỉ có thể sử dụng chúng và không phải đi sâu vào khuôn mẫu khủng khiếp lồng nhau khủng khiếp.

Hai bài viết của Dự án Code giải thích những gì tôi muốn nói nhưng không đặc biệt ngắn gọn:


4
Bạn đang nói về C ++ được quản lý theo .NET?
dasblinkenlight


5
delegatekhông phải là một tên phổ biến trong cách nói c ++. Bạn nên thêm một số thông tin vào câu hỏi để bao gồm bối cảnh mà bạn đã đọc nó. Lưu ý rằng mặc dù mô hình có thể phổ biến, các câu trả lời có thể khác nhau nếu bạn nói về đại biểu nói chung hoặc trong ngữ cảnh của C ++ CLI hoặc bất kỳ thư viện nào khác có triển khai cụ thể của đại biểu .
David Rodríguez - dribeas

6
chờ 2 năm ?;)
xếp hạng1

6
Vẫn đang chờ ...
Grimm The Opiner

Câu trả lời:


173

Bạn có một số lượng đáng kinh ngạc các lựa chọn để đạt được các đại biểu trong C ++. Đây là những cái mà tôi nghĩ đến.


Tùy chọn 1: functor:

Một đối tượng chức năng có thể được tạo bằng cách thực hiện operator()

struct Functor
{
     // Normal class/struct members

     int operator()(double d) // Arbitrary return types and parameter list
     {
          return (int) d + 1;
     }
};

// Use:
Functor f;
int i = f(3.14);

Tùy chọn 2: biểu thức lambda (chỉ C ++ 11 )

// Syntax is roughly: [capture](parameter list) -> return type {block}
// Some shortcuts exist
auto func = [](int i) -> double { return 2*i/1.15; };
double d = func(1);

Tùy chọn 3: con trỏ hàm

int f(double d) { ... }
typedef int (*MyFuncT) (double d);
MyFuncT fp = &f;
int a = fp(3.14);

Tùy chọn 4: con trỏ tới các hàm thành viên (giải pháp nhanh nhất)

Xem Đại biểu C ++ nhanh (trên Dự án mã ).

struct DelegateList
{
     int f1(double d) { }
     int f2(double d) { }
};

typedef int (DelegateList::* DelegateType)(double d);

DelegateType d = &DelegateList::f1;
DelegateList list;
int a = (list.*d)(3.14);

Tùy chọn 5: std :: chức năng

(hoặc boost::functionnếu thư viện tiêu chuẩn của bạn không hỗ trợ nó). Nó chậm hơn, nhưng nó là linh hoạt nhất.

#include <functional>
std::function<int(double)> f = [can be set to about anything in this answer]
// Usually more useful as a parameter to another functions

Tùy chọn 6: liên kết (sử dụng std :: bind )

Cho phép đặt trước một số tham số, thuận tiện để gọi hàm thành viên chẳng hạn.

struct MyClass
{
    int DoStuff(double d); // actually a DoStuff(MyClass* this, double d)
};

std::function<int(double d)> f = std::bind(&MyClass::DoStuff, this, std::placeholders::_1);
// auto f = std::bind(...); in C++11

Tùy chọn 7: mẫu

Chấp nhận bất cứ điều gì miễn là nó phù hợp với danh sách đối số.

template <class FunctionT>
int DoSomething(FunctionT func)
{
    return func(3.14);
}

2
Danh sách tuyệt vời, +1. Tuy nhiên, chỉ có hai người thực sự được coi là đại biểu ở đây - bắt giữ lambdas và đối tượng trở về std::bindvà cả hai thực sự làm điều tương tự, ngoại trừ lambdas không đa hình theo nghĩa là họ có thể chấp nhận các loại đối số khác nhau.
Xèo

1
@JN: Tại sao bạn khuyên không nên sử dụng các con trỏ hàm nhưng có vẻ ổn với việc sử dụng các con trỏ phương thức thành viên? Chúng giống hệt nhau!
Matthieu M.

1
@MatthieuM. : điểm công bằng. Tôi đã xem xét con trỏ chức năng là di sản, nhưng đó có lẽ chỉ là sở thích cá nhân của tôi.
JN

1
@Xeo: ý tưởng của tôi về một đại biểu là khá kinh nghiệm. Có lẽ tôi là đối tượng hỗn hợp quá nhiều đối tượng và đại biểu (đổ lỗi cho kinh nghiệm C # trước đây của tôi).
JN

2
@SirYakalot Một cái gì đó hoạt động như một hàm, nhưng có thể giữ trạng thái cùng một lúc và có thể được thao tác như bất kỳ biến nào khác. Ví dụ, một cách sử dụng là lấy một hàm có hai tham số và buộc tham số thứ nhất có giá trị nhất định tạo một hàm mới với một tham số duy nhất ( bindingtrong danh sách của tôi). Bạn không thể đạt được điều này với các con trỏ hàm.
JN

37

Một ủy nhiệm là một lớp bao bọc một con trỏ hoặc tham chiếu đến một thể hiện đối tượng, một phương thức thành viên của lớp đối tượng đó được gọi trên thể hiện đối tượng đó và cung cấp một phương thức để kích hoạt lệnh gọi đó.

Đây là một ví dụ:

template <class T>
class CCallback
{
public:
    typedef void (T::*fn)( int anArg );

    CCallback(T& trg, fn op)
        : m_rTarget(trg)
        , m_Operation(op)
    {
    }

    void Execute( int in )
    {
        (m_rTarget.*m_Operation)( in );
    }

private:

    CCallback();
    CCallback( const CCallback& );

    T& m_rTarget;
    fn m_Operation;

};

class A
{
public:
    virtual void Fn( int i )
    {
    }
};


int main( int /*argc*/, char * /*argv*/ )
{
    A a;
    CCallback<A> cbk( a, &A::Fn );
    cbk.Execute( 3 );
}

cung cấp một phương thức để kích hoạt cuộc gọi? đại biểu? làm sao? con trỏ hàm?
SirYakalot

Lớp ủy nhiệm sẽ đưa ra một phương thức giống như Execute()kích hoạt lệnh gọi hàm trên đối tượng kết thúc đại biểu.
Grimm The Opiner

4
Thay vì Thực thi, tôi rất khuyến nghị trong trường hợp của bạn ghi đè toán tử cuộc gọi - void CCallback :: toán tử () (int). Lý do cho điều này là trong lập trình chung, các đối tượng có thể gọi được dự kiến ​​sẽ được gọi như một hàm. o.Execute (5) sẽ không tương thích, nhưng o (5) sẽ phù hợp độc đáo như một đối số mẫu có thể gọi được. Lớp này cũng có thể được khái quát hóa, nhưng tôi giả sử bạn giữ nó đơn giản cho ngắn gọn (đó là một điều tốt). Khác với nitpick, đây là một lớp rất hữu ích.
David Peterson

17

Sự cần thiết phải triển khai đại biểu C ++ là một sự thay đổi lâu dài đối với cộng đồng C ++. Mọi lập trình viên C ++ đều thích có chúng, vì vậy cuối cùng họ sử dụng chúng bất chấp sự thật rằng:

  1. std::function() sử dụng các hoạt động heap (và ngoài tầm với cho lập trình nhúng nghiêm trọng).

  2. Tất cả các triển khai khác đều nhượng bộ về tính di động hoặc sự phù hợp tiêu chuẩn ở mức độ lớn hơn hoặc thấp hơn (vui lòng xác minh bằng cách kiểm tra các triển khai đại biểu khác nhau ở đây và trên bảng mã). Tôi vẫn chưa thấy một triển khai không sử dụng reinterpret_casts, "nguyên mẫu" của lớp Nested, hy vọng tạo ra các con trỏ hàm có cùng kích thước như được người dùng truyền vào, các thủ thuật biên dịch như khai báo trước, sau đó gõ lại rồi khai báo lại, lần này kế thừa từ một lớp khác hoặc các kỹ thuật mờ ám tương tự. Mặc dù đó là một thành tựu tuyệt vời cho những người thực hiện đã xây dựng nó, nó vẫn là một bằng chứng đáng buồn về cách C ++ phát triển.

  3. Chỉ hiếm khi được chỉ ra rằng, bây giờ hơn 3 phiên bản tiêu chuẩn C ++, các đại biểu đã không được giải quyết đúng đắn. (Hoặc thiếu các tính năng ngôn ngữ cho phép triển khai ủy nhiệm đơn giản.)

  4. Với cách các hàm lambda C ++ được định nghĩa theo tiêu chuẩn (mỗi lambda có ẩn danh, loại khác nhau), tình hình chỉ được cải thiện trong một số trường hợp sử dụng. Nhưng đối với trường hợp sử dụng các đại biểu trong API thư viện (DLL), chỉ riêng lambdas vẫn không sử dụng được. Kỹ thuật phổ biến ở đây, trước tiên là đóng gói lambda vào hàm std :: và sau đó chuyển nó qua API.


Tôi đã thực hiện một phiên bản gọi lại chung của Elbert Mai trong C ++ 11 dựa trên các ý tưởng khác được thấy ở nơi khác và dường như nó đã làm rõ hầu hết các vấn đề bạn nêu trong 2). Vấn đề dai dẳng duy nhất còn lại đối với tôi là tôi bị mắc kẹt khi sử dụng macro trong mã máy khách để tạo các đại biểu. Có lẽ có một cách kỳ diệu mẫu mà tôi chưa giải quyết được.
kert

3
Tôi đang sử dụng hàm std :: không có heap trong hệ thống nhúng và nó vẫn hoạt động.
prasad

1
std::functionkhông phải lúc nào cũng phân bổ linh hoạt
Các cuộc đua nhẹ nhàng trong quỹ đạo

3
Câu trả lời này mang nhiều ý nghĩa hơn là một câu trả lời thực tế
Các cuộc đua nhẹ nhàng trong quỹ đạo

7

Rất đơn giản, một đại biểu cung cấp chức năng cho cách con trỏ hàm NÊN hoạt động. Có nhiều hạn chế của con trỏ hàm trong C ++. Một đại biểu sử dụng một số sự khó chịu của mẫu hậu trường để tạo ra một kiểu con trỏ hàm kiểu lớp con trỏ hoạt động theo cách bạn có thể muốn.

tức là - bạn có thể đặt chúng trỏ đến một chức năng nhất định và bạn có thể chuyển chúng xung quanh và gọi chúng bất cứ khi nào và bất cứ nơi nào bạn muốn.

Có một số ví dụ rất hay ở đây:


4

Một tùy chọn cho các đại biểu trong C ++ không được đề cập ở đây là thực hiện kiểu C bằng cách sử dụng hàm ptr và đối số ngữ cảnh. Đây có lẽ là mô hình tương tự mà nhiều người hỏi câu hỏi này đang cố gắng tránh. Nhưng, mẫu này là di động, hiệu quả và có thể sử dụng được trong mã nhúng và mã hạt nhân.

class SomeClass
{
    in someMember;
    int SomeFunc( int);

    static void EventFunc( void* this__, int a, int b, int c)
    {
        SomeClass* this_ = static_cast< SomeClass*>( this__);

        this_->SomeFunc( a );
        this_->someMember = b + c;
    }
};

void ScheduleEvent( void (*delegateFunc)( void*, int, int, int), void* delegateContext);

    ...
    SomeClass* someObject = new SomeObject();
    ...
    ScheduleEvent( SomeClass::EventFunc, someObject);
    ...

1

Windows Runtime tương đương với một đối tượng chức năng trong C ++ tiêu chuẩn. Người ta có thể sử dụng toàn bộ hàm làm tham số (thực ra đó là con trỏ hàm). Nó chủ yếu được sử dụng kết hợp với các sự kiện. Các đại biểu đại diện cho một hợp đồng mà xử lý sự kiện thực hiện nhiều. Nó tạo điều kiện cho một con trỏ hàm có thể làm việc như thế nào.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.