การส่งแลมบ์ดาเป็นตัวชี้ฟังก์ชัน


210

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

ลองพิจารณาตัวอย่างต่อไปนี้

using DecisionFn = bool(*)();

class Decide
{
public:
    Decide(DecisionFn dec) : _dec{dec} {}
private:
    DecisionFn _dec;
};

int main()
{
    int x = 5;
    Decide greaterThanThree{ [x](){ return x > 3; } };
    return 0;
}

เมื่อฉันพยายามรวบรวมสิ่งนี้ฉันได้รับข้อผิดพลาดในการรวบรวมดังต่อไปนี้:

In function 'int main()':
17:31: error: the value of 'x' is not usable in a constant expression
16:9:  note: 'int x' is not const
17:53: error: no matching function for call to 'Decide::Decide(<brace-enclosed initializer list>)'
17:53: note: candidates are:
9:5:   note: Decide::Decide(DecisionFn)
9:5:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'DecisionFn {aka bool (*)()}'
6:7:   note: constexpr Decide::Decide(const Decide&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'const Decide&'
6:7:   note: constexpr Decide::Decide(Decide&&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'Decide&&'

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


34
แลมบ์ดาสามารถสลายตัวไปยังฟังก์ชั่นของตัวชี้เฉพาะในกรณีที่พวกเขาไม่ได้จับอะไรเลย
Jarod42


สำหรับผู้โพสต์ตอนนี้โพสต์บล็อกที่ลิงก์ข้างต้นอาศัยอยู่ที่devblogs.microsoft.com/oldnewthing/20150220-00/?p=44623
warrenm

คำตอบ:


205

แลมบ์ดาสามารถแปลงเป็นตัวชี้ฟังก์ชั่นได้หากไม่จับภาพจากส่วนมาตรฐาน C ++ 11 ฉบับร่าง5.1.2 [expr.prim.lambda]พูดว่า ( เหมืองของฉัน ):

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

หมายเหตุ cppreference ยังครอบคลุมถึงนี้ในส่วนของพวกเขาในฟังก์ชั่นแลมบ์ดา

ดังนั้นทางเลือกต่อไปนี้จะใช้ได้:

typedef bool(*DecisionFn)(int);

Decide greaterThanThree{ []( int x ){ return x > 3; } };

และสิ่งนี้จะ:

typedef bool(*DecisionFn)();

Decide greaterThanThree{ [](){ return true ; } };

และในฐานะที่เป็น5gon12ederชี้ให้เห็นคุณยังสามารถใช้std::functionแต่โปรดทราบว่าstd::functionมีน้ำหนักมากดังนั้นจึงไม่ได้เป็นการลดต้นทุน


2
หมายเหตุด้านข้าง: โซลูชันทั่วไปหนึ่งอย่างที่ใช้โดยสิ่งของ C คือส่งผ่านvoid*พารามิเตอร์ตัวเดียว ปกติจะเรียกว่า "ตัวชี้ผู้ใช้" มันค่อนข้างเบาเหมือนกัน แต่มีแนวโน้มว่าคุณจะต้องใช้mallocพื้นที่บางส่วน
คดีของกองทุนโมนิกา

94

คำตอบของ Shafik Yaghmourอธิบายได้อย่างถูกต้องว่าทำไมแลมบ์ดาไม่สามารถส่งผ่านเป็นตัวชี้ฟังก์ชันได้หากมีการจับภาพ ฉันต้องการแสดงวิธีแก้ไขปัญหาอย่างง่ายสองประการ

  1. ใช้std::functionแทนตัวชี้ฟังก์ชั่นดิบ

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

    #include <functional>
    #include <utility>
    
    struct Decide
    {
      using DecisionFn = std::function<bool()>;
      Decide(DecisionFn dec) : dec_ {std::move(dec)} {}
      DecisionFn dec_;
    };
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree { [x](){ return x > 3; } };
    }
  2. ใช้การแสดงออกแลมบ์ดาที่ไม่ได้จับอะไรเลย

    เนื่องจากภาคแสดงของคุณเป็นเพียงค่าคงที่บูลีนต่อไปนี้จะแก้ไขปัญหาปัจจุบันได้อย่างรวดเร็ว ดูคำตอบนี้สำหรับคำอธิบายที่ดีว่าเพราะอะไรและทำงานอย่างไร

    // Your 'Decide' class as in your post.
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree {
        (x > 3) ? [](){ return true; } : [](){ return false; }
      };
    }

4
@TC ดูคำถามนี้เพื่อดูรายละเอียดว่าทำไมมันถึงได้ผล
Shafik Yaghmour

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

