C ++ lambda với chụp dưới dạng con trỏ hàm


94

Tôi đã chơi với lambdas C ++ và sự chuyển đổi ngầm của chúng thành con trỏ hàm. Ví dụ ban đầu của tôi là sử dụng chúng làm lệnh gọi lại cho hàm ftw. Điều này hoạt động như mong đợi.

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

Sau khi sửa đổi nó để sử dụng chụp:

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

Tôi gặp lỗi trình biên dịch:

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

Sau khi đọc một số. Tôi đã biết rằng lambdas sử dụng chụp không thể được chuyển đổi hoàn toàn thành con trỏ hàm.

Có một cách giải quyết cho điều này? Thực tế là chúng không thể được chuyển đổi "ngầm" có nghĩa là chúng có thể được chuyển đổi "rõ ràng" không? (Tôi đã thử casting, nhưng không thành công). Cách tốt nhất để sửa đổi ví dụ làm việc để tôi có thể nối các mục vào một số đối tượng bằng lambdas là gì ?.


Bạn đang sử dụng trình biên dịch nào? nó là VS10?
Ramon Zarazua B.

gcc phiên bản 4.6.1 20110801 [phiên bản gcc-4_6-branch 177033] (SUSE Linux)
duncan 21/10/11

4
Thông thường, cách C để chuyển trạng thái tới các lệnh gọi lại được thực hiện thông qua một đối số bổ sung cho lệnh gọi lại (thường là kiểu void *). Nếu thư viện bạn đang sử dụng cho phép đối số bổ sung này, bạn sẽ tìm thấy một cách giải quyết. Nếu không, bạn không có cách nào để đạt được những gì bạn muốn làm.
Alexandre C.

Đúng. Tôi nhận ra api của ftw.h và nftw.h là thiếu sót. Tôi sẽ thử fts.h
duncan

1
Tuyệt quá! /usr/include/fts.h:41:3: error: #error "<fts.h> không thể được sử dụng với -D_FILE_OFFSET_BITS == 64"
duncan

Câu trả lời:


48

Vì việc nắm bắt các lambdas cần phải duy trì trạng thái, thực sự không có "giải pháp" đơn giản nào, vì chúng không chỉ là các hàm thông thường. Điểm về một con trỏ hàm là nó trỏ đến một hàm duy nhất, toàn cục và thông tin này không có chỗ cho một trạng thái.

Cách giải quyết gần nhất (về cơ bản loại bỏ trạng thái) là cung cấp một số loại biến toàn cục được truy cập từ lambda / function của bạn. Ví dụ, bạn có thể tạo một đối tượng functor truyền thống và cung cấp cho nó một hàm thành viên tĩnh tham chiếu đến một số cá thể duy nhất (toàn cục / tĩnh).

Nhưng đó là cách đánh bại toàn bộ mục đích bắt lambdas.


3
Một giải pháp tốt hơn là bọc lambda bên trong một bộ điều hợp, giả sử rằng con trỏ hàm có một tham số ngữ cảnh.
Raymond Chen

4
@RaymondChen: Chà, nếu bạn được tự do xác định cách hàm được sử dụng, thì vâng, đó là một tùy chọn. Mặc dù trong trường hợp đó, sẽ dễ dàng hơn nếu chỉ làm cho tham số trở thành đối số của chính lambda!
Kerrek SB

3
@KerrekSB đặt các biến toàn cục vào a namespacevà đánh dấu chúng là thread_local, đó là ftwcách tiếp cận mà tôi đã chọn để giải quyết vấn đề tương tự.
Kjell Hedström

"một con trỏ hàm trỏ đến một hàm duy nhất, toàn cục và thông tin này không có chỗ cho một trạng thái." -> Làm thế quái nào mà các ngôn ngữ như Java có thể thực hiện được điều này? Tất nhiên, bởi vì hàm duy nhất, toàn cục đó được tạo trong thời gian chạynhúng trạng thái (hay đúng hơn là tham chiếu đến nó) trong mã riêng của nó. Đó toàn bộ vấn đề - có nên không thể là một , chức năng toàn cầu duy nhất nhưng nhiều chức năng toàn cầu - một cho mỗi lần lambda được sử dụng trong thời gian chạy. Có thực sự KHÔNG CÓ ĐIỀU GÌ trong C ++ làm được điều đó không? (Tôi nghĩ hàm std :: được tạo ra chính xác cho mục đích duy nhất đó)
Dexter

1
@Dexter: errr .. câu trả lời ngắn gọn là không, câu trả lời dài liên quan đến việc nạp chồng toán tử. Bất chấp, quan điểm của tôi là đúng. Java là một ngôn ngữ khác không giống với C ++; Java không có con trỏ (hoặc toán tử cuộc gọi có thể quá tải) và so sánh không hoạt động tốt.
Kerrek SB

47

Tôi chỉ gặp phải vấn đề này.

Mã biên dịch tốt mà không có lambda chụp, nhưng có một lỗi chuyển đổi kiểu với lambda capture.

Giải pháp với C ++ 11 là sử dụng std::function(chỉnh sửa: một giải pháp khác không yêu cầu sửa đổi chữ ký hàm được hiển thị sau ví dụ này). Bạn cũng có thể sử dụng boost::function(thực sự chạy nhanh hơn đáng kể). Mã mẫu - được thay đổi để nó sẽ biên dịch, được biên dịch với 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;
}

