C ++ functors และการใช้งานคืออะไร?


876

ฉันได้ยินเกี่ยวกับฟังก์ชั่นใน C ++ ต่อไป ใครสามารถให้ภาพรวมกับฉันเกี่ยวกับสิ่งที่พวกเขาและในกรณีใดพวกเขาจะมีประโยชน์?


4
หัวข้อนี้ได้รับการคุ้มครองเพื่อตอบคำถามนี้: stackoverflow.com/questions/317450/why-override-operator#317528
Luc Touraille

2
มันถูกใช้เพื่อสร้างการปิดใน C ++
ทองแดง

ดูคำตอบด้านล่างหากมีคนสงสัยว่ามันoperator()(...)หมายถึงอะไร: มันกำลังโหลดตัวดำเนินการ"การเรียกใช้ฟังก์ชัน"มากเกินไป มันเป็นเพียงการดำเนินการเกินพิกัดสำหรับ()ผู้ประกอบการ อย่าเข้าใจผิดว่าoperator()มีการเรียกใช้ฟังก์ชันที่เรียกว่าoperatorแต่เห็นว่าเป็นตัวดำเนินการตามปกติที่มีการโหลดเกินไวยากรณ์
zardosht

คำตอบ:


1041

functor เป็นเพียงคลาสที่กำหนดโอเปอเรเตอร์ () ที่ช่วยให้คุณสร้างวัตถุที่ "ดูเหมือน" ฟังก์ชั่น:

// this is a functor
struct add_x {
  add_x(int val) : x(val) {}  // Constructor
  int operator()(int y) const { return x + y; }

private:
  int x;
};

// Now you can use it like this:
add_x add42(42); // create an instance of the functor class
int i = add42(8); // and "call" it
assert(i == 50); // and it added 42 to its argument

std::vector<int> in; // assume this contains a bunch of values)
std::vector<int> out(in.size());
// Pass a functor to std::transform, which calls the functor on every element 
// in the input sequence, and stores the result to the output sequence
std::transform(in.begin(), in.end(), out.begin(), add_x(1)); 
assert(out[i] == in[i] + 1); // for all i

มีสองสิ่งที่ดีเกี่ยวกับฟังก์ชั่น หนึ่งคือว่าแตกต่างจากฟังก์ชั่นปกติพวกเขาสามารถมีสถานะ ตัวอย่างข้างต้นสร้างฟังก์ชั่นซึ่งจะเพิ่ม 42 ให้กับสิ่งที่คุณให้ แต่ค่า 42 นั้นไม่ใช่ฮาร์ดโค้ดมันถูกระบุเป็นอาร์กิวเมนต์ตัวสร้างเมื่อเราสร้างอินสแตนซ์ functor ของเรา ฉันสามารถสร้าง adder อีกอันซึ่งเพิ่ม 27 เพียงแค่เรียกนวกรรมิกที่มีค่าต่างกัน สิ่งนี้ทำให้พวกเขาปรับแต่งได้อย่างสวยงาม

ดังที่บรรทัดสุดท้ายแสดงขึ้นคุณมักจะส่งผ่าน functors เป็นอาร์กิวเมนต์ของฟังก์ชันอื่นเช่น std :: transform หรืออัลกอริธึมไลบรารีมาตรฐานอื่น ๆ คุณสามารถทำเช่นเดียวกันกับตัวชี้ฟังก์ชั่นปกติยกเว้นอย่างที่ฉันได้กล่าวไว้ข้างต้นฟังก์ชั่นสามารถ "ปรับแต่ง" ได้เพราะมันมีสถานะทำให้มันยืดหยุ่นมากขึ้น (ถ้าฉันต้องการใช้ตัวชี้ฟังก์ชั่นฉันต้องเขียนฟังก์ชัน ซึ่งเพิ่ม 1 เข้ากับอาร์กิวเมนต์ของ functor โดยทั่วไปและเพิ่มทุกอย่างที่คุณเตรียมไว้ด้วย) และอาจมีประสิทธิภาพมากกว่า ในตัวอย่างด้านบนคอมไพเลอร์รู้ว่าstd::transformควรเรียกใช้ฟังก์ชันใด add_x::operator()มันควรจะเรียก นั่นหมายความว่ามันสามารถอินไลน์การเรียกใช้ฟังก์ชันนั้นได้ และนั่นทำให้มันมีประสิทธิภาพเท่ากับว่าฉันได้เรียกฟังก์ชั่นในแต่ละค่าของเวกเตอร์ด้วยตนเอง

ถ้าฉันผ่านตัวชี้ฟังก์ชั่นแทนคอมไพเลอร์ก็ไม่สามารถดูได้ทันทีว่าฟังก์ชันนั้นชี้ไปที่ใดดังนั้นถ้ามันไม่ทำการปรับให้เหมาะสมที่ซับซ้อนทั่วโลกมันจะต้องทำการชี้ไปที่ตัวชี้ ณ รันไทม์


32
คุณช่วยอธิบายบรรทัดนี้ได้ไหม std :: transform (in.begin (), in.end (), out.begin (), add_x (1)); ทำไมคุณเขียนที่นั่น add_x ไม่ใช่ add42
Alecs

