Hàm được truyền dưới dạng đối số mẫu


224

Tôi đang tìm các quy tắc liên quan đến việc chuyển các hàm mẫu C ++ làm đối số.

Điều này được hỗ trợ bởi C ++ như được hiển thị bằng một ví dụ ở đây:

#include <iostream>

void add1(int &v)
{
  v+=1;
}

void add2(int &v)
{
  v+=2;
}

template <void (*T)(int &)>
void doOperation()
{
  int temp=0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
  doOperation<add1>();
  doOperation<add2>();
}

Học về kỹ thuật này là khó, tuy nhiên. Googling cho "chức năng như một đối số mẫu" không dẫn đến nhiều. Và các mẫu C ++ cổ điển Hướng dẫn hoàn chỉnh đáng ngạc nhiên cũng không thảo luận về nó (ít nhất là không phải từ tìm kiếm của tôi).

Các câu hỏi tôi có là liệu đây có phải là C ++ hợp lệ không (hoặc chỉ là một số tiện ích mở rộng được hỗ trợ rộng rãi).

Ngoài ra, có cách nào để cho phép một functor có cùng chữ ký được sử dụng thay thế cho nhau với các hàm rõ ràng trong kiểu gọi mẫu này không?

Phần sau không hoạt động trong chương trình trên, ít nhất là trong Visual C ++ , vì cú pháp rõ ràng là sai. Thật tuyệt khi có thể chuyển đổi một hàm cho functor và ngược lại, tương tự như cách bạn có thể chuyển một con trỏ hàm hoặc functor cho thuật toán std :: sort nếu bạn muốn xác định một hoạt động so sánh tùy chỉnh.

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

Con trỏ đến một hoặc hai liên kết web hoặc một trang trong sách Mẫu C ++ sẽ được đánh giá cao!


Lợi ích của một chức năng như là đối số mẫu là gì? Loại trả về sẽ không được sử dụng một loại mẫu?
DaClown

Liên quan: một lambda không có hình ảnh chụp có thể phân rã thành một con trỏ hàm và bạn có thể chuyển nó dưới dạng tham số mẫu trong C ++ 17. Clang biên dịch nó ổn, nhưng gcc hiện tại (8.2) có lỗi và từ chối không chính xác vì nó "không có liên kết" ngay cả với -std=gnu++17. Tôi có thể sử dụng kết quả của toán tử chuyển đổi lambda constexpr không bị giam giữ C ++ 17 làm đối số kiểu con trỏ hàm không? .
Peter Cordes

Câu trả lời:


123

Vâng, nó là hợp lệ.

Đối với việc làm cho nó hoạt động với functor là tốt, giải pháp thông thường là một cái gì đó như thế này:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

mà bây giờ có thể được gọi là một trong hai:

doOperation(add2);
doOperation(add3());

Xem nó trực tiếp

Vấn đề với điều này là nếu nó làm cho trình biên dịch khó thực hiện cuộc gọi đến add2, vì tất cả các trình biên dịch đều biết rằng một loại con trỏ hàm void (*)(int &)đang được truyền tới doOperation. (Tuy nhiên add3, là một functor, có thể được inlined dễ dàng. Ở đây, trình biên dịch biết rằng một đối tượng kiểu add3được truyền cho hàm, có nghĩa là các chức năng để gọi là add3::operator(), chứ không phải chỉ là một số con trỏ hàm chưa biết.)


19
Bây giờ đây là một câu hỏi thú vị. Khi được thông qua một tên hàm, nó KHÔNG giống như có một con trỏ hàm liên quan. Đây là một hàm rõ ràng, được đưa ra tại thời gian biên dịch. Vì vậy, trình biên dịch biết chính xác những gì nó có tại thời gian biên dịch.
SPWorley