Chỉnh sửa: Tôi đã phải truy cập lại điều này khi tôi gặp mã kế thừa, nơi tôi không thể sửa đổi chữ ký hàm ban đầu, nhưng vẫn cần sử dụng lambdas. Dưới đây là giải pháp không yêu cầu sửa đổi chữ ký hàm của hàm gốc:

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

72
Không, đây không phải là câu trả lời được chấp nhận. Vấn đề không được thay đổi ftwđể chăm std::functionthay vì một con trỏ hàm ...
Gregory Pakosz

Giải pháp thứ hai được đề xuất trong câu trả lời này giải quyết mối lo ngại từ @ gregory-pakosz bằng cách giữ nguyên chữ ký gốc, nhưng nó vẫn không tuyệt vời vì nó giới thiệu trạng thái toàn cầu. Nếu ftwcó đối số void * userdata, thì tôi muốn câu trả lời từ @ evgeny-karpov hơn.
hào vào

@prideout đồng ý - Tôi cũng không thích trạng thái toàn cầu. Thật không may, giả sử chữ ký của ftw không thể được sửa đổi và cho rằng nó không có void * userdata, trạng thái phải được lưu trữ ở đâu đó. Tôi gặp sự cố này khi sử dụng thư viện của bên thứ ba. Điều này sẽ hoạt động tốt miễn là thư viện không nắm bắt lệnh gọi lại và sử dụng nó sau đó, trong trường hợp đó, biến toàn cục chỉ đơn giản hoạt động như một tham số bổ sung trên ngăn xếp cuộc gọi. Nếu chữ ký của ftw có thể được sửa đổi, thì tôi muốn sử dụng hàm std :: thay vì void * userdata.
Jay West,

1
đây là một giải pháp cực kỳ phức tạp và hữu ích, @Gregory Tôi nên nói với bạn "nó hoạt động".
fiorentinoing

17

NGUYÊN

Các hàm Lambda rất tiện lợi và giảm thiểu một đoạn mã. Trong trường hợp của tôi, tôi cần lambdas để lập trình song song. Nhưng nó yêu cầu chụp và con trỏ chức năng. Giải pháp của tôi là ở đây. Nhưng hãy cẩn thận với phạm vi của các biến mà bạn đã nắm bắt.

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

Thí dụ

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

Ví dụ với giá trị trả về

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

CẬP NHẬT

Phiên bản cải tiến

Đã một thời gian kể từ khi bài đăng đầu tiên về C ++ lambda với các ảnh chụp dưới dạng con trỏ hàm được đăng. Vì nó có thể sử dụng được cho tôi và những người khác, tôi đã thực hiện một số cải tiến.

Hàm tiêu chuẩn C con trỏ api sử dụng quy ước void fn (void * data). Theo mặc định, quy ước này được sử dụng và lambda phải được khai báo với đối số void *.

Cải thiện triển khai

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

Chuyển đổi lambda với các ảnh chụp thành con trỏ C

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

Cũng có thể được sử dụng theo cách này

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

Trong trường hợp giá trị trả về nên được sử dụng

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

Và trong trường hợp dữ liệu được sử dụng

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
Đây chắc chắn là giải pháp thuận tiện nhất mà tôi từng thấy để chuyển đổi lambda sang con trỏ hàm kiểu C. Hàm lấy nó làm đối số sẽ chỉ cần một tham số phụ đại diện cho trạng thái của nó, thường được đặt tên là "void * user" trong thư viện C, để nó có thể chuyển nó tới con trỏ hàm khi gọi nó.
Codoscope

10

Sử dụng phương thức cục bộ toàn cục (tĩnh), nó có thể được thực hiện như sau

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

Giả sử chúng ta có

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

Vì vậy, cách sử dụng sẽ

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

Điều này hoạt động vì mỗi lambda có một chữ ký duy nhất nên việc làm cho nó tĩnh không phải là một vấn đề. Sau đây là một trình bao bọc chung với số lượng đối số khác nhau và bất kỳ kiểu trả về nào sử dụng cùng một phương thức.

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

Và cách sử dụng tương tự

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
lưu ý rằng điều này sẽ sao chép bao đóng (khi nhận ptr) + args (khi gọi). Nếu không, nó là một giải pháp thanh lịch
Ivan Sanz-Carasa


1
@ IvanSanz-Carasa Cảm ơn bạn đã chỉ ra. Các loại đóng cửa không phải là CopyAssignable, nhưng các bộ chức năng thì có. Vì vậy, bạn đúng, tốt hơn là sử dụng chuyển tiếp hoàn hảo ở đây. Mặt khác, đối với các args, chúng ta không thể làm gì nhiều vì C đơn giản không hỗ trợ các tham chiếu phổ quát, nhưng ít nhất chúng ta có thể chuyển tiếp các giá trị trở lại lambda của chúng ta. Điều này có thể tiết kiệm thêm một bản sao. Đã chỉnh sửa mã.
Vladimir Talybin

@RiaD Có, vì lambda một trường hợp tĩnh ở đây bạn sẽ cần phải nắm bắt bằng tham chiếu thay thế, ví dụ thay vì =sử dụng &itrong vòng lặp của bạn.
Vladimir Talybin

5

Hehe - một câu hỏi khá cũ, nhưng vẫn ...

#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

Có một cách rất hay để chuyển lambda bắt thành con trỏ hàm, nhưng bạn cần phải cẩn thận khi sử dụng nó:

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

Mã của bạn sau đó sẽ trông như thế này (cảnh báo: biên dịch não):

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

Giải pháp của tôi, chỉ cần sử dụng một con trỏ hàm để tham chiếu đến lambda tĩnh.

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

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.