2009-03-20 77 views
14

Tôi đã lập trình được vài năm và đã sử dụng các con trỏ hàm trong một số trường hợp nhất định. Những gì tôi muốn biết là khi nào nó phù hợp hoặc không sử dụng chúng vì lý do hiệu suất và tôi có nghĩa là trong bối cảnh của trò chơi, không phải phần mềm kinh doanh.Lợi ích của việc sử dụng các con trỏ chức năng

con trỏ chức năng được nhanh chóng, John Carmack sử dụng chúng để mức độ lạm dụng trong Quake và nguồn Doom mã và vì ông là một thiên tài :)

Tôi muốn sử dụng con trỏ hàm nhiều nhưng tôi muốn sử dụng nơi chúng thích hợp nhất.

Những ngày này sử dụng tốt nhất và thực tế nhất của các con trỏ hàm trong ngôn ngữ kiểu c hiện đại như C, C++, C# và Java, v.v.

+0

Viết một số mã bằng JavaScript trong đó các hàm là hạng nhất và bạn sẽ yêu. Chỉ là một ví dụ nhỏ: Tôi có một lớp học theo dõi một tập tin với các thiết lập cho những thay đổi. Khi bạn tạo lớp, bạn chuyển cho nó một ủy nhiệm để gọi mỗi khi tệp thay đổi. – core

Trả lời

23

Không có gì đặc biệt "nhanh" về các con trỏ hàm. Chúng cho phép bạn gọi một hàm được xác định trong thời gian chạy. Nhưng bạn có chính xác cùng một chi phí như bạn nhận được từ bất kỳ cuộc gọi chức năng khác (cộng với con trỏ bổ sung). Hơn nữa, kể từ khi chức năng gọi được xác định trong thời gian chạy, trình biên dịch thường có thể không inline gọi hàm như nó có thể bất cứ nơi nào khác. Như vậy, các con trỏ hàm có thể trong một số trường hợp thêm chậm hơn đáng kể so với cuộc gọi hàm thông thường.

Con trỏ hàm không có gì liên quan đến hiệu suất và không bao giờ được sử dụng để đạt được hiệu suất. Thay vào đó, chúng là một cái gật đầu rất nhỏ với mô hình lập trình chức năng, trong đó chúng cho phép bạn truyền một hàm xung quanh như tham số hoặc giá trị trả về trong một hàm khác. Quay lại đầu trang Cung cấp Phản hồi THÔNG TIN THÊM

Ví dụ đơn giản là hàm phân loại chung. Nó phải có một số cách để so sánh hai phần tử để xác định cách chúng được sắp xếp. Đây có thể là một con trỏ hàm được truyền cho hàm sắp xếp, và trên thực tế C++ std :: sort() có thể được sử dụng chính xác như thế. Nếu bạn yêu cầu nó sắp xếp các chuỗi của một kiểu không xác định toán tử nhỏ hơn, bạn phải truyền vào một con trỏ hàm mà nó có thể gọi để thực hiện so sánh.

Và điều này dẫn chúng ta đến một sự thay thế tuyệt vời. Trong C++, bạn không bị giới hạn đối với các con trỏ hàm. Bạn thường sử dụng functors thay vào đó - đó là, các lớp quá tải toán tử(), để chúng có thể được gọi là "như" chúng là các hàm. Functors có một vài lợi thế lớn so với con trỏ hàm:

  • Chúng mang lại sự linh hoạt hơn: chúng là các lớp chính thức, với hàm tạo, hàm hủy và biến thành viên. Chúng có thể duy trì trạng thái và chúng có thể hiển thị các hàm thành viên khác mà mã xung quanh có thể gọi.
  • Chúng nhanh hơn: không giống như con trỏ hàm, loại của chúng chỉ mã hóa chữ ký của hàm (một biến kiểu void (*)(int) có thể là bất kỳ hàm nào mất khoảng int và trả về. Chúng tôi không thể biết cái nào), Kiểu của functor mã hóa hàm chính xác cần được gọi (Vì một functor là một lớp, gọi nó là C, chúng ta biết rằng hàm cần gọi là, và sẽ luôn là, C :: operator()). Và điều này có nghĩa là trình biên dịch có thể nội tuyến cuộc gọi hàm. Đó là phép thuật làm cho chuẩn std :: sắp xếp nhanh như hàm phân loại được mã hóa bằng tay của bạn được thiết kế đặc biệt cho kiểu dữ liệu của bạn. Trình biên dịch có thể loại bỏ tất cả các chi phí của việc gọi một hàm do người dùng định nghĩa.
  • Chúng an toàn hơn: Có rất ít loại an toàn trong con trỏ hàm. Bạn không có đảm bảo rằng nó trỏ đến một chức năng hợp lệ. Nó có thể là NULL. Và hầu hết các vấn đề với con trỏ cũng áp dụng cho các con trỏ hàm. Chúng nguy hiểm và dễ bị lỗi.