102
@Alecs ทั้งสองจะได้ทำงาน (แต่ผลจะแตกต่างกัน) ถ้าฉันใช้add42ฉันจะใช้นักแต่งเพลงที่ฉันสร้างไว้ก่อนหน้านี้และเพิ่ม 42 ลงในแต่ละค่า ด้วยการที่add_x(1)ฉันสร้างอินสแตนซ์ใหม่ของ functor หนึ่งซึ่งจะเพิ่ม 1 ให้กับแต่ละค่าเท่านั้น มันเป็นเพียงการแสดงให้เห็นว่าบ่อยครั้งที่คุณยกตัวอย่าง functor "ทันที" เมื่อคุณต้องการแทนที่จะสร้างมันขึ้นมาก่อนและเก็บมันไว้รอบ ๆ ก่อนที่คุณจะใช้มันเพื่ออะไรก็ตาม
jalf

8
@zadane แน่นอน พวกเขาเพียงแค่ต้องมีoperator()เพราะนั่นคือสิ่งที่ผู้โทรใช้เพื่อเรียกมัน อะไรอื่น functor มีฟังก์ชั่นสมาชิกก่อสร้างประกอบการและตัวแปรสมาชิกจะสมบูรณ์ขึ้นอยู่กับคุณ
jalf

4
@ rikimaru2013 ในการพูดจาของฟังก์ชั่นการเขียนโปรแกรมคุณถูกต้องฟังก์ชั่นยังเป็น functor แต่ในสำนวนของ C ++, functor เป็นชั้นเรียนที่ใช้เป็นฟังก์ชั่นโดยเฉพาะ คำศัพท์ถูกใช้ในช่วงต้น แต่การแบ่งเป็นความแตกต่างที่มีประโยชน์และยังคงมีอยู่ในปัจจุบัน หากคุณเริ่มอ้างถึงฟังก์ชั่นเป็น "ฟังก์ชั่น" ในบริบท C ++ คุณจะสับสนระหว่างการสนทนา
srm

6
มันเป็นคลาสหรือเป็นตัวอย่างของคลาส? ในแหล่งข้อมูลส่วนใหญ่add42จะเรียกว่า functor ไม่ใช่add_x(ซึ่งเป็นคลาสของ functor หรือเพียงแค่ระดับ functor) ฉันพบว่าคำศัพท์ที่สอดคล้องกันเพราะ functors จะเรียกว่าวัตถุฟังก์ชั่นไม่เรียนฟังก์ชั่น คุณช่วยชี้แจงประเด็นนี้ได้หรือไม่?
Sergei Tachenov

121

นอกจากนี้เล็กน้อย คุณสามารถใช้boost::functionเพื่อสร้างฟังก์ชั่นจากฟังก์ชั่นและวิธีการดังนี้:

class Foo
{
public:
    void operator () (int i) { printf("Foo %d", i); }
};
void Bar(int i) { printf("Bar %d", i); }
Foo foo;
boost::function<void (int)> f(foo);//wrap functor
f(1);//prints "Foo 1"
boost::function<void (int)> b(&Bar);//wrap normal function
b(1);//prints "Bar 1"

และคุณสามารถใช้ boost :: bind เพื่อเพิ่มสถานะให้กับ functor นี้ได้

boost::function<void ()> f1 = boost::bind(foo, 2);
f1();//no more argument, function argument stored in f1
//and this print "Foo 2" (:
//and normal function
boost::function<void ()> b1 = boost::bind(&Bar, 2);
b1();// print "Bar 2"

และมีประโยชน์มากที่สุดด้วยฟังก์ชั่น boost :: bind and boost :: คุณสามารถสร้าง functor จากวิธีเรียนได้จริง ๆ แล้วนี่คือตัวแทน:

class SomeClass
{
    std::string state_;
public:
    SomeClass(const char* s) : state_(s) {}

    void method( std::string param )
    {
        std::cout << state_ << param << std::endl;
    }
};
SomeClass *inst = new SomeClass("Hi, i am ");
boost::function< void (std::string) > callback;
callback = boost::bind(&SomeClass::method, inst, _1);//create delegate
//_1 is a placeholder it holds plase for parameter
callback("useless");//prints "Hi, i am useless"

คุณสามารถสร้างรายการหรือเวกเตอร์ของ functors

std::list< boost::function<void (EventArg e)> > events;
//add some events
....
//call them
std::for_each(
        events.begin(), events.end(), 
        boost::bind( boost::apply<void>(), _1, e));

มีปัญหาหนึ่งสำหรับทุกสิ่งนี้ข้อความแสดงข้อผิดพลาดของคอมไพเลอร์ไม่ใช่มนุษย์อ่านได้ :)


4
ไม่ควรoperator ()เป็นแบบสาธารณะในตัวอย่างแรกของคุณตั้งแต่คลาสเริ่มต้นเป็นแบบส่วนตัว?
NathanOliver

4
บางทีในบางประเด็นคำตอบนี้ควรได้รับการอัปเดตเนื่องจากตอนนี้ lambdas เป็นวิธีที่ง่ายที่สุดในการรับ functor จากอะไรก็ตาม
idclev 463035818

102

Functor เป็นวัตถุที่ทำหน้าที่เหมือนฟังก์ชั่น operator()โดยทั่วไประดับที่กำหนด

class MyFunctor
{
   public:
     int operator()(int x) { return x * 2;}
}

MyFunctor doubler;
int x = doubler(5);

ข้อได้เปรียบที่แท้จริงคือ functor สามารถถือรัฐ

class Matcher
{
   int target;
   public:
     Matcher(int m) : target(m) {}
     bool operator()(int x) { return x == target;}
}