วัตถุไม่ควรมีอายุการใช้งานนานกว่าฟังก์ชั่นตัวชี้หรือไม่? glutReshapeFuncผมอยากจะใช้มันสำหรับ
ar2015

ฉันไม่แนะนำคำแนะนำนี้สิ่งต่าง ๆ ที่มีแนวโน้มที่จะทำงานได้ดีแนะนำข้อผิดพลาดใหม่ และแนวทางปฏิบัติที่สอดคล้องกับข้อผิดพลาดเหล่านั้น ถ้าคุณต้องการใช้ฟังก์ชั่น std :: คุณควรเห็นวิธีที่ std :: function ทุกชนิดสามารถใช้ได้ เพราะวิธีบางอย่างอาจเป็นสิ่งที่คุณไม่ต้องการ
เชิงลบ

1
สิ่งนี้ไม่ตอบคำถาม หากใครสามารถใช้std::functionหรือแลมบ์ดา - ทำไมพวกเขาจะไม่? อย่างน้อยที่สุดมันเป็นไวยากรณ์ที่อ่านได้มากขึ้น โดยปกติแล้วเราต้องใช้ตัวชี้ฟังก์ชั่นเพื่อโต้ตอบกับไลบรารี C (จริง ๆ กับไลบรารีภายนอกใด ๆ )และตรวจสอบว่าคุณไม่สามารถแก้ไขได้เพื่อยอมรับ std :: function หรือ lambda
สวัสดีแองเจิล

40

นิพจน์แลมบ์ดาที่จับได้สามารถใช้เป็นตัวชี้ฟังก์ชั่นได้

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

เมื่อคุณมีความคิดสร้างสรรค์คุณสามารถใช้สิ่งนี้! ลองนึกถึงคลาส "ฟังก์ชัน" ในรูปแบบของ std :: function หากคุณบันทึกวัตถุคุณยังสามารถใช้ตัวชี้ฟังก์ชั่น

ในการใช้ตัวชี้ฟังก์ชั่นคุณสามารถใช้สิ่งต่อไปนี้:

int first = 5;
auto lambda = [=](int x, int z) {
    return x + z + first;
};
int(decltype(lambda)::*ptr)(int, int)const = &decltype(lambda)::operator();
std::cout << "test = " << (lambda.*ptr)(2, 3) << std::endl;

ในการสร้างคลาสที่สามารถเริ่มทำงานได้เหมือน "std :: function" อันดับแรกคุณต้องมีคลาส / struct กว่าสามารถเก็บวัตถุและตัวชี้ฟังก์ชันได้ นอกจากนี้คุณต้องมีโอเปอเรเตอร์ () เพื่อดำเนินการ:

// OT => Object Type
// RT => Return Type
// A ... => Arguments
template<typename OT, typename RT, typename ... A>
struct lambda_expression {
    OT _object;
    RT(OT::*_function)(A...)const;

    lambda_expression(const OT & object)
        : _object(object), _function(&decltype(_object)::operator()) {}

    RT operator() (A ... args) const {
        return (_object.*_function)(args...);
    }
};

ด้วยวิธีนี้คุณสามารถเรียกใช้ lambdas ที่จับได้และไม่จับได้เช่นเดียวกับที่คุณใช้:

