ฟังก์ชันส่งผ่านเป็นอาร์กิวเมนต์แม่แบบ


224

ฉันกำลังมองหากฎที่เกี่ยวข้องกับการส่งผ่านแม่แบบ C ++ ทำหน้าที่เป็นอาร์กิวเมนต์

สิ่งนี้ได้รับการสนับสนุนโดย C ++ ดังที่แสดงในตัวอย่างที่นี่:

#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>();
}

อย่างไรก็ตามการเรียนรู้เกี่ยวกับเทคนิคนี้เป็นเรื่องยาก Googling สำหรับ "ฟังก์ชันเป็นอาร์กิวเมนต์แม่แบบ"ไม่ได้นำไปสู่อะไรมากมาย และแม่แบบ C ++ แบบคลาสสิกคู่มือฉบับสมบูรณ์ยังไม่ได้กล่าวถึงอย่างน่าประหลาดใจ (อย่างน้อยก็ไม่ใช่จากการค้นหาของฉัน)

คำถามที่ฉันมีคือไม่ว่าจะเป็น C ++ ที่ถูกต้อง (หรือเพียงบางส่วนขยายได้รับการสนับสนุนอย่างกว้างขวาง)

นอกจากนี้ยังมีวิธีให้ functor ที่มีลายเซ็นเดียวกันสามารถใช้แทนกันได้กับฟังก์ชั่นที่ชัดเจนในระหว่างการเรียกใช้เทมเพลตชนิดนี้หรือไม่?

ต่อไปนี้ไม่ทำงานในโปรแกรมข้างต้นอย่างน้อยในVisual C ++เนื่องจากไวยากรณ์ผิดอย่างเห็นได้ชัด มันจะเป็นการดีถ้าคุณสามารถสลับฟังก์ชั่นสำหรับ functor และในทางกลับกันคล้ายกับวิธีที่คุณสามารถส่งตัวชี้ฟังก์ชันหรือ functor ไปยังอัลกอริทึม std :: sort หากคุณต้องการกำหนดการดำเนินการเปรียบเทียบแบบกำหนดเอง

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

    doOperation<add3>();

ตัวชี้ไปยังเว็บลิงค์หรือสองหรือหน้าในหนังสือ C ++ เทมเพลตจะได้รับการชื่นชม!


ประโยชน์ของฟังก์ชั่นเป็นอาร์กิวเมนต์แม่แบบคืออะไร? ประเภทการส่งคืนจะไม่ใช้ประเภทเทมเพลตหรือไม่
DaClown

ที่เกี่ยวข้อง: แลมบ์ดาที่ไม่มีการจับภาพสามารถสลายตัวไปยังตัวชี้ฟังก์ชั่นและคุณสามารถผ่านมันเป็นเทมเพลตพารามิเตอร์ใน C ++ 17 เสียงดังกราวรวบรวมมัน ok แต่ gcc ปัจจุบัน (8.2) มีข้อผิดพลาดและไม่ถูกต้องปฏิเสธมันว่า "ไม่มีการเชื่อมโยง" -std=gnu++17แม้จะมี ฉันสามารถใช้ผลลัพธ์ของตัวดำเนินการแปลงแลมบ์ดา constexpr ที่ไม่มีตัวอักษร C ++ 17 เป็นเทมเพลตตัวชี้ฟังก์ชันที่ไม่ใช่อาร์กิวเมนต์ชนิดได้หรือไม่ .
Peter Cordes

คำตอบ:


123

ใช่มันถูกต้อง

สำหรับการทำให้มันใช้งานได้กับ functors เช่นกันวิธีแก้ปัญหาปกติคืออะไรเช่นนี้แทน:

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

ซึ่งสามารถเรียกได้ว่าเป็น:

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

ดูมันสด