1
Có một lợi thế để sử dụng functor trên các con trỏ hàm. Functor có thể được cung cấp bên trong lớp và do đó cung cấp nhiều khả năng hơn cho trình biên dịch để tối ưu hóa (chẳng hạn như nội tuyến). Trình biên dịch sẽ được nhấn mạnh để tối ưu hóa một cuộc gọi qua một con trỏ hàm.
Martin York

11
Khi hàm được sử dụng trong một tham số mẫu, nó 'phân rã' thành một con trỏ tới hàm được truyền. Thật khó hiểu khi các mảng phân rã thành các con trỏ khi được truyền dưới dạng đối số cho các tham số. Tất nhiên, giá trị con trỏ được biết đến tại thời điểm biên dịch và phải trỏ đến một hàm có liên kết ngoài để trình biên dịch có thể sử dụng thông tin này cho mục đích tối ưu hóa.
CB Bailey

5
Chuyển nhanh đến vài năm sau, tình hình sử dụng các hàm làm đối số khuôn mẫu được cải thiện rất nhiều trong C ++ 11. Bạn không còn bị ràng buộc sử dụng Javaism như các lớp functor và có thể sử dụng ví dụ các hàm nội tuyến tĩnh làm đối số khuôn mẫu trực tiếp. Vẫn còn thua xa so với các macro Lisp của những năm 1970, nhưng C ++ 11 chắc chắn có tiến bộ tốt trong những năm qua.
pfalcon

5
vì c ++ 11 sẽ không tốt hơn nếu lấy hàm làm tham chiếu rvalue ( template <typename F> void doOperation(F&& f) {/**/}), vì vậy, ví dụ liên kết có thể vượt qua một biểu thức liên kết thay vì ràng buộc nó?
dùng1810087

70

Các tham số mẫu có thể được tham số hóa theo loại (tên chữ T) hoặc theo giá trị (int X).

Cách C ++ "truyền thống" tạo khuôn mẫu cho một đoạn mã là sử dụng functor - nghĩa là mã nằm trong một đối tượng và do đó đối tượng cung cấp loại mã duy nhất.

Khi làm việc với các chức năng truyền thống, kỹ thuật này không hoạt động tốt, vì thay đổi về loại không chỉ ra một chức năng cụ thể - thay vào đó chỉ xác định chữ ký của nhiều chức năng có thể. Vì thế:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

Không tương đương với trường hợp functor. Trong ví dụ này, do_op được khởi tạo cho tất cả các con trỏ hàm có chữ ký là int X (int, int). Trình biên dịch sẽ phải khá tích cực để hoàn toàn nội tuyến trong trường hợp này. (Tôi sẽ không loại trừ nó, vì tối ưu hóa trình biên dịch đã trở nên khá tiên tiến.)

Một cách để nói rằng mã này không hoàn toàn làm những gì chúng ta muốn là:

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

vẫn còn hợp pháp và rõ ràng điều này không được đưa vào. Để có được nội tuyến đầy đủ, chúng ta cần phải tạo mẫu theo giá trị, vì vậy hàm này có sẵn đầy đủ trong mẫu.

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

Trong trường hợp này, mỗi phiên bản tức thời của do_op được khởi tạo với một chức năng cụ thể đã có sẵn. Do đó, chúng tôi hy vọng mã cho do_op trông giống như "return a + b". (Lập trình viên Lisp, hãy ngừng cười!)

Chúng tôi cũng có thể xác nhận rằng điều này gần với những gì chúng tôi muốn bởi vì điều này:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

sẽ không biên dịch được. GCC nói: "lỗi: 'func_ptr' không thể xuất hiện trong biểu thức không đổi. Nói cách khác, tôi không thể mở rộng hoàn toàn do_op vì bạn đã không cung cấp cho tôi đủ thông tin tại thời điểm trình biên dịch để biết op của chúng tôi là gì.

Vì vậy, nếu ví dụ thứ hai thực sự hoàn toàn nội tuyến op của chúng tôi, và ví dụ thứ nhất thì không, mẫu nào tốt? Nó đang làm gì vậy Câu trả lời là: loại cưỡng chế. Đoạn riff này trong ví dụ đầu tiên sẽ hoạt động:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