Matcher Is5(5);

if (Is5(n))    // same as if (n == 5)
{ ....}

11
เพียงแค่ต้องเพิ่มว่ามันสามารถใช้งานได้เหมือนตัวชี้ฟังก์ชั่น
Martin York

7
@ LokiAstari - สำหรับผู้ที่ยังใหม่กับแนวคิดที่อาจทำให้เข้าใจผิดเล็กน้อย Funisors สามารถเป็น "ใช้เหมือน" แต่ไม่เสมอไป "แทนที่" ตัวชี้ฟังก์ชัน ตัวอย่างเช่นฟังก์ชั่นที่ใช้ตัวชี้ฟังก์ชั่นไม่สามารถใช้ functor ในสถานที่ของมันแม้ว่า functor มีข้อโต้แย้งที่เหมือนกันและค่าตอบแทนเป็นตัวชี้ฟังก์ชั่น แต่โดยมากเมื่อออกแบบฟังก์ชั่นเป็นที่ต้องการและในทางทฤษฎี "ทันสมัยมากขึ้น" ไป
MasonWinsauer

ทำไมวินาทีที่สองถึงกลับมาintเมื่อมันควรกลับมาbool? นี่คือ C ++ ไม่ใช่ C เมื่อคำตอบนี้ถูกเขียนขึ้นboolไม่มีอยู่จริง?
คดีฟ้องร้องกองทุนโมนิก้า

@QPaysTaxes ฉันเดาผิด ฉันอาจจะคัดลอกโค้ดจากตัวอย่างแรกและลืมเปลี่ยน ฉันซ่อมมันแล้ว
James Curran

1
@Riasat หาก Matcher อยู่ในไลบรารีการกำหนด Is5 () นั้นค่อนข้างง่าย และคุณสามารถสร้าง Is7 (), Is32 () ฯลฯ ได้อีกนั่นเป็นเพียงตัวอย่าง นักแสดงชายอาจมีความซับซ้อนมากกว่านี้
James Curran

51

ชื่อ "functor" ถูกใช้แบบดั้งเดิมในทฤษฎีหมวดหมู่มานานก่อนที่ C ++ จะปรากฏในที่เกิดเหตุ สิ่งนี้ไม่เกี่ยวกับแนวคิดของ C ++ ของ functor จะดีกว่าถ้าใช้ชื่อฟังก์ชั่นวัตถุแทนสิ่งที่เราเรียกว่า "functor" ใน C ++ นี่คือสิ่งที่ภาษาการเขียนโปรแกรมอื่นเรียกการสร้างที่คล้ายกัน

ใช้แทนฟังก์ชั่นธรรมดา:

คุณสมบัติ:

  • วัตถุฟังก์ชันอาจมีสถานะ
  • ฟังก์ชั่นวัตถุพอดีกับ OOP (มันทำหน้าที่เหมือนวัตถุอื่น ๆ ทุกคน)

จุดด้อย:

  • เพิ่มความซับซ้อนให้กับโปรแกรมมากขึ้น

ใช้แทนฟังก์ชั่นตัวชี้:

คุณสมบัติ:

  • ฟังก์ชั่นวัตถุมักจะมีการ inlined

จุดด้อย:

  • ฟังก์ชั่นวัตถุไม่สามารถสลับกับประเภทวัตถุฟังก์ชั่นอื่น ๆ ในช่วงรันไทม์ (อย่างน้อยถ้ามันขยายชั้นฐานบางอย่างซึ่งทำให้ค่าใช้จ่ายบางส่วน)

ใช้แทนฟังก์ชันเสมือน:

คุณสมบัติ:

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

จุดด้อย:

  • ฟังก์ชั่นวัตถุไม่สามารถสลับกับประเภทวัตถุฟังก์ชั่นอื่น ๆ ในช่วงรันไทม์ (อย่างน้อยถ้ามันขยายชั้นฐานบางอย่างซึ่งทำให้ค่าใช้จ่ายบางส่วน)

1
คุณสามารถอธิบายกรณีการใช้งานเหล่านี้ในตัวอย่างจริงได้หรือไม่? เราจะใช้ functors เป็น polymorphism และ adn funtion pointer ได้อย่างไร?
Milad Khajavi

1
จริง ๆ แล้วหมายความว่าอะไร functor ดำรงสถานะ
erogol

ขอบคุณสำหรับการชี้ให้เห็นว่าเราต้องการคลาสพื้นฐานเพื่อให้มีความหลากหลาย ฉันมีปัญหาที่ฉันต้องใช้ functor ในสถานที่เดียวกันเป็นตัวชี้ฟังก์ชั่นที่เรียบง่ายและวิธีเดียวที่ฉันพบคือการเขียนชั้นฐาน functor (เพราะฉันไม่สามารถใช้สิ่ง C ++ 11) ไม่แน่ใจว่าค่าใช้จ่ายนี้สมเหตุสมผลหรือไม่จนกว่าฉันจะอ่านคำตอบของคุณ
idclev 463035818

