Vượt qua bắt lambda như con trỏ hàm


210

Có thể truyền một hàm lambda như một con trỏ hàm không? Nếu vậy, tôi phải làm điều gì đó không chính xác vì tôi đang gặp lỗi biên dịch.

Hãy xem xét ví dụ sau

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

Khi tôi cố gắng biên dịch này , tôi gặp lỗi biên dịch sau:

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&&'

Đó là một trong những thông báo lỗi để tiêu hóa, nhưng tôi nghĩ điều tôi nhận ra là lambda không thể được coi là một constexprvì vậy tôi không thể chuyển nó như một con trỏ hàm? Tôi cũng đã thử làm xconst, nhưng điều đó dường như không có ích.


34
lambda có thể phân rã thành con trỏ hàm chỉ khi chúng không bắt được bất cứ thứ gì.
Jarod42


Đối với hậu thế, bài đăng trên blog được liên kết ở trên hiện đang tồn tại tại devbloss.microsoft.com/oldnewthing/20150220-00/?p=44623
warrenm

Câu trả lời:


205

Một lambda chỉ có thể được chuyển đổi sang một con trỏ hàm nếu nó không chụp, từ dự thảo C ++ 11 tiêu chuẩn phần 5.1.2 [expr.prim.lambda] nói ( tôi nhấn mạnh ):

Kiểu đóng cho biểu thức lambda không có lambda-Capturehàm chuyển đổi const không ảo không công khai thành con trỏ thành hàm có cùng tham số và trả về kiểu như toán tử gọi hàm của kiểu đóng. Giá trị được trả về bởi hàm chuyển đổi này sẽ là địa chỉ của hàm, khi được gọi, có tác dụng tương tự như gọi toán tử gọi hàm của kiểu đóng.

Lưu ý, cppreference cũng bao gồm điều này trong phần của họ về các hàm Lambda .

Vì vậy, các lựa chọn thay thế sau đây sẽ hoạt động:

typedef bool(*DecisionFn)(int);

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

và điều này cũng sẽ như vậy:

typedef bool(*DecisionFn)();

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

và như 5gon12eder chỉ ra, bạn cũng có thể sử dụng std::function, nhưng lưu ý rằng đó std::functionlà trọng lượng nặng , vì vậy nó không phải là một sự đánh đổi ít chi phí.


2
Lưu ý bên lề: Một giải pháp phổ biến được sử dụng bởi công cụ C là truyền một void*tham số duy nhất. Nó thường được gọi là "con trỏ người dùng". Nó cũng tương đối nhẹ, nhưng có xu hướng yêu cầu bạn mallocra ngoài một số không gian.
Vụ kiện của Quỹ Monica

94

Câu trả lời của Shafik Yaghmour giải thích chính xác lý do tại sao lambda không thể được chuyển qua như một con trỏ hàm nếu nó có một bản chụp. Tôi muốn hiển thị hai bản sửa lỗi đơn giản cho sự cố.

  1. Sử dụng std::functionthay cho con trỏ hàm thô.

    Đây là một giải pháp rất sạch sẽ. Tuy nhiên, xin lưu ý rằng nó bao gồm một số chi phí bổ sung cho kiểu xóa (có thể là một cuộc gọi chức năng ảo).

    #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. Sử dụng biểu thức lambda không thu được bất cứ thứ gì.

    Vì vị từ của bạn thực sự chỉ là hằng số boolean, nên những điều sau đây sẽ nhanh chóng giải quyết vấn đề hiện tại. Xem câu trả lời này để được giải thích tốt tại sao và làm thế nào điều này đang hoạt động.

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

4
@TC Xem câu hỏi này để biết chi tiết lý do tại sao nó hoạt động
Shafik Yaghmour 27/2/2015

Lưu ý rằng, nói chung, nếu bạn biết dữ liệu chụp vào thời gian biên dịch, bạn có thể chuyển đổi nó thành nhập dữ liệu và sau đó bạn quay lại để có lambda mà không chụp - hãy xem câu trả lời này mà tôi vừa viết cho một câu hỏi khác (cảm ơn @ Câu trả lời của 5gon12eder tại đây).
dan-man