Ví dụ đó sẽ hoạt động! (Tôi không cho rằng đó là C ++ tốt nhưng ...) Điều đã xảy ra là do_op đã được tạo khuôn mẫu xung quanh chữ ký của các hàm khác nhau và mỗi lần khởi tạo riêng biệt sẽ viết mã cưỡng chế loại khác nhau. Vì vậy, mã được khởi tạo cho do_op với fadd trông giống như:

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

Để so sánh, trường hợp theo giá trị của chúng tôi yêu cầu khớp chính xác trên các đối số hàm.


2
Xem stackoverflow.com/questions/13674935/ cấp cho câu hỏi tiếp theo để trả lời trực tiếp cho quan sát ở đây int c = do_op(4,5,func_ptr);"rõ ràng là không được nội tuyến".
Dan Nissenbaum

Xem ở đây để biết ví dụ về điều này đang được nội tuyến: stackoverflow.com/questions/4860762/ Có vẻ như các trình biên dịch đang trở nên khá thông minh trong những ngày này.
BigSandwich

15

Các con trỏ hàm có thể được truyền dưới dạng tham số mẫu và đây là một phần của C ++ tiêu chuẩn . Tuy nhiên, trong khuôn mẫu, chúng được khai báo và sử dụng như các hàm thay vì con trỏ tới hàm. Tại khởi tạo mẫu, người ta chuyển địa chỉ của hàm thay vì chỉ tên.

Ví dụ:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

Nếu bạn muốn truyền loại functor làm đối số mẫu:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

Một số câu trả lời vượt qua một ví dụ functor làm đối số:

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

Cách gần nhất bạn có thể có với giao diện thống nhất này với đối số mẫu là xác định do_ophai lần một lần với tham số không loại và một lần với tham số loại.

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

Thành thật mà nói, tôi thực sự mong đợi điều này không được biên dịch, nhưng nó hoạt động với tôi với gcc-4.8 và Visual Studio 2013.


9

Trong mẫu của bạn

template <void (*T)(int &)>
void doOperation()

Thông số T này là một tham số mẫu không loại. Điều này có nghĩa là hành vi của hàm khuôn mẫu thay đổi theo giá trị của tham số (phải được cố định tại thời gian biên dịch, hằng số con trỏ hàm là gì).

Nếu bạn muốn một cái gì đó hoạt động với cả các đối tượng chức năng và các tham số chức năng, bạn cần một khuôn mẫu được gõ. Tuy nhiên, khi bạn làm điều này, bạn cũng cần cung cấp một thể hiện đối tượng (thể hiện đối tượng hàm hoặc con trỏ hàm) cho hàm khi chạy.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

Có một số cân nhắc hiệu suất nhỏ. Phiên bản mới này có thể kém hiệu quả hơn với các đối số con trỏ hàm vì con trỏ hàm cụ thể chỉ bị hủy đăng ký và được gọi trong thời gian chạy trong khi mẫu con trỏ hàm của bạn có thể được tối ưu hóa (có thể là hàm gọi nội tuyến) dựa trên con trỏ hàm cụ thể được sử dụng. Các đối tượng hàm thường có thể được mở rộng rất hiệu quả với khuôn mẫu được gõ, mặc dù đặc biệt operator()được xác định hoàn toàn bởi loại đối tượng hàm.


1

Lý do ví dụ functor của bạn không hoạt động là bạn cần một cá thể để gọi operator().


0

Chỉnh sửa: Truyền toán tử dưới dạng tham chiếu không hoạt động. Để đơn giản, hiểu nó như là một con trỏ hàm. Bạn chỉ cần gửi con trỏ, không phải là một tài liệu tham khảo. Tôi nghĩ rằng bạn đang cố gắng để viết một cái gì đó như thế này.

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

. .

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
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.