1
@Erogol functor foo(arguments)เป็นวัตถุที่เกิดขึ้นเพื่อสนับสนุนไวยากรณ์ ดังนั้นจึงสามารถมีตัวแปรได้ ตัวอย่างเช่นหากคุณมีupdate_password(string)ฟังก์ชั่นคุณอาจต้องการติดตามความถี่ที่เกิดขึ้น ด้วยนักแสดงที่สามารถเป็นprivate long timeตัวแทนของการประทับเวลามันเกิดขึ้นล่าสุด ด้วยตัวชี้ฟังก์ชั่นหรือฟังก์ชั่นธรรมดาคุณจะต้องใช้ตัวแปรนอกเนมสเปซของมันซึ่งเกี่ยวข้องโดยตรงกับเอกสารและการใช้งานแทนที่จะตาม definition.l
คดีของกองทุนโมนิก้า

4
⁺¹สำหรับการกล่าวถึงว่าชื่อนี้ถูกสร้างขึ้นมาโดยไม่มีเหตุผล ฉันเพิ่งค้นหาว่าอะไรคือความสัมพันธ์ระหว่างฟังก์ชั่นทางคณิตศาสตร์(หรือฟังก์ชั่นถ้าคุณต้องการ) functor กับ c ++
สวัสดีแองเจิล

41

เช่นเดียวกับที่คนอื่น ๆ ได้กล่าวถึงฟังก์ชั่น functor เป็นวัตถุที่ทำหน้าที่เหมือนฟังก์ชั่นคือมันเกินตัวดำเนินการเรียกฟังก์ชั่น

โดยทั่วไปจะใช้ Functors ในอัลกอริธึม STL มีประโยชน์เพราะสามารถเก็บสถานะก่อนและระหว่างการเรียกใช้ฟังก์ชันเช่นปิดในภาษาที่ใช้งานได้ ตัวอย่างเช่นคุณสามารถกำหนดMultiplyByfunctor ที่คูณอาร์กิวเมนต์ด้วยจำนวนที่ระบุ:

class MultiplyBy {
private:
    int factor;

public:
    MultiplyBy(int x) : factor(x) {
    }

    int operator () (int other) const {
        return factor * other;
    }
};

จากนั้นคุณสามารถส่งMultiplyByวัตถุไปยังอัลกอริทึมเช่น std :: แปรรูป

int array[5] = {1, 2, 3, 4, 5};
std::transform(array, array + 5, array, MultiplyBy(3));
// Now, array is {3, 6, 9, 12, 15}

ข้อดีอีกอย่างของ functor เหนือตัวชี้ไปยังฟังก์ชั่นคือการโทรสามารถ inline ในกรณีเพิ่มเติม หากคุณส่งตัวชี้ฟังก์ชันไปยังtransformเว้นแต่ว่าการโทรนั้นได้รับการ inline และคอมไพเลอร์รู้ว่าคุณมักจะส่งผ่านฟังก์ชั่นเดียวกันไปยังมันก็ไม่สามารถอินไลน์การโทรผ่านตัวชี้


37

สำหรับมือใหม่อย่างฉันในหมู่พวกเรา: หลังจากการวิจัยเล็กน้อยฉันก็พบว่าโค้ด jalf โพสต์ทำอะไร

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

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

รหัสของฉัน (ฉันพบว่าชื่อตัวแปรของ jalf สับสน)

class myFunctor
{ 
    public:
        /* myFunctor is the constructor. parameterVar is the parameter passed to
           the constructor. : is the initializer list operator. myObject is the
           private member object of the myFunctor class. parameterVar is passed
           to the () operator which takes it and adds it to myObject in the
           overloaded () operator function. */
        myFunctor (int parameterVar) : myObject( parameterVar ) {}

        /* the "operator" word is a keyword which indicates this function is an 
           overloaded operator function. The () following this just tells the
           compiler that () is the operator being overloaded. Following that is
           the parameter for the overloaded operator. This parameter is actually
           the argument "parameterVar" passed by the constructor we just wrote.
           The last part of this statement is the overloaded operators body
           which adds the parameter passed to the member object. */
        int operator() (int myArgument) { return myObject + myArgument; }

    private: 
        int myObject; //Our private member object.
}; 

หากสิ่งนี้ไม่ถูกต้องหรือผิดปกติเพียงแค่รู้สึกอิสระที่จะแก้ไขฉัน!


1
ตัวดำเนินการ () เรียกว่าตัวดำเนินการเรียกฟังก์ชัน ฉันเดาว่าคุณอาจเรียกมันว่าโอเปอเรเตอร์วงเล็บ
Gautam

4
"พารามิเตอร์นี้เป็นจริงอาร์กิวเมนต์" parameterVar "ผ่านตัวสร้างที่เราเพิ่งเขียนว่า"อืม?
การแข่งขัน Lightness ใน Orbit

22

functor เป็นฟังก์ชั่นขั้นสูงที่ใช้ฟังก์ชั่นเพื่อ parametrized (เช่นเทมเพลต) ประเภท มันเป็นลักษณะทั่วไปของฟังก์ชั่นการสั่งซื้อแผนที่ที่สูงขึ้น ตัวอย่างเช่นเราสามารถกำหนด functor สำหรับstd::vectorเช่นนี้:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::vector<U> fmap(F f, const std::vector<T>& vec)
{
    std::vector<U> result;
    std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);
    return result;
}

ฟังก์ชั่นนี้จะใช้เวลาstd::vector<T>และผลตอบแทนstd::vector<U>เมื่อได้รับฟังก์ชั่นFที่ใช้และผลตอบแทนT Uไม่จำเป็นต้องกำหนด functor บนประเภทคอนเทนเนอร์สามารถกำหนดประเภทเทมเพลทใด ๆ ได้เช่นกันรวมถึงstd::shared_ptr:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::shared_ptr<U> fmap(F f, const std::shared_ptr<T>& p)
{
    if (p == nullptr) return nullptr;
    else return std::shared_ptr<U>(new U(f(*p)));
}