Con trỏ hàm (trong C) hoặc functors (trong C++) hoặc đại biểu (trong C#) tất cả giải quyết cùng một vấn đề, với các mức độ sang trọng và linh hoạt khác nhau: Chúng cho phép bạn xử lý các hàm như giá trị hạng nhất, chuyển chúng xung quanh khi bạn thực hiện bất kỳ biến nào khác. Bạn có thể truyền chức năng cho một chức năng khác, và nó sẽ gọi hàm của bạn vào các thời điểm cụ thể (khi hết giờ, khi cửa sổ cần vẽ lại hoặc khi cần so sánh hai phần tử trong mảng của bạn)

Theo tôi biết (và tôi có thể sai, bởi vì tôi đã không làm việc với Java cho các lứa tuổi), Java không có một tương đương trực tiếp. Thay vào đó, bạn phải tạo một lớp, thực hiện một giao diện và định nghĩa một hàm (gọi nó là Execute(), ví dụ). Và sau đó thay vì gọi hàm do người dùng cung cấp (trong hình dạng của một con trỏ hàm, hàm functor hoặc delegate), bạn gọi hàm foo.Execute(). Tương tự như việc thực hiện C++ về nguyên tắc, nhưng không có tính tổng quát của các mẫu C++, và không có cú pháp hàm cho phép bạn xử lý các con trỏ hàm và hàm functors theo cùng một cách.

Vì vậy, đó là nơi bạn sử dụng các con trỏ hàm: Khi không có các lựa chọn thay thế phức tạp hơn (ví dụ: bạn bị mắc kẹt trong C), và bạn cần chuyển một hàm này sang hàm khác. Kịch bản phổ biến nhất là gọi lại. Bạn định nghĩa một hàm F mà bạn muốn hệ thống gọi khi X xảy ra. Vì vậy, bạn tạo một con trỏ hàm trỏ tới F và chuyển nó tới hệ thống được đề cập.

Vì vậy, thực sự, hãy quên John Carmack và đừng cho rằng bất cứ điều gì bạn thấy trong mã của mình sẽ làm cho mã của bạn trở nên tốt hơn nếu bạn sao chép nó. Ông đã sử dụng các con trỏ hàm vì các trò chơi mà bạn đề cập được viết bằng C, trong đó các lựa chọn thay thế cao không có sẵn, và không phải vì chúng là một thành phần ma thuật mà sự tồn tại duy nhất làm cho mã chạy nhanh hơn.

+1

"Chức năng con trỏ không có gì để làm với hiệu suất, và không bao giờ nên được sử dụng để đạt được hiệu suất." Trên thực tế chúng có thể được sử dụng để cải thiện hiệu suất, và do đó có thể functors/đại biểu vv Xem ví dụ trong câu trả lời của tôi ... tại sao điều này không bao giờ được thực hiện? Tôi không thấy những gì xấu về nó. – jheriko

+1

Đúng. Quan điểm của tôi đơn giản chỉ đơn thuần là gọi một con trỏ hàm thay vì gọi hàm không nhanh hơn, vì điều đó dường như là những gì OP ngụ ý. Bạn nói đúng, nếu bạn sử dụng nó để thay đổi logic của chương trình, nó cũng có thể ảnh hưởng đến hiệu năng. – jalf

+0

sau khi tôi nhận được một số tăng rep từ câu trả lời của tôi ... đọc lại đã cho tôi suy nghĩ, thực sự hiệu suất khác nhau tùy thuộc vào cơ chế gọi điện thoại chức năng, đôi khi quyết liệt - ví dụ trong C++ các cuộc gọi chức năng ảo và thừa kế ảo thêm các lớp bổ sung của sự vô hướng, đến mức bạn có thể thụ thai toàn bộ bộ nhớ cache. đó là mã xấu vì nhiều lý do khác mặc dù ... con trỏ hàm có thể triển khai cùng chức năng (thừa kế phức tạp), với chi phí bộ nhớ bổ sung nhưng chỉ để lại cuộc gọi động, ví dụ: nhấn vào hướng dẫn và lưu trữ dữ liệu một lần duy nhất, so với 2..n lần – jheriko

6

Bất cứ lúc nào bạn sử dụng trình xử lý sự kiện hoặc ủy nhiệm trong C#, bạn đang sử dụng con trỏ hàm hiệu quả.

Và không, chúng không phải là về tốc độ. Chức năng con trỏ là về sự tiện lợi.

Jonathan

5

Con trỏ hàm được sử dụng làm gọi lại trong nhiều trường hợp. Một sử dụng là một hàm so sánh trong các thuật toán sắp xếp. Vì vậy, nếu bạn đang cố gắng so sánh các đối tượng tùy chỉnh, bạn có thể cung cấp một con trỏ hàm cho hàm so sánh biết cách xử lý dữ liệu đó.

Điều đó nói rằng, tôi sẽ cung cấp một báo tôi nhận được từ một cựu giáo sư của tôi:

Hãy đối xử với tính năng C++ mới như bạn sẽ đối xử với một vũ khí tự động nạp trong một căn phòng đông đúc: không bao giờ sử dụng nó giống bởi vì nó trông rất tiện lợi. Chờ cho đến khi bạn hiểu hậu quả, đừng dễ thương, viết những gì bạn biết và biết bạn viết gì.

+0

haha, điều đó thật đúng! nhắc tôi về http://www.gotw.ca/publications/advice97.htm :) –

+0

Đó chính xác là những gì anh ta đang đề cập! Tuyệt vời. –

+0

"Viết những gì bạn biết" hoạt động nếu bạn làm việc một mình trong dự án. Nhưng nếu bạn giỏi ở mảng ** và không quen thuộc với STL thì sẽ không hay khi làm việc trong một nhóm. Những điều tương tự với C thuần túy giống như các hàm xung quanh mã trong dự án lớn. –

3

Chỉ nói về C#, nhưng con trỏ hàm được sử dụng trên C#. Các đại biểu và sự kiện (và Lambdas, vv) là tất cả các con trỏ hàm dưới mui xe, vì vậy gần như bất kỳ dự án C# nào cũng sẽ được riddled với các con trỏ hàm. Về cơ bản, mọi trình xử lý sự kiện, gần mọi truy vấn LINQ, vv - sẽ sử dụng các con trỏ hàm.

1

con trỏ hàm là nhanh

Trong bối cảnh những gì? So với?

Có vẻ như bạn chỉ muốn sử dụng các con trỏ hàm để sử dụng chúng. Điều đó sẽ rất tệ.

Một con trỏ tới một hàm thường được sử dụng như một trình xử lý sự kiện hoặc gọi lại.

+0

Nhanh chóng theo nghĩa là chúng có hiệu quả chuyển sang một phần khác của mã giúp bạn không quay trở lại phương pháp hiện tại và có thể rời khỏi vòng lặp của bạn và sau đó trở về sau. –

+0

Tôi thấy điểm của bạn mặc dù và đồng ý, không nên sử dụng chúng chỉ vì lợi ích của nó. –

+0

Các cuộc gọi chức năng thông thường cũng là "một bước nhảy tới một phần khác của mã của bạn". Có, đây là hành vi hữu ích, nhưng bạn thường nhận được nó bằng các cuộc gọi hàm đơn giản, không cần phải liên quan đến hàm con trỏ – jalf

5

Những ngày này cách sử dụng tốt nhất và thực tế nhất của các số nguyên trong ngôn ngữ kiểu c hiện đại là gì?

+0

Tôi xin lỗi nhưng tôi không làm theo? –

+0

Nếu bạn cần số nguyên, hãy sử dụng số nguyên. Nếu bạn cần một con trỏ hàm, sử dụng một con trỏ hàm. Cả hai (giống như tất cả các cấu trúc lập trình) chỉ là công cụ, không phải là đặc biệt nhanh hay huyền diệu theo bất kỳ cách nào. –

+0

Tôi, đối với một, hãy tận hưởng sự mỉa mai của bạn, thưa bạn. – Marcin

9

Chúng có thể hữu ích nếu bạn không biết chức năng được nền tảng đích của bạn hỗ trợ cho đến thời gian chạy (ví dụ: chức năng CPU, bộ nhớ khả dụng).Các giải pháp rõ ràng là để viết các chức năng như thế này:

int MyFunc() 
{ 
    if(SomeFunctionalityCheck()) 
    { 
    ... 
    } 
    else 
    { 
    ... 
    } 
} 

Nếu chức năng này được gọi là sâu bên trong của vòng quan trọng thì có lẽ nó tốt hơn để sử dụng một con trỏ hàm cho MyFunc:

int (*MyFunc)() = MyFunc_Default; 

int MyFunc_SomeFunctionality() 
{ 
    // if(SomeFunctionalityCheck()) 
    .. 
} 

int MyFunc_Default() 
{ 
    // else 
    ... 
} 

int MyFuncInit() 
{ 
    if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality; 
} 

Có công dụng khác tất nhiên, giống như callback functions, thực thi mã byte từ bộ nhớ hoặc để tạo ngôn ngữ thông dịch.

Để thực thi Intel compatible byte code trên Windows, có thể hữu ích cho thông dịch viên. Ví dụ, đây là một hàm stdcall trả về 42 (0x2A) được lưu trữ trong một mảng có thể được thực hiện:

code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); 
// mov eax, 42 
code[0] = 0x8b; 
code[1] = 0x2a; 
code[2] = 0x00; 
code[3] = 0x00; 
code[4] = 0x00; 
// ret 
code[5] = 0xc3; 
// this line executes the code in the byte array 
reinterpret_cast<unsigned int (_stdcall *)()>(code)(); 