ปัญหานี้ก็คือว่าถ้ามันทำให้ยุ่งยากสำหรับรวบรวมเพื่อ inline การเรียกร้องให้add2เนื่องจากทุกคอมไพเลอร์รู้ว่านั่นคือตัวชี้ประเภทฟังก์ชั่นจะถูกส่งผ่านไปยังvoid (*)(int &) doOperation(แต่add3เป็น functor สามารถ inline ได้อย่างง่ายดายที่นี่คอมไพเลอร์รู้ว่าประเภทของวัตถุadd3ถูกส่งผ่านไปยังฟังก์ชั่นซึ่งหมายความว่าฟังก์ชั่นที่จะเรียกเป็นadd3::operator()และไม่เพียงบางฟังก์ชั่นตัวชี้ที่ไม่รู้จัก)


19
ตอนนี้นี่เป็นคำถามที่น่าสนใจ เมื่อผ่านชื่อฟังก์ชั่นมันไม่เหมือนมีตัวชี้ฟังก์ชั่นที่เกี่ยวข้อง มันเป็นฟังก์ชั่นที่ชัดเจนที่ได้รับในเวลารวบรวม ดังนั้นคอมไพเลอร์รู้ดีว่ามันมีอะไรในเวลารวบรวม
SPWorley

1
มีข้อได้เปรียบในการใช้ functors แทนฟังก์ชันพอยน์เตอร์ functor สามารถนำไปใช้ในชั้นเรียนและทำให้ opertunity มากขึ้นในการรวบรวมเพื่อเพิ่มประสิทธิภาพ (เช่น inlining) คอมไพเลอร์จะกดยากที่จะเพิ่มประสิทธิภาพการโทรผ่านตัวชี้ฟังก์ชั่น
Martin York

11
เมื่อฟังก์ชั่นใช้ในพารามิเตอร์เทมเพลตมันจะ 'สลายตัว' เป็นตัวชี้ไปยังฟังก์ชั่นที่ผ่าน มันเป็นวิธีการที่อาร์เรย์สลายตัวเป็นพอยน์เตอร์เมื่อส่งผ่านเป็นอาร์กิวเมนต์ไปยังพารามิเตอร์ แน่นอนค่าตัวชี้เป็นที่รู้จักกันในเวลารวบรวมและจะต้องชี้ไปที่ฟังก์ชั่นที่มีการเชื่อมโยงภายนอกเพื่อให้คอมไพเลอร์สามารถใช้ข้อมูลนี้เพื่อวัตถุประสงค์ในการเพิ่มประสิทธิภาพ
CB Bailey

5
กรอไปข้างหน้าอย่างรวดเร็วไม่กี่ปีต่อมาสถานการณ์ที่มีการใช้ฟังก์ชันเป็นอาร์กิวเมนต์เทมเพลตได้รับการปรับปรุงอย่างมากใน C ++ 11 คุณไม่จำเป็นต้องใช้ Javaisms เช่นคลาส functor อีกต่อไปและสามารถใช้ฟังก์ชันอินไลน์แบบสแตติกเป็นอาร์กิวเมนต์เทมเพลตได้โดยตรง ยังห่างไกลเมื่อเทียบกับมาโคร Lisp ของปี 1970 แต่ C ++ 11 มีความก้าวหน้าที่ดีในช่วงหลายปีที่ผ่านมา
pfalcon

5
เนื่องจาก c ++ 11 จะดีกว่าไหมถ้าใช้ฟังก์ชั่นเป็น rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}) ดังนั้นการโยงเช่นสามารถผ่าน bind-expression แทนการผูกมันได้หรือไม่
user1810087

70

พารามิเตอร์เทมเพลตสามารถทำให้เป็นพารามิเตอร์ตามประเภท (typename T) หรือตามค่า (int X)

วิธี "ดั้งเดิม" ของ C ++ ในการสร้างเทมเพลตชิ้นส่วนของรหัสคือการใช้ functor ซึ่งก็คือรหัสนั้นอยู่ในวัตถุและวัตถุจึงให้รหัสประเภทที่ไม่ซ้ำกัน