นี่เป็นตัวอย่างง่ายๆที่แปลงประเภทเป็นdouble:

double to_double(int x)
{
    return x;
}

std::shared_ptr<int> i(new int(3));
std::shared_ptr<double> d = fmap(to_double, i);

std::vector<int> is = { 1, 2, 3 };
std::vector<double> ds = fmap(to_double, is);

มีกฎหมายสองฉบับที่ควรปฏิบัติตาม ประการแรกคือกฎหมายเอกลักษณ์ซึ่งระบุว่าหาก functor ได้รับฟังก์ชั่นตัวตนมันควรจะเหมือนกับการใช้ฟังก์ชั่นเอกลักษณ์กับประเภทที่fmap(identity, x)ควรจะเป็นเช่นidentity(x):

struct identity_f
{
    template<class T>
    T operator()(T x) const
    {
        return x;
    }
};
identity_f identity = {};

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<int> is1 = fmap(identity, is);
std::vector<int> is2 = identity(is);

กฎต่อไปคือกฎหมายการแต่งเพลงซึ่งระบุว่าหากผู้จัดทำฟังก์ชั่นของทั้งสองฟังก์ชั่นมันควรจะเป็นเช่นเดียวกับการใช้ functor สำหรับฟังก์ชั่นแรกและจากนั้นอีกครั้งสำหรับฟังก์ชั่นที่สอง ดังนั้นfmap(std::bind(f, std::bind(g, _1)), x)ควรเป็นเช่นเดียวกับfmap(f, fmap(g, x)):

double to_double(int x)
{
    return x;
}

struct foo
{
    double x;
};

foo to_foo(double x)
{
    foo r;
    r.x = x;
    return r;
}

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<foo> is1 = fmap(std::bind(to_foo, std::bind(to_double, _1)), is);
std::vector<foo> is2 = fmap(to_foo, fmap(to_double, is));

2
บทความยืนยันว่า functor ควรใช้อย่างถูกต้องสำหรับความหมายนี้ (ดูen.wikipedia.org/wiki/Functor ) และการใช้งานสำหรับวัตถุฟังก์ชั่นเป็นเพียงเลอะเทอะ: jackieokay.com/2017/01/26/functors.htmlมัน อาจจะสายเกินไปสำหรับการที่แม้ว่ากำหนดจำนวนคำตอบที่นี่ที่พิจารณาเฉพาะความหมายวัตถุฟังก์ชัน
armb

2
คำตอบนี้ควรเป็นคำตอบที่> 700 Upvotes ในฐานะที่เป็นคนรู้จัก Haskell ดีกว่า C ++ ภาษา C ++ lingua ทำให้ฉันประหลาดใจตลอดเวลา
mschmidt

หมวดหมู่ทฤษฎีและ C ++ นี่เป็นบัญชี SO ลับของ Bartosz Milewski หรือไม่
Mateen Ulhaq

1
มันอาจจะเป็นประโยชน์ที่จะสรุปกฎหมาย functor ในสัญกรณ์มาตรฐานและfmap(id, x) = id(x) fmap(f ◦ g, x) = fmap(f, fmap(g, x))
Mateen Ulhaq

@mschmidt ในขณะที่ functor ก็หมายถึงสิ่งนี้เช่นกัน C ++ โอเวอร์โหลดชื่อให้มีความหมายเช่นเดียวกับ "function object"
4322

9

นี่เป็นสถานการณ์จริงที่ฉันถูกบังคับให้ใช้ Functor เพื่อแก้ปัญหาของฉัน:

ฉันมีชุดฟังก์ชั่น (พูด, 20 รายการ) และพวกมันเหมือนกันทุกอย่างยกเว้นแต่ละสายจะมีฟังก์ชั่นเฉพาะที่แตกต่างกันในจุดเฉพาะ 3 จุด

นี่คือของเสียที่น่าเหลือเชื่อและการทำสำเนารหัส ปกติฉันจะผ่านตัวชี้ฟังก์ชั่นและเรียกมันว่าใน 3 จุด (ดังนั้นรหัสจะต้องปรากฏเพียงครั้งเดียวแทนที่จะเป็นยี่สิบครั้ง)

แต่แล้วฉันก็รู้ว่าในแต่ละกรณีฟังก์ชั่นเฉพาะนั้นต้องการโปรไฟล์พารามิเตอร์ที่แตกต่างอย่างสิ้นเชิง! บางครั้ง 2 พารามิเตอร์บางครั้ง 5 พารามิเตอร์ ฯลฯ

วิธีแก้ปัญหาอื่นก็คือให้มีคลาสพื้นฐานโดยที่ฟังก์ชั่นเฉพาะนั้นเป็นวิธีการแทนที่ในคลาสที่ได้รับ แต่ฉันต้องการที่จะสร้างทั้งหมดของมรดกนี้เพียงเพื่อให้ฉันสามารถผ่านตัวชี้ฟังก์ชั่น ????