... 

VirtualFree(code, 6, MEM_RELEASE); 

);

+2

Thực sự sâu sắc, cảm ơn cho ví dụ mã –

+0

Nên có chiến lược hoặc một số khác như mẫu thiết kế trang trí. Tất nhiên nếu chúng ta nói về C++. –

4

Trong mờ tối, trước tuổi C++, có một mẫu chung tôi dùng để xác định cấu trúc với một tập hợp các con trỏ hàm (thường) hoạt động trên cấu trúc đó theo một cách nào đó và cung cấp các hành vi cụ thể cho nó. Trong thuật ngữ C++, tôi chỉ xây dựng một vtable. Sự khác biệt là tôi có thể tác dụng phụ trong cấu trúc khi chạy để thay đổi hành vi của các đối tượng riêng lẻ khi cần thiết. Điều này mang lại một mô hình kế thừa phong phú hơn với chi phí ổn định và dễ gỡ lỗi. Tuy nhiên, chi phí lớn nhất là có chính xác một người có thể viết mã này một cách hiệu quả: tôi.

Tôi đã sử dụng rất nhiều trong khuôn khổ giao diện người dùng, cho phép tôi thay đổi cách thức đối tượng được vẽ, mục tiêu của lệnh, v.v. - trên thực tế - rất ít giao diện người dùng được cung cấp.