เมื่อทำงานกับฟังก์ชั่นดั้งเดิมเทคนิคนี้ใช้งานไม่ได้เพราะการเปลี่ยนประเภทไม่ได้ระบุฟังก์ชั่นเฉพาะ - แต่มันระบุเฉพาะลายเซ็นของฟังก์ชั่นที่เป็นไปได้มากมาย ดังนั้น:

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);

ไม่เท่ากับกรณีของ functor ในตัวอย่างนี้ do_op ถูกสร้างอินสแตนซ์สำหรับตัวชี้ฟังก์ชันทั้งหมดที่มีลายเซ็นคือ int X (int, int) คอมไพเลอร์จะต้องค่อนข้างก้าวร้าวเพื่อให้อินไลน์กรณีนี้อย่างเต็มที่ (ฉันจะไม่ออกกฎเพราะการเพิ่มประสิทธิภาพของคอมไพเลอร์ได้ขั้นสูงสวย)

วิธีหนึ่งในการบอกว่ารหัสนี้ไม่ได้ทำในสิ่งที่เราต้องการคือ:

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

ยังคงถูกกฎหมายและชัดเจนว่านี่ไม่ได้รับการ inline เพื่อให้ได้อินไลน์แบบเต็มเราจะต้องเทมเพลตตามค่าดังนั้นฟังก์ชั่นนั้นมีอยู่ในเทมเพลต

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);

ในกรณีนี้แต่ละรุ่นที่สร้างอินสแตนซ์ของ do_op นั้นมีอินสแตนซ์ที่มีฟังก์ชั่นเฉพาะที่มีอยู่แล้ว ดังนั้นเราคาดหวังว่าโค้ดสำหรับ do_op จะมีลักษณะเหมือน "return a + b" (โปรแกรมเมอร์ Lisp หยุด smirking ของคุณ!)

เราสามารถยืนยันได้ว่าสิ่งนี้ใกล้เคียงกับสิ่งที่เราต้องการเพราะสิ่งนี้:

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

จะล้มเหลวในการรวบรวม GCC พูดว่า: "ข้อผิดพลาด: 'func_ptr' ไม่สามารถปรากฏในรูปแบบคงที่ในคำอื่น ๆ ฉันไม่สามารถขยาย do_op ได้อย่างเต็มที่เพราะคุณยังไม่ได้ให้ข้อมูลเพียงพอในเวลาที่คอมไพเลอร์รู้ว่า op ของเราคืออะไร

ดังนั้นถ้าตัวอย่างที่สองอินไลน์ซับใน op ของเราอย่างเต็มที่และอันแรกไม่ใช่เทมเพลตอะไรดี มันกำลังทำอะไรอยู่? คำตอบคือ: ประเภทการข่มขู่ riff ในตัวอย่างแรกจะใช้งานได้:

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);

ตัวอย่างนั้นจะใช้ได้! (ฉันไม่ได้บอกว่ามันดี C ++ แต่ ... ) สิ่งที่เกิดขึ้นคือ do_op ได้รับการเทมเพลตรอบลายเซ็นของฟังก์ชั่นต่าง ๆ และแต่ละอินสแตนซ์ที่แยกจากกันจะเขียนรหัสบีบบังคับประเภทต่าง ๆ ดังนั้นโค้ดที่สร้างอินสแตนซ์สำหรับ do_op ด้วย fadd ดูเหมือนว่า:

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.

โดยการเปรียบเทียบกรณีโดยค่าของเราต้องตรงกับข้อโต้แย้งฟังก์ชั่น


2
ดูstackoverflow.com/questions/13674935/…สำหรับคำถามติดตามผลในการตอบสนองโดยตรงต่อการสังเกตที่นี่ซึ่งint c = do_op(4,5,func_ptr);เป็น "ชัดเจนไม่ได้รับการ inline"
Dan Nissenbaum

ดูที่นี่สำหรับตัวอย่างของสิ่งนี้ถูก inline: stackoverflow.com/questions/4860762/ ......ดูเหมือนว่าคอมไพเลอร์จะฉลาดมากในวันนี้
BigSandwich