การแก้ไข: ดังนั้นสิ่งที่ฉันทำคือฉันสร้างคลาส wrapper ("Functor") ซึ่งสามารถเรียกใช้ฟังก์ชันใด ๆ ที่ฉันต้องการเรียก ฉันตั้งค่าล่วงหน้า (ด้วยพารามิเตอร์ ฯลฯ ) แล้วฉันผ่านมันแทนการใช้ตัวชี้ฟังก์ชั่น ตอนนี้โค้ดที่เรียกสามารถเรียก Functor โดยไม่รู้ว่าเกิดอะไรขึ้นภายใน มันสามารถเรียกได้หลายครั้ง (ฉันต้องการโทร 3 ครั้ง)


นั่นเป็น - ตัวอย่างที่ใช้งานได้จริงที่ Functor กลายเป็นโซลูชันที่ชัดเจนและง่ายซึ่งช่วยให้ฉันลดการทำซ้ำรหัสจาก 20 ฟังก์ชันเป็น 1


3
หาก functor ของคุณเรียกว่าฟังก์ชั่นเฉพาะที่แตกต่างกันและฟังก์ชั่นอื่น ๆ เหล่านี้แตกต่างกันไปตามจำนวนของพารามิเตอร์ที่พวกเขายอมรับนั่นหมายความว่า functor ของคุณยอมรับจำนวนตัวแปรของข้อโต้แย้งสำหรับการส่งไปยังฟังก์ชันอื่น ๆ
johnbakers

4
คุณสามารถกรุณาอธิบายสถานการณ์ข้างต้นโดยการอ้างบางส่วนของรหัสบางผมใหม่ไปที่ C ++ ต้องการที่จะเข้าใจแนวคิดนี้ ..
sanjeev

3

นอกจากจะใช้ในการโทรกลับ C ++ functors ยังสามารถช่วยให้Matlabชื่นชอบสไตล์การเข้าถึง คลาสเมทริกซ์ มีเป็นตัวอย่าง


นี่ (ตัวอย่างเมทริกซ์) เป็นการใช้แบบธรรมดาoperator()แต่ไม่ได้ใช้คุณสมบัติวัตถุฟังก์ชั่น
Renardesque

3

Like ซ้ำแล้วซ้ำอีก functors เป็นคลาสที่สามารถถือได้ว่าเป็นฟังก์ชั่น (ตัวดำเนินการเกิน ())

สิ่งเหล่านี้มีประโยชน์มากที่สุดสำหรับสถานการณ์ที่คุณต้องการเชื่อมโยงข้อมูลบางอย่างกับการโทรซ้ำหรือล่าช้าไปยังฟังก์ชัน

ตัวอย่างเช่นรายการที่เชื่อมโยงของ functors สามารถนำมาใช้ในการสร้างระบบ coroutine แบบซิงโครนัสแบบ low-overhead ขั้นพื้นฐานตัวแจกจ่ายงานหรือการแยกวิเคราะห์ไฟล์แบบอินเตอร์รัปต์ได้ ตัวอย่าง:

/* prints "this is a very simple and poorly used task queue" */
class Functor
{
public:
    std::string output;
    Functor(const std::string& out): output(out){}
    operator()() const
    {
        std::cout << output << " ";
    }
};

int main(int argc, char **argv)
{
    std::list<Functor> taskQueue;
    taskQueue.push_back(Functor("this"));
    taskQueue.push_back(Functor("is a"));
    taskQueue.push_back(Functor("very simple"));
    taskQueue.push_back(Functor("and poorly used"));
    taskQueue.push_back(Functor("task queue"));
    for(std::list<Functor>::iterator it = taskQueue.begin();
        it != taskQueue.end(); ++it)
    {
        *it();
    }
    return 0;
}

/* prints the value stored in "i", then asks you if you want to increment it */
int i;
bool should_increment;
int doSomeWork()
{
    std::cout << "i = " << i << std::endl;
    std::cout << "increment? (enter the number 1 to increment, 0 otherwise" << std::endl;
    std::cin >> should_increment;
    return 2;
}
void doSensitiveWork()
{
     ++i;
     should_increment = false;
}
class BaseCoroutine
{
public:
    BaseCoroutine(int stat): status(stat), waiting(false){}
    void operator()(){ status = perform(); }
    int getStatus() const { return status; }
protected:
    int status;
    bool waiting;
    virtual int perform() = 0;
    bool await_status(BaseCoroutine& other, int stat, int change)
    {
        if(!waiting)
        {
            waiting = true;
        }
        if(other.getStatus() == stat)
        {
            status = change;
            waiting = false;
        }
        return !waiting;
    }
}

class MyCoroutine1: public BaseCoroutine
{
public:
    MyCoroutine1(BaseCoroutine& other): BaseCoroutine(1), partner(other){}
protected:
    BaseCoroutine& partner;
    virtual int perform()
    {
        if(getStatus() == 1)
            return doSomeWork();
        if(getStatus() == 2)
        {
            if(await_status(partner, 1))
                return 1;
            else if(i == 100)
                return 0;
            else
                return 2;
        }
    }
};

class MyCoroutine2: public BaseCoroutine
{
public:
    MyCoroutine2(bool& work_signal): BaseCoroutine(1), ready(work_signal) {}
protected:
    bool& work_signal;
    virtual int perform()
    {
        if(i == 100)
            return 0;
        if(work_signal)
        {
            doSensitiveWork();
            return 2;
        }
        return 1;
    }
};