Không phải đối tượng có tuổi thọ dài hơn chức năng con trỏ sao? Tôi muốn sử dụng nó cho glutReshapeFunc.
ar2015

Tôi không đề xuất đề xuất này, những thứ có xu hướng hoạt động kỳ diệu, giới thiệu các lỗi mới. và thực hành đi cùng với những lỗi đó. nếu bạn muốn sử dụng hàm std ::, bạn sẽ thấy tất cả các cách mà hàm std :: có thể được sử dụng. bởi vì một số cách có thể là một cái gì đó mà bạn không muốn.
TheNegative

1
Điều này không trả lời câu hỏi. Nếu một người có thể sử dụng std::functionhoặc lambda - tại sao họ sẽ không? Ít nhất đó là một cú pháp dễ đọc hơn. Thông thường, người ta cần sử dụng một con trỏ hàm để tương tác với các thư viện C (thực ra, với bất kỳ thư viện bên ngoài nào) và chắc chắn rằng bạn không thể sửa đổi nó để chấp nhận hàm std :: function hoặc lambda.
Hi-Angel

40

Các biểu thức Lambda, thậm chí là các biểu thức được bắt giữ, có thể được xử lý như một con trỏ hàm (con trỏ tới hàm thành viên).

Đó là khó khăn vì một biểu thức lambda không phải là một chức năng đơn giản. Nó thực sự là một đối tượng với một toán tử ().

Khi bạn sáng tạo, bạn có thể sử dụng điều này! Hãy nghĩ về một lớp "hàm" theo kiểu std :: function. Nếu bạn lưu đối tượng, bạn cũng có thể sử dụng con trỏ hàm.

Để sử dụng con trỏ hàm, bạn có thể sử dụng như sau:

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;

Để xây dựng một lớp có thể bắt đầu hoạt động như một "std :: function", trước tiên bạn cần một lớp / struct hơn là có thể lưu trữ đối tượng và con trỏ hàm. Ngoài ra, bạn cần một toán tử () để thực thi nó:

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

Với điều này, giờ đây bạn có thể chạy lambdas bị bắt, không bị bắt, giống như bạn đang sử dụng bản gốc:

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

Mã này hoạt động với VS2015

Cập nhật 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;
}

Ồ, thật tuyệt với! Vì vậy, chúng ta chỉ có thể sử dụng các con trỏ bên trong lớp của lambda (cho toán tử hàm thành viên ()) để gọi lambdas được lưu trữ trong một lớp bao bọc !! KINH NGẠC!! Tại sao chúng ta cần đến hàm std ::? Và có thể tạo lambda_expression <Dectype (lambda), int, int, int> để tự động suy ra / các tham số "int" này trực tiếp từ chính lambda đã qua không?
barney

2
Tôi đã thêm một phiên bản ngắn của mã của riêng tôi. điều này sẽ hoạt động với một auto f = make :: function (lambda) đơn giản; Nhưng tôi khá chắc chắn bạn sẽ thấy nhiều tình huống mã của tôi sẽ không hoạt động. chức năng std :: được xây dựng tốt hơn so với điều này và nên đi đến khi bạn đang làm việc. Đây là cho giáo dục và sử dụng cá nhân.
Noxxer

14
Giải pháp này liên quan đến việc gọi lambda thông qua operator()triển khai, vì vậy nếu tôi đọc đúng thì tôi không nghĩ rằng việc gọi lambda bằng con trỏ hàm kiểu C sẽ rất hiệu quả? Đó là những gì câu hỏi ban đầu yêu cầu.
Rémy Lebeau

13
Bạn tuyên bố rằng lambdas có thể được xử lý như con trỏ hàm, điều mà bạn không làm. Bạn đã tạo một đối tượng khác để giữ lambda, không có gì, bạn chỉ có thể sử dụng lambda ban đầu.
qua đường vào

9
Đây không phải là "chuyển bắt lambda như con trỏ hàm". Đây là "truyền bắt lambda như một đối tượng có chứa một con trỏ hàm trong số những thứ khác". Có một thế giới của sự khác biệt.
n. 'đại từ' m.

15

Chụp lambdas không thể được chuyển đổi thành con trỏ hàm, vì câu trả lời này đã chỉ ra.

