แลมด้า C ++ พร้อมแคปเจอร์เป็นตัวชี้ฟังก์ชัน


94

ฉันกำลังเล่นกับ C ++ lambdas และการแปลงโดยนัยเป็นฟังก์ชันพอยน์เตอร์ ตัวอย่างเริ่มต้นของฉันใช้เป็นคำเรียกกลับสำหรับฟังก์ชัน ftw สิ่งนี้ได้ผลตามที่คาดไว้

#include <ftw.h>
#include <iostream>

using namespace std;

int main()
{
    auto callback = [](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        cout << fpath << endl;
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    return ret;
}

หลังจากแก้ไขเพื่อใช้การจับภาพ:

int main()
{

    vector<string> entries;

    auto callback = [&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

ฉันได้รับข้อผิดพลาดของคอมไพเลอร์:

error: cannot convert ‘main()::<lambda(const char*, const stat*, int)>’ to ‘__ftw_func_t {aka int (*)(const char*, const stat*, int)}’ for argument ‘2’ to ‘int ftw(const char*, __ftw_func_t, int)

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

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


คุณใช้คอมไพเลอร์อะไร มันคือ VS10 หรือเปล่า
Ramon Zarazua B.

gcc เวอร์ชัน 4.6.1 20110801 [gcc-4_6-branch revision 177033] (SUSE Linux)
duncan

4
โดยปกติวิธี C ในการส่งผ่านสถานะไปยังการเรียกกลับจะกระทำผ่านอาร์กิวเมนต์พิเศษของการเรียกกลับ (โดยปกติจะเป็นประเภทvoid *) หากไลบรารีที่คุณใช้อนุญาตให้มีอาร์กิวเมนต์พิเศษนี้คุณจะพบวิธีแก้ปัญหาชั่วคราว มิฉะนั้นคุณไม่มีทางบรรลุสิ่งที่คุณต้องการทำอย่างหมดจด
Alexandre C.

ใช่. ฉันตระหนักดีว่า API ของ ftw.h และ nftw.h มีข้อบกพร่อง ฉันจะลอง fts.h
duncan

1
เยี่ยมมาก! /usr/include/fts.h:41:3: error: #error "<fts.h> ไม่สามารถใช้กับ -D_FILE_OFFSET_BITS == 64"
duncan

คำตอบ:


48

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

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

แต่นั่นเป็นการเอาชนะจุดประสงค์ทั้งหมดของการจับ lambdas


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

4
@RaymondChen: ถ้าคุณมีเสรีภาพที่จะกำหนดว่าจะใช้ฟังก์ชันนี้อย่างไรใช่นั่นเป็นตัวเลือก แม้ว่าในกรณีนี้จะง่ายกว่าที่จะทำให้พารามิเตอร์เป็นอาร์กิวเมนต์ของแลมบ์ดาเอง!
Kerrek SB

3
@KerrekSB ใส่ตัวแปร global ใน a namespaceและทำเครื่องหมายthread_localว่านั่นคือftwแนวทางที่ฉันเลือกสำหรับการแก้ปัญหาที่คล้ายกัน
Kjell Hedström

"ตัวชี้ฟังก์ชันชี้ไปที่ฟังก์ชันส่วนกลางเดียวและข้อมูลนี้ไม่มีที่ว่างสำหรับสถานะ" -> ภาษานรกเช่น Java สามารถบรรลุสิ่งนี้ได้อย่างไร? แน่นอนว่าเนื่องจากฟังก์ชันส่วนกลางเดียวนั้นถูกสร้างขึ้นที่รันไทม์และฝังสถานะ (หรือมากกว่าการอ้างอิงถึง) ในรหัสของตัวเอง นั่นเป็นจุดรวม - ควรมีไม่ได้เป็นคนเดียวฟังก์ชั่นระดับโลกแต่หลายฟังก์ชั่นระดับโลก - หนึ่งสำหรับแต่ละครั้งที่แลมบ์ดาถูกนำมาใช้ในการรันไทม์ ไม่มีอะไรใน C ++ จริงเหรอ? (ฉันคิดว่าฟังก์ชัน std :: ถูกสร้างขึ้นมาเพื่อจุดประสงค์เดียว)
Dexter

1
@Dexter: เอ่อ .. คำตอบสั้น ๆ คือไม่คำตอบแบบยาวเกี่ยวข้องกับการทำงานหนักเกินไป โดยไม่คำนึงถึงจุดของฉัน Java เป็นภาษาอื่นที่ไม่เหมือนกับ C ++ Java ไม่มีพอยน์เตอร์ (หรือตัวดำเนินการโทรที่โอเวอร์โหลด) และการเปรียบเทียบทำงานได้ไม่ดี
Kerrek SB

47

ฉันเพิ่งพบปัญหานี้

โค้ดคอมไพล์ได้ดีโดยไม่มีการจับแลมบ์ดา แต่มีข้อผิดพลาดในการแปลงประเภทด้วยการจับแลมด้า

วิธีแก้ปัญหาด้วย C ++ 11 คือการใช้งานstd::function(แก้ไข: โซลูชันอื่นที่ไม่จำเป็นต้องแก้ไขลายเซ็นฟังก์ชันจะแสดงหลังจากตัวอย่างนี้) คุณยังสามารถใช้boost::function(ซึ่งทำงานเร็วกว่ามาก) ตัวอย่างโค้ด - เปลี่ยนแปลงเพื่อให้คอมไพล์คอมไพล์ด้วยgcc 4.7.1:

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

int ftw(const char *fpath, std::function<int (const char *path)> callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };

  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

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

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// Original ftw function taking raw function pointer that cannot be modified
int ftw(const char *fpath, int(*callback)(const char *path)) {
  return callback(fpath);
}

static std::function<int(const char*path)> ftw_callback_function;

static int ftw_callback_helper(const char *path) {
  return ftw_callback_function(path);
}

// ftw overload accepting lambda function
static int ftw(const char *fpath, std::function<int(const char *path)> callback) {
  ftw_callback_function = callback;
  return ftw(fpath, ftw_callback_helper);
}

int main() {
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };
  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

73
ไม่ควรเป็นคำตอบที่ยอมรับ ประเด็นไม่ได้เปลี่ยนftwไปใช้std::functionแทนตัวชี้ฟังก์ชัน ...
Gregory Pakosz

โซลูชันที่สองที่เสนอในคำตอบนี้ช่วยแก้ปัญหาของ @ gregory-pakosz โดยการรักษาลายเซ็นดั้งเดิมไว้ แต่ก็ยังไม่ดีนักเนื่องจากเป็นการแนะนำสถานะทั่วโลก หากftwมีอาร์กิวเมนต์ void * userdata ฉันต้องการคำตอบจาก @ evgeny-karpov
prideout

@prideout เห็นด้วย - ฉันไม่ชอบรัฐระดับโลกเช่นกัน น่าเสียดายที่สมมติว่าลายเซ็นของ ftw ไม่สามารถแก้ไขได้และเนื่องจากไม่มี void * userdata จึงต้องจัดเก็บ state ไว้ที่ใดที่หนึ่ง ฉันพบปัญหานี้โดยใช้ไลบรารีของบุคคลที่สาม สิ่งนี้จะทำงานได้ดีตราบเท่าที่ไลบรารีไม่จับการเรียกกลับและใช้ในภายหลังซึ่งในกรณีนี้ตัวแปรส่วนกลางจะทำหน้าที่เหมือนพารามิเตอร์พิเศษบน call stack ถ้าลายเซ็นของ ftw สามารถแก้ไขได้ฉันต้องการใช้ std :: function แทน void * userdata
Jay West

1
นี่เป็นวิธีแก้ปัญหาที่ซับซ้อนและมีประโยชน์อย่างยิ่ง @Gregory ฉันควรบอกคุณว่า "ได้ผล"
fiorentinoing

17

เดิม

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

template<typename Tret, typename T>
Tret lambda_ptr_exec(T* v) {
    return (Tret) (*v)();
}

template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
Tfp lambda_ptr(T& v) {
    return (Tfp) lambda_ptr_exec<Tret, T>;
}

ตัวอย่าง

int a = 100;
auto b = [&]() { a += 1;};
void (*fp)(void*) = lambda_ptr(b);
fp(&b);

ตัวอย่างที่มีค่าส่งคืน

int a = 100;
auto b = [&]() {return a;};
int (*fp)(void*) = lambda_ptr<int>(b);
fp(&b);

อัปเดต

รุ่นปรับปรุง

มีการโพสต์โพสต์ครั้งแรกเกี่ยวกับ C ++ lambda พร้อมแคปเจอร์เป็นตัวชี้ฟังก์ชัน เนื่องจากสามารถใช้งานได้สำหรับฉันและคนอื่น ๆ ฉันจึงปรับปรุงบางอย่าง

ฟังก์ชันมาตรฐานตัวชี้ C api ใช้การประชุม void fn (void * data) โดยค่าเริ่มต้นจะใช้อนุสัญญานี้และแลมด้าควรประกาศด้วยอาร์กิวเมนต์โมฆะ *

ปรับปรุงการใช้งาน

struct Lambda {
    template<typename Tret, typename T>
    static Tret lambda_ptr_exec(void* data) {
        return (Tret) (*(T*)fn<T>())(data);
    }

    template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
    static Tfp ptr(T& t) {
        fn<T>(&t);
        return (Tfp) lambda_ptr_exec<Tret, T>;
    }

    template<typename T>
    static void* fn(void* new_fn = nullptr) {
        static void* fn;
        if (new_fn != nullptr)
            fn = new_fn;
        return fn;
    }
};

Exapmle

int a = 100;
auto b = [&](void*) {return ++a;};

การแปลงแลมด้าพร้อมแคปเตอร์เป็นตัวชี้ C

void (*f1)(void*) = Lambda::ptr(b);
f1(nullptr);
printf("%d\n", a);  // 101 

สามารถใช้วิธีนี้ได้เช่นกัน

auto f2 = Lambda::ptr(b);
f2(nullptr);
printf("%d\n", a); // 102

ในกรณีที่ควรใช้ค่าส่งคืน

int (*f3)(void*) = Lambda::ptr<int>(b);
printf("%d\n", f3(nullptr)); // 103

และในกรณีที่มีการใช้ข้อมูล

auto b2 = [&](void* data) {return *(int*)(data) + a;};
int (*f4)(void*) = Lambda::ptr<int>(b2);
int data = 5;
printf("%d\n", f4(&data)); // 108

3
นี่เป็นวิธีที่สะดวกที่สุดที่ฉันเคยเห็นในการแปลงแลมด้าเป็นตัวชี้ฟังก์ชันรูปแบบ C ฟังก์ชันที่ใช้เป็นอาร์กิวเมนต์จะต้องมีพารามิเตอร์พิเศษที่แสดงสถานะของมันซึ่งมักตั้งชื่อว่า "void * user" ในไลบรารี C เพื่อให้สามารถส่งผ่านไปยังตัวชี้ฟังก์ชันเมื่อเรียกใช้
Codoscope

10

ใช้วิธีโกลบอล (แบบคงที่) แบบโลคัลสามารถทำได้ดังต่อไปนี้

template <class F>
auto cify_no_args(F&& f) {
  static F fn = std::forward<F>(f);
  return [] {
    return fn();
  };
}

สมมติว่าเรามี

void some_c_func(void (*callback)());

ดังนั้นการใช้งานจะเป็นอย่างไร

some_c_func(cify_no_args([&] {
  // code
}));

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

template <class F>
struct lambda_traits : lambda_traits<decltype(&F::operator())>
{ };

template <typename F, typename R, typename... Args>
struct lambda_traits<R(F::*)(Args...)> : lambda_traits<R(F::*)(Args...) const>
{ };

template <class F, class R, class... Args>
struct lambda_traits<R(F::*)(Args...) const> {
    using pointer = typename std::add_pointer<R(Args...)>::type;

    static pointer cify(F&& f) {
        static F fn = std::forward<F>(f);
        return [](Args... args) {
            return fn(std::forward<Args>(args)...);
        };
    }
};

template <class F>
inline lambda_traits<F>::pointer cify(F&& f) {
    return lambda_traits<F>::cify(std::forward<F>(f));
}

และการใช้งานที่คล้ายกัน

void some_c_func(int (*callback)(some_struct*, float));

some_c_func(cify([&](some_struct* s, float f) {
    // making use of "s" and "f"
    return 0;
}));

1
โปรดทราบว่าสิ่งนี้จะคัดลอกการปิด (เมื่อได้รับ ptr) + args (เมื่อโทร) มิฉะนั้นจะเป็นทางออกที่ดี
Ivan Sanz-Carasa

ไลบรารีตัวช่วยส่วนหัวเท่านั้น: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa

1
@ IvanSanz-Carasa ขอบคุณที่ชี้ให้เห็น ประเภทการปิดไม่ใช่ CopyAssignable แต่เป็น functors ใช่แล้วการใช้การส่งต่อที่สมบูรณ์แบบที่นี่จะดีกว่า สำหรับ args ในทางกลับกันเราไม่สามารถทำได้มากนักเนื่องจาก C ธรรมดาไม่รองรับการอ้างอิงสากล แต่อย่างน้อยเราก็สามารถส่งต่อค่ากลับไปยังแลมบ์ดาของเราได้ ซึ่งอาจบันทึกสำเนาเพิ่มเติม มีการแก้ไขรหัส
Vladimir Talybin

@RiaD ใช่เนื่องจากแลมบ์ดาเป็นอินสแตนซ์แบบคงที่ที่นี่คุณจะต้องจับโดยการอ้างอิงแทนเช่นแทนที่จะ=ใช้&iใน for-loop ของคุณ
Vladimir Talybin

5

ฮิฮิ - ค่อนข้างเป็นคำถามเก่า แต่ก็ยัง ...

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// We dont try to outsmart the compiler...
template<typename T>
int ftw(const char *fpath, T callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  // ... now the @ftw can accept lambda
  int ret = ftw("/etc", [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  });

  // ... and function object too 
  struct _ {
    static int lambda(vector<string>& entries, const char* fpath) {
      entries.push_back(fpath);
      return 0;
    }
  };
  ret = ftw("/tmp", bind(_::lambda, ref(entries), placeholders::_1));

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

0

มีวิธีที่แฮ็กในการแปลงแลมด้าที่จับเป็นตัวชี้ฟังก์ชันได้ แต่คุณต้องระมัดระวังในการใช้งาน:

/codereview/79612/c-ifying-a-capturing-lambda

รหัสของคุณจะมีลักษณะดังนี้ (คำเตือน: Brain compile):

int main()
{

    vector<string> entries;

    auto const callback = cify<int(*)(const char *, const struct stat*,
        int)>([&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    });

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

0

วิธีแก้ปัญหาของฉันเพียงแค่ใช้ตัวชี้ฟังก์ชันเพื่ออ้างถึงแลมด้าแบบคงที่

typedef int (* MYPROC)(int);

void fun(MYPROC m)
{
    cout << m(100) << endl;
}

template<class T>
void fun2(T f)
{
    cout << f(100) << endl;
}

void useLambdaAsFunPtr()
{
    int p = 7;
    auto f = [p](int a)->int {return a * p; };

    //fun(f);//error
    fun2(f);
}

void useLambdaAsFunPtr2()
{
    int p = 7;
    static auto f = [p](int a)->int {return a * p; };
    MYPROC ff = [](int i)->int { return f(i); };
    //here, it works!
    fun(ff);
}

void test()
{
    useLambdaAsFunPtr2();
}

-1

พบคำตอบที่นี่: http://meh.schizofreni.co/programming/magic/2013/01/23/function-pointer-from-lambda.html

มันจะแปลงlambda pointerไปvoid*และแปลงกลับเมื่อมีความจำเป็น

  1. ถึงvoid*:

    auto voidfunction = ประเภทการปฏิเสธใหม่ (to_function (lambda)) (to_function (lambda));

  2. จากvoid*:

    ฟังก์ชั่นอัตโนมัติ = static_cast <std :: function *> (voidfunction);

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