int main()
{
     std::list<BaseCoroutine* > coroutineList;
     MyCoroutine2 *incrementer = new MyCoroutine2(should_increment);
     MyCoroutine1 *printer = new MyCoroutine1(incrementer);

     while(coroutineList.size())
     {
         for(std::list<BaseCoroutine *>::iterator it = coroutineList.begin();
             it != coroutineList.end(); ++it)
         {
             *it();
             if(*it.getStatus() == 0)
             {
                 coroutineList.erase(it);
             }
         }
     }
     delete printer;
     delete incrementer;
     return 0;
}

แน่นอนตัวอย่างเหล่านี้ไม่ได้มีประโยชน์ในตัวเอง พวกเขาแสดงให้เห็นเพียงว่า functors จะมีประโยชน์ได้อย่างไร functors นั้นมีพื้นฐานและไม่ยืดหยุ่นและสิ่งนี้ทำให้พวกมันมีประโยชน์น้อยลงตัวอย่างเช่นสิ่งที่บูสต์มอบให้


2

มีการใช้ Functors ใน gtkmm เพื่อเชื่อมต่อปุ่ม GUI กับฟังก์ชั่นหรือวิธีการ C ++


หากคุณใช้ไลบรารี pthread เพื่อสร้างแอปพลิเคชันของคุณแบบมัลติเธรด Functors สามารถช่วยคุณได้
ในการเริ่มเธรดหนึ่งในข้อโต้แย้งของpthread_create(..)คือตัวชี้ฟังก์ชั่นที่จะดำเนินการบนเธรดของเขาเอง
แต่มีความไม่สะดวกอย่างหนึ่ง ตัวชี้นี้ไม่สามารถเป็นตัวชี้ไปยังวิธีการเว้นแต่ว่ามันเป็นวิธีการคงหรือจนกว่าคุณจะระบุระดับของมันclass::methodเช่น และอีกอย่างอินเทอร์เฟซของวิธีการของคุณสามารถ:

void* method(void* something)

ดังนั้นคุณจึงไม่สามารถเรียกใช้ (ในวิธีที่ชัดเจนง่าย) วิธีการจากชั้นเรียนของคุณในหัวข้อโดยไม่ต้องทำอะไรพิเศษ

วิธีที่ดีมากในการจัดการกับเธรดใน C ++ คือการสร้างThreadคลาสของคุณเอง หากคุณต้องการเรียกใช้เมธอดจากMyClassคลาสสิ่งที่ฉันทำคือเปลี่ยนเมธอดเหล่านั้นเป็นFunctorคลาสที่ได้รับมา

นอกจากนี้Threadชั้นจะมีวิธีการนี้: ตัวชี้ไปยังวิธีการนี้จะใช้เป็นอาร์กิวเมนต์เรียกstatic void* startThread(void* arg)
pthread_create(..)และสิ่งที่startThread(..)ควรได้รับใน arg คือการvoid*อ้างอิง casted กับอินสแตนซ์ในกองของFunctorคลาสที่ได้รับซึ่งจะถูกส่งกลับไปFunctor*เมื่อดำเนินการแล้วเรียกว่าเป็นrun()วิธีการ


2

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

สมมติว่าวิธีการของคุณมีลายเซ็น:

int CTask::ThreeParameterTask(int par1, int par2, int par3)

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

หมายเหตุ - นี่น่าเกลียดและอาจเป็นไปได้ว่าคุณสามารถใช้ตัวช่วยเพิ่มการผูกเป็นต้น แต่ถ้าคุณไม่สามารถหรือไม่ต้องการนี่เป็นวิธีหนึ่ง

// a template class for converting a member function of the type int        function(int,int,int)
//to be called as a function object
template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
class mem_fun3_t
{
  public:
explicit mem_fun3_t(_Ret (_Class::*_Pm)(_arg1,_arg2,_arg3))
    :m_Ptr(_Pm) //okay here we store the member function pointer for later use
    {}

//this operator call comes from the bind method
_Ret operator()(_Class *_P, _arg1 arg1, _arg2 arg2, _arg3 arg3) const
{
    return ((_P->*m_Ptr)(arg1,arg2,arg3));
}
private:
_Ret (_Class::*m_Ptr)(_arg1,_arg2,_arg3);// method pointer signature
};

นอกจากนี้เราต้องการเมธอดตัวช่วย mem_fun3 สำหรับคลาสข้างต้นเพื่อช่วยในการโทร

template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3> mem_fun3 ( _Ret (_Class::*_Pm)          (_arg1,_arg2,_arg3) )
{
  return (mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3>(_Pm));

}

ตอนนี้เพื่อผูกพารามิเตอร์เราต้องเขียนฟังก์ชั่นเครื่องผูก ดังนั้นที่นี่มันไป:

template<typename _Func,typename _Ptr,typename _arg1,typename _arg2,typename _arg3>
class binder3
{
public:
//This is the constructor that does the binding part
binder3(_Func fn,_Ptr ptr,_arg1 i,_arg2 j,_arg3 k)
    :m_ptr(ptr),m_fn(fn),m1(i),m2(j),m3(k){}

 //and this is the function object 
 void operator()() const
 {
        m_fn(m_ptr,m1,m2,m3);//that calls the operator
    }
private:
    _Ptr m_ptr;
    _Func m_fn;
    _arg1 m1; _arg2 m2; _arg3 m3;
};

และฟังก์ชั่นตัวช่วยใช้คลาส binder3 - bind3:

//a helper function to call binder3
template <typename _Func, typename _P1,typename _arg1,typename _arg2,typename _arg3>
binder3<_Func, _P1, _arg1, _arg2, _arg3> bind3(_Func func, _P1 p1,_arg1 i,_arg2 j,_arg3 k)
{
    return binder3<_Func, _P1, _arg1, _arg2, _arg3> (func, p1,i,j,k);
}

ตอนนี้เราต้องใช้สิ่งนี้กับคลาสคำสั่ง ใช้ typedef ต่อไปนี้:

typedef binder3<mem_fun3_t<int,T,int,int,int> ,T* ,int,int,int> F3;
//and change the signature of the ctor
//just to illustrate the usage with a method signature taking more than one parameter
explicit Command(T* pObj,F3* p_method,long timeout,const char* key,
long priority = PRIO_NORMAL ):
m_objptr(pObj),m_timeout(timeout),m_key(key),m_value(priority),method1(0),method0(0),
method(0)
{
    method3 = p_method;
}

นี่คือวิธีที่คุณเรียกว่า:

F3 f3 = PluginThreadPool::bind3( PluginThreadPool::mem_fun3( 
      &CTask::ThreeParameterTask), task1,2122,23 );

หมายเหตุ: f3 (); จะเรียกใช้เมธอด task1-> ThreeParameterTask (21,22,23);

บริบททั้งหมดของรูปแบบนี้ที่ลิงค์ต่อไปนี้


2

ข้อได้เปรียบที่ยิ่งใหญ่ของการใช้ฟังก์ชั่นในฐานะฟังก์ชั่นคือพวกเขาสามารถรักษาและใช้ซ้ำระหว่างการโทร ตัวอย่างเช่นอัลกอริทึมการเขียนโปรแกรมแบบไดนามิกจำนวนมากเช่นอัลกอริทึมของWagner-Fischerสำหรับการคำนวณระยะทาง Levenshteinระหว่างสตริงทำงานโดยการกรอกข้อมูลลงในตารางผลลัพธ์ขนาดใหญ่ มันไม่มีประสิทธิภาพมากในการจัดสรรตารางนี้ทุกครั้งที่มีการเรียกใช้ฟังก์ชั่นดังนั้นการใช้ฟังก์ชั่นเป็น functor และการทำให้ตารางเป็นตัวแปรสมาชิกสามารถปรับปรุงประสิทธิภาพได้อย่างมาก

ด้านล่างเป็นตัวอย่างของการใช้อัลกอริทึมของ Wagner-Fischer ในฐานะนักแสดง สังเกตว่าตารางถูกจัดสรรใน Constructor แล้วนำกลับมาใช้ใหม่operator()โดยปรับขนาดตามความจำเป็น

#include <string>
#include <vector>
#include <algorithm>

template <typename T>
T min3(const T& a, const T& b, const T& c)
{
   return std::min(std::min(a, b), c);
}

class levenshtein_distance 
{
    mutable std::vector<std::vector<unsigned int> > matrix_;

public:
    explicit levenshtein_distance(size_t initial_size = 8)
        : matrix_(initial_size, std::vector<unsigned int>(initial_size))
    {
    }

    unsigned int operator()(const std::string& s, const std::string& t) const
    {
        const size_t m = s.size();
        const size_t n = t.size();
        // The distance between a string and the empty string is the string's length
        if (m == 0) {
            return n;
        }
        if (n == 0) {
            return m;
        }
        // Size the matrix as necessary
        if (matrix_.size() < m + 1) {
            matrix_.resize(m + 1, matrix_[0]);
        }
        if (matrix_[0].size() < n + 1) {
            for (auto& mat : matrix_) {
                mat.resize(n + 1);
            }
        }
        // The top row and left column are prefixes that can be reached by
        // insertions and deletions alone
        unsigned int i, j;
        for (i = 1;  i <= m; ++i) {
            matrix_[i][0] = i;
        }
        for (j = 1; j <= n; ++j) {
            matrix_[0][j] = j;
        }
        // Fill in the rest of the matrix
        for (j = 1; j <= n; ++j) {
            for (i = 1; i <= m; ++i) {
                unsigned int substitution_cost = s[i - 1] == t[j - 1] ? 0 : 1;
                matrix_[i][j] =
                    min3(matrix_[i - 1][j] + 1,                 // Deletion
                    matrix_[i][j - 1] + 1,                      // Insertion
                    matrix_[i - 1][j - 1] + substitution_cost); // Substitution
            }
        }
        return matrix_[m][n];
    }
};

1

Functor ยังสามารถใช้เพื่อจำลองการกำหนดฟังก์ชั่นท้องถิ่นภายในฟังก์ชั่น อ้างถึง คำถามและอื่น

แต่ functor ท้องถิ่นไม่สามารถเข้าถึงตัวแปรอัตโนมัติภายนอก ฟังก์ชั่นแลมบ์ดา (C ++ 11) เป็นทางออกที่ดีกว่า


-10

ฉันมี "ค้นพบ" การใช้ฟังก์ชั่นที่น่าสนใจมาก: ฉันใช้พวกเขาเมื่อฉันไม่มีชื่อที่ดีสำหรับวิธีหนึ่งเนื่องจาก functor เป็นวิธีที่ไม่มีชื่อ ;-)


ทำไมคุณถึงอธิบาย functor เป็น "วิธีการที่ไม่มีชื่อ"?
Anderson Green

5
ฟังก์ชั่นที่ไม่มีชื่อเรียกว่าแลมบ์ดา
Paul Fultz II
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.