Tuy nhiên, việc cung cấp một con trỏ hàm cho một API chỉ chấp nhận một điều khá khó khăn. Phương pháp thường được trích dẫn nhất để làm như vậy là cung cấp một hàm và gọi một đối tượng tĩnh với nó.

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

Điều này là tẻ nhạt. Chúng tôi đưa ý tưởng này đi xa hơn và tự động hóa quá trình tạo ra wrappervà làm cho cuộc sống dễ dàng hơn nhiều.

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

Và sử dụng nó như là

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

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

Trực tiếp

Điều này về cơ bản là khai báo một hàm ẩn danh tại mỗi lần xuất hiện của fnptr .

Lưu ý rằng các yêu cầu fnptrghi đè lên các tên callablegọi được viết trước đó cùng loại. Chúng tôi khắc phục điều này, ở một mức độ nhất định, với inttham số N.

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

buộc số nguyên N được khai báo sẽ là một cách thanh lịch để ghi nhớ máy khách để tránh ghi đè các con trỏ hàm tại thời điểm biên dịch.
fiorentinoing

2

Một lối tắt để sử dụng lambda với con trỏ hàm C là:

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

Sử dụng Curl làm exmample ( thông tin gỡ lỗi curl )

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
Mà lambda không có một chụp. Vấn đề của OP là bắt giữ, không phải suy ra kiểu con trỏ hàm (đó là những gì mà +mánh khóe mang lại cho bạn).
Sneftel

2

Mặc dù cách tiếp cận mẫu là thông minh vì nhiều lý do, điều quan trọng là phải nhớ vòng đời của lambda và các biến được bắt. Nếu bất kỳ hình thức nào của một con trỏ lambda sẽ được sử dụng và lambda không phải là sự tiếp tục đi xuống, thì chỉ nên sử dụng một bản sao [=] lambda. Tức là, ngay cả khi đó, việc bắt một con trỏ tới một biến trên ngăn xếp là UNSAFE nếu thời gian tồn tại của các con trỏ bị bắt (stack bung) ngắn hơn thời gian của lambda.

Một giải pháp đơn giản hơn để bắt lambda làm con trỏ là:

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

ví dụ, new std::function<void()>([=]() -> void {...}

Chỉ cần nhớ sau này delete pLamdbađể đảm bảo rằng bạn không bị rò rỉ bộ nhớ lambda. Bí mật để nhận ra ở đây là lambdas có thể nắm bắt lambdas (tự hỏi cách thức hoạt động của nó) và std::functionđể hoạt động chung, việc triển khai lambda cần phải chứa đủ thông tin nội bộ để cung cấp quyền truy cập vào kích thước của dữ liệu lambda (và bị bắt) ( đó là lý do tại sao deletenên hoạt động [chạy các hàm hủy của các kiểu bị bắt]).


Tại sao phải bận tâm với newchức năng - std :: đã lưu trữ lambda trên heap VÀ tránh cần phải nhớ xóa cuộc gọi.
Chris Dodd

0

Không phải là một câu trả lời trực tiếp, nhưng một biến thể nhỏ để sử dụng mẫu mẫu "functor" để ẩn đi các chi tiết cụ thể của loại lambda và giữ cho mã đẹp và đơn giản.

Tôi không chắc bạn muốn sử dụng lớp quyết định như thế nào nên tôi phải mở rộng lớp bằng một hàm sử dụng nó. Xem ví dụ đầy đủ tại đây: https://godbolt.org/z/jtByqE

Hình thức cơ bản của lớp học của bạn có thể trông như thế này:

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

Nơi bạn chuyển loại hàm trong một phần của loại lớp được sử dụng như:

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

Một lần nữa, tôi không chắc tại sao bạn lại bắt xnó có ý nghĩa hơn (với tôi) để có một tham số mà bạn truyền vào lambda) để bạn có thể sử dụng như:

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

Xem liên kết cho một ví dụ đầy đủ


-2

Như đã được đề cập bởi những người khác, bạn có thể thay thế hàm Lambda thay vì con trỏ hàm. Tôi đang sử dụng phương thức này trong giao diện C ++ của mình để giải RKSUITE F77 ODE.

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