15

คำแนะนำการทำงานสามารถผ่านเป็นพารามิเตอร์แม่แบบและนี้เป็นส่วนหนึ่งของมาตรฐาน C ++ อย่างไรก็ตามในเทมเพลตจะมีการประกาศและใช้เป็นฟังก์ชันแทนที่จะใช้ตัวชี้ไปยังฟังก์ชัน ที่instantiationแม่แบบหนึ่งที่อยู่ของฟังก์ชั่นมากกว่าเพียงแค่ชื่อ

ตัวอย่างเช่น:

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);

หากคุณต้องการส่งประเภท functor เป็นอาร์กิวเมนต์แม่แบบ:

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);

หลายคำตอบผ่านตัวอย่างเช่น functor เป็นอาร์กิวเมนต์:

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);

สิ่งที่ใกล้เคียงที่สุดที่คุณสามารถเข้าถึงได้ด้วยชุดรูปแบบนี้คืออาร์กิวเมนต์เทมเพลตคือการกำหนดdo_opสองครั้งพร้อมพารามิเตอร์ที่ไม่ใช่ประเภทและอีกครั้งด้วยพารามิเตอร์ประเภท

// 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);

สุจริตผมจริงๆคาดว่านี้ไม่ได้ที่จะรวบรวม แต่มันทำงานให้ฉันกับ GCC-4.8 และ Visual Studio 2013


9

ในเทมเพลตของคุณ

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

พารามิเตอร์Tเป็นพารามิเตอร์เท็มเพลตที่ไม่ใช่ชนิด ซึ่งหมายความว่าพฤติกรรมของฟังก์ชั่นแม่แบบการเปลี่ยนแปลงกับค่าของพารามิเตอร์ (ซึ่งจะต้องได้รับการแก้ไขในเวลารวบรวมซึ่งเป็นค่าคงที่ตัวชี้ฟังก์ชั่น)

หากคุณต้องการบางสิ่งที่ทำงานได้กับทั้งฟังก์ชั่นวัตถุและพารามิเตอร์ฟังก์ชั่นคุณต้องพิมพ์เทมเพลต อย่างไรก็ตามเมื่อคุณทำเช่นนี้คุณจะต้องจัดเตรียมอินสแตนซ์ออบเจ็กต์ (เช่นฟังก์ชันออบเจกต์อินสแตนซ์หรือตัวชี้ฟังก์ชัน) ให้กับฟังก์ชัน ณ รันไทม์

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

มีข้อควรพิจารณาเกี่ยวกับประสิทธิภาพเล็กน้อย เวอร์ชันใหม่นี้อาจมีประสิทธิภาพน้อยลงด้วยอาร์กิวเมนต์ตัวชี้ฟังก์ชันเนื่องจากตัวชี้ฟังก์ชันเฉพาะนั้นถูกยกเลิกการลงทะเบียนและเรียกใช้ในขณะที่ใช้งานในขณะที่เทมเพลตของตัวชี้ฟังก์ชันของคุณสามารถปรับให้เหมาะสม วัตถุฟังก์ชั่นมักจะสามารถขยายได้อย่างมีประสิทธิภาพมากด้วยเทมเพลตที่พิมพ์แม้ว่าจะoperator()ถูกกำหนดโดยเฉพาะอย่างยิ่งโดยประเภทของวัตถุฟังก์ชั่น


1

เหตุผลตัวอย่าง functor operator()ของคุณไม่ได้ทำงานที่คุณต้องการเช่นการเรียก


0

แก้ไข: การส่งโอเปอเรเตอร์เป็นการอ้างอิงไม่ทำงาน เพื่อความง่ายเข้าใจมันเป็นตัวชี้ฟังก์ชั่น คุณเพิ่งส่งตัวชี้ไม่ใช่การอ้างอิง ฉันคิดว่าคุณกำลังพยายามเขียนสิ่งนี้

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;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.