Việc quá trình này được chính thức hóa bằng ngôn ngữ OO tốt hơn theo mọi cách có ý nghĩa.

+0

Chính xác. Tôi đã sử dụng tài liệu tham khảo mã trong một số ngôn ngữ để mô phỏng OO. FYI: không phải tất cả các ngôn ngữ "đóng" các lớp học. Ruby, Javascript và Objective C cho phép bạn làm những gì bạn nói về việc thay đổi các triển khai trong thời gian chạy. – Roboprog

3

Con trỏ hàm là nỗ lực của người nghèo hoạt động. Bạn thậm chí có thể tạo ra một đối số có các con trỏ hàm làm cho một ngôn ngữ hoạt động, vì bạn có thể viết các hàm bậc cao hơn với chúng.

Không có đóng cửa và cú pháp dễ dàng, chúng sắp xếp tổng. Vì vậy, bạn có xu hướng sử dụng chúng ít hơn so với mong muốn. Chủ yếu cho các chức năng "gọi lại".

Đôi khi, thiết kế OO hoạt động xung quanh bằng cách sử dụng các hàm bằng cách thay thế tạo một loại giao diện toàn bộ để truyền vào hàm cần thiết.

C# có bao đóng, vì vậy các con trỏ hàm (thực sự lưu trữ một đối tượng để nó không chỉ là một hàm thô, mà còn là trạng thái đã nhập) có thể sử dụng được ở đó.