auto capture_lambda() {
    int first = 5;
    auto lambda = [=](int x, int z) {
        return x + z + first;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

auto noncapture_lambda() {
    auto lambda = [](int x, int z) {
        return x + z;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

void refcapture_lambda() {
    int test;
    auto lambda = [&](int x, int z) {
        test = x + z;
    };
    lambda_expression<decltype(lambda), void, int, int>f(lambda);
    f(2, 3);

    std::cout << "test value = " << test << std::endl;
}

int main(int argc, char **argv) {
    auto f_capture = capture_lambda();
    auto f_noncapture = noncapture_lambda();

    std::cout << "main test = " << f_capture(2, 3) << std::endl;
    std::cout << "main test = " << f_noncapture(2, 3) << std::endl;

    refcapture_lambda();

    system("PAUSE");
    return 0;
}

รหัสนี้ใช้ได้กับ VS2015

อัพเดท 04.07.17:

template <typename CT, typename ... A> struct function
: public function<decltype(&CT::operator())(A...)> {};

template <typename C> struct function<C> {
private:
    C mObject;

public:
    function(const C & obj)
        : mObject(obj) {}

    template<typename... Args> typename 
    std::result_of<C(Args...)>::type operator()(Args... a) {
        return this->mObject.operator()(a...);
    }

    template<typename... Args> typename 
    std::result_of<const C(Args...)>::type operator()(Args... a) const {
        return this->mObject.operator()(a...);
    }
};

namespace make {
    template<typename C> auto function(const C & obj) {
        return ::function<C>(obj);
    }
}

int main(int argc, char ** argv) {
   auto func = make::function([](int y, int x) { return x*y; });
   std::cout << func(2, 4) << std::endl;
   system("PAUSE");
   return 0;
}

ว้าวน่าทึ่งมาก! ดังนั้นเราจึงสามารถใช้พอยน์เตอร์ภายในคลาสของแลมบ์ดา (เพื่อผู้ประกอบการฟังก์ชั่นสมาชิก ()) เพื่อเรียกใช้แลมบ์ดาที่เก็บไว้ในคลาส wrapper !! AMAZING !! ทำไมเราต้องการฟังก์ชัน std :: แล้ว? และเป็นไปได้หรือไม่ที่จะสร้าง lambda_expression <decltype (lambda), int, int, int> เพื่อลดค่าโดยอัตโนมัติ / พารามิเตอร์ "int" เหล่านี้โดยตรงจากแลมบ์ดาที่ผ่านมานั้นเอง?
บาร์นีย์

2
ฉันได้เพิ่มรหัสย่อของฉันเอง สิ่งนี้ควรจะทำงานร่วมกับ auto f = make :: function (lambda) แบบง่าย; แต่ฉันค่อนข้างมั่นใจว่าคุณจะพบสถานการณ์มากมายรหัสของฉันจะไม่ทำงาน ฟังก์ชั่น std :: เป็นวิธีที่สร้างขึ้นได้ดีกว่านี้และควรจะไปเมื่อคุณกำลังทำงาน นี่คือเพื่อการศึกษาและการใช้งานส่วนตัว
Noxxer

14
วิธีแก้ปัญหานี้เกี่ยวข้องกับการเรียกแลมด้าผ่านoperator()การนำไปใช้ดังนั้นถ้าฉันอ่านถูกต้องฉันไม่คิดว่ามันจะใช้เรียกแลมด้าโดยใช้ตัวชี้ฟังก์ชั่นรูปแบบ Cได้หรือไม่ นั่นคือสิ่งที่คำถามเดิมถาม
Remy Lebeau

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

9
นี่ไม่ใช่ "การส่งแลมบ์ดาผ่านเป็นตัวชี้ฟังก์ชัน" นี่คือ "การส่งแลมบ์ดาผ่านเป็นวัตถุที่มีตัวชี้ฟังก์ชันเหนือสิ่งอื่นใด" มีโลกที่แตกต่าง
n คำสรรพนาม 'm

15

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

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

static Callable callable;
static bool wrapper()
{
    return callable();
}

นี่มันน่าเบื่อ เรานำแนวคิดนี้ไปใช้และทำให้กระบวนการสร้างwrapperและทำให้ชีวิตง่ายขึ้นมากขึ้น

#include<type_traits>
#include<utility>

template<typename Callable>
union storage
{
    storage() {}
    std::decay_t<Callable> callable;
};

template<int, typename Callable, typename Ret, typename... Args>
auto fnptr_(Callable&& c, Ret (*)(Args...))
{
    static bool used = false;
    static storage<Callable> s;
    using type = decltype(s.callable);

    if(used)
        s.callable.~type();
    new (&s.callable) type(std::forward<Callable>(c));
    used = true;

    return [](Args... args) -> Ret {
        return Ret(s.callable(std::forward<Args>(args)...));
    };
}

template<typename Fn, int N = 0, typename Callable>
Fn* fnptr(Callable&& c)
{
    return fnptr_<N>(std::forward<Callable>(c), (Fn*)nullptr);
}

และใช้มันเป็น

void foo(void (*fn)())
{
    fn();   
}

int main()
{
    int i = 42;
    auto fn = fnptr<void()>([i]{std::cout << i;});
    foo(fn);  // compiles!
}

มีชีวิต

fnptrนี้เป็นหลักประกาศฟังก์ชั่นที่ไม่ระบุชื่อที่เกิดขึ้นของแต่ละคน

โปรดทราบว่าการเรียกการfnptrเขียนทับcallablecallables ที่กำหนดไว้ก่อนหน้านี้ของชนิดเดียวกัน เราแก้ไขปัญหานี้ในระดับหนึ่งกับพารามิเตอร์intN

std::function<void()> func1, func2;
auto fn1 = fnptr<void(), 1>(func1);
auto fn2 = fnptr<void(), 2>(func2);  // different function

การบังคับให้มีการประกาศจำนวนเต็ม N จะเป็นวิธีที่ยอดเยี่ยมในการจดจำไคลเอ็นต์เพื่อหลีกเลี่ยงการเขียนทับตัวชี้ฟังก์ชันในเวลารวบรวม
fiorentinoing

2

ทางลัดสำหรับการใช้แลมบ์ดาที่ใช้เป็นตัวชี้ฟังก์ชัน C คือ:

"auto fun = +[](){}"

ใช้ Curl เป็น exmample ( ข้อมูล debug debug )

auto callback = +[](CURL* handle, curl_infotype type, char* data, size_t size, void*){ //add code here :-) };
curl_easy_setopt(curlHande, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curlHande,CURLOPT_DEBUGFUNCTION,callback);

3
แลมบ์ดานั้นไม่มีการดักจับ ปัญหาของ OP คือการดักจับโดยไม่ต้องอนุมานประเภทฟังก์ชั่นของตัวชี้ (ซึ่งเป็น+เคล็ดลับที่ทำให้คุณได้รับ)
Sneftel

2

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

ทางออกที่ง่ายกว่าสำหรับการจับแลมบ์ดาเป็นตัวชี้คือ:

auto pLamdba = new std::function<...fn-sig...>([=](...fn-sig...){...});

เช่น, new std::function<void()>([=]() -> void {...}

เพียงจำไว้ในภายหลังdelete pLamdbaเพื่อให้แน่ใจว่าคุณจะไม่รั่วไหลหน่วยความจำแลมบ์ดา ความลับในการตระหนักถึงที่นี่คือstd::functionแลมบ์ดาสามารถจับแลมบ์ดาส(ถามตัวเองว่ามันทำงานอย่างไร) และเพื่อให้การใช้งานแลมบ์ดาโดยทั่วไปจำเป็นต้องมีข้อมูลภายในที่เพียงพอเพื่อให้เข้าถึงข้อมูลขนาดแลมด้า ซึ่งเป็นสาเหตุที่deleteควรทำงาน [destructors ทำงานประเภทจับ])


ทำไมต้องกังวลกับnewฟังก์ชั่น - std :: แล้วเก็บแลมบ์ดาไว้ในฮีปและหลีกเลี่ยงการจำการโทรลบ
Chris Dodd

0

ไม่ใช่คำตอบโดยตรง แต่มีการเปลี่ยนแปลงเล็กน้อยในการใช้รูปแบบเทมเพลต "functor" เพื่อซ่อนรายละเอียดเฉพาะของแลมบ์ดาและเก็บรหัสไว้อย่างดีและเรียบง่าย

ฉันไม่แน่ใจว่าคุณต้องการใช้คลาสตัดสินใจได้อย่างไรดังนั้นฉันจึงต้องขยายคลาสด้วยฟังก์ชันที่ใช้งาน ดูตัวอย่างทั้งหมดได้ที่นี่: https://godbolt.org/z/jtByqE

รูปแบบพื้นฐานของชั้นเรียนของคุณอาจมีลักษณะเช่นนี้:

template <typename Functor>
class Decide
{
public:
    Decide(Functor dec) : _dec{dec} {}
private:
    Functor _dec;
};

โดยที่คุณผ่านประเภทของฟังก์ชั่นเป็นส่วนหนึ่งของประเภทชั้นเรียนที่ใช้เช่น:

auto decide_fc = [](int x){ return x > 3; };
Decide<decltype(decide_fc)> greaterThanThree{decide_fc};

อีกครั้งฉันไม่แน่ใจว่าทำไมคุณถึงจับxมันทำให้รู้สึกมากกว่า (ถึงฉัน) เพื่อให้พารามิเตอร์ที่คุณผ่านไปแลมบ์ดา) เพื่อให้คุณสามารถใช้เช่น:

int result = _dec(5); // or whatever value

ดูลิงค์สำหรับตัวอย่างที่สมบูรณ์


-2

ตามที่ถูกกล่าวถึงโดยคนอื่นคุณสามารถแทนที่ฟังก์ชั่นแลมบ์ดาแทนฟังก์ชั่นตัวชี้ ฉันใช้วิธีนี้ในอินเทอร์เฟซ C ++ ของฉันไปยัง F77 ODE Solver RKSUITE

//C interface to Fortran subroutine UT
extern "C"  void UT(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

// C++ wrapper which calls extern "C" void UT routine
static  void   rk_ut(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

//  Call of rk_ut with lambda passed instead of function pointer to derivative
//  routine
mathlib::RungeKuttaSolver::rk_ut([](double* T,double* Y,double* YP)->void{YP[0]=Y[1]; YP[1]= -Y[0];}, TWANT,T,Y,YP,YMAX,WORK,UFLAG);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.