Chỉnh sửa Một trong các nhận xét cho biết cần có một trình diễn các hàm bậc cao hơn với các con trỏ hàm.Bất kỳ hàm nào lấy hàm gọi lại là hàm bậc cao hơn. Giống như, giả sử, EnumWindows:

BOOL EnumWindows(   
    WNDENUMPROC lpEnumFunc, 
    LPARAM lParam 
); 

Tham số đầu tiên là chức năng truyền vào, đủ dễ dàng. Nhưng vì không có bao đóng trong C, chúng ta nhận được tham số thứ hai đáng yêu này: "Chỉ định giá trị do ứng dụng xác định được chuyển tới hàm gọi lại." Giá trị do ứng dụng xác định đó cho phép bạn tự chuyển qua trạng thái chưa được giải nén để bù đắp cho việc thiếu đóng.

Khuôn khổ .NET cũng được thiết kế tương tự. Ví dụ: IAsyncResult .AsyncState: "Nhận đối tượng do người dùng xác định đủ điều kiện hoặc chứa thông tin về thao tác không đồng bộ". Vì IAR là tất cả những gì bạn nhận được khi gọi lại, không có đóng cửa, bạn cần một cách để chuyển một số dữ liệu vào trong op không đồng bộ để bạn có thể bỏ nó ra sau.

+0

Tôi tò mò vì sao mọi người bỏ phiếu này xuống -1. Có vẻ như một câu trả lời đúng (và được nghĩ ra). +1 từ đây rồi bù lại. – jalf

+0

+1, hoàn toàn đồng ý với jalf - WTF? –

+0

Tôi đã không downvote, nhưng có lẽ ông nên chứng minh làm thế nào để constuct chức năng đặt hàng cao hơn với fps? –

3

Có những trường hợp khi sử dụng các con trỏ hàm có thể tăng tốc quá trình xử lý. Các bảng công văn đơn giản có thể được sử dụng thay cho các câu lệnh chuyển đổi dài hoặc các chuỗi if-then-else.

2

Theo sự gia tăng cá nhân của tôi, họ có thể giúp bạn lưu các dòng mã quan trọng.

Hãy xem xét các điều kiện: {

switch(sample_var) 
{ 

case 0: 
      func1(<parameters>); 
      break; 

case 1: 
      func2(<parameters>); 
      break; 











up to case n: 
       funcn(<parameters>); 
       break; 

}

nơi Func1() ... funcn() là hàm với cùng trươc. gì chúng ta có thể làm là: Khai báo một mảng các con trỏ hàm arrFuncPoint chứa các địa chỉ của các chức năng Func1() để funcn()

Sau đó, trường hợp toàn bộ công tắc sẽ được thay thế bởi

* arrFuncPoint [sample_var];

Các vấn đề liên quan