2016-11-22 39 views
29

Trong C++, có thể tách khai báo và định nghĩa các hàm. Ví dụ, nó là khá bình thường để khai báo một hàm:Chuyển tiếp khai báo lambdas bằng C++

int Foo(int x); 

trong Foo.h và thực hiện nó trong Foo.cpp. Có thể làm điều gì đó tương tự với lambdas? Ví dụ, xác định một

std::function<int(int)> bar; 

trong bar.h và thực hiện nó trong bar.cpp như:

std::function<int(int)> bar = [](int n) 
{ 
    if (n >= 5) 
     return n; 
    return n*(n + 1); 
}; 

Disclaimer: Tôi có kinh nghiệm với lambdas trong C#, nhưng tôi đã không sử dụng chúng trong C++ rất nhiều .

+32

Nếu bạn định đặt tên cho lambda, bạn cũng có thể làm cho nó trở thành một hàm bình thường. – Brian

+0

@Brian Tôi biết và điều đó có ý nghĩa. Lý do chính khiến tôi đặt câu hỏi là tò mò và tôi muốn biết khả năng của nó. – MxNx

+8

FYI; 'std :: function ' có thể lưu trữ lambdas; nhưng lambdas không có kiểu 'std :: function <...>'; mà đúng hơn là có loại riêng biệt không tên riêng của họ. Bạn có thể lưu trữ một lambda trực tiếp trong một biến nếu loại biến đó được suy luận (hoặc thông qua 'auto' hoặc loại trừ đối số mẫu). 'std :: function' có một hàm tạo mẫu có thể lấy một đối số lambda thông qua nguyên tắc loại trừ đối số mẫu này. – Mankarse

Trả lời

36

Bạn không thể tách khai báo và định nghĩa của lambdas, không chuyển tiếp khai báo. Kiểu của nó là kiểu đóng không tên được khai báo với biểu thức lambda. Nhưng bạn có thể làm điều đó với các đối tượng std::function, được thiết kế để có thể lưu trữ bất kỳ mục tiêu có thể gọi nào, bao gồm cả biểu thức lambda. Khi mã mẫu của bạn hiển thị bạn đã sử dụng std::function, chỉ cần lưu ý rằng đối với trường hợp này, bar là biến toàn cục thực sự và bạn cần sử dụng extern trong tệp tiêu đề để biến nó thành một tuyên bố (không phải định nghĩa).

// bar.h 
extern std::function<int(int)> bar;  // declaration 

// bar.cpp 
std::function<int(int)> bar = [](int n) // definition 
{ 
    if (n >= 5) return n; 
    return n*(n + 1); 
}; 

Lưu ý một lần nữa rằng đây không phải là khai báo và định nghĩa của lambda riêng biệt; Nó chỉ là khai báo và định nghĩa riêng biệt của một biến toàn cầu bar với loại std::function<int(int)>, được khởi tạo từ một biểu thức lambda.

+2

Mẫu mã rất minh họa. – MxNx

+1

@Mzhr lưu ý rằng kiểu lambda không phải là std :: function, nhưng một kiểu chưa được đặt tên duy nhất cho lambda. std :: function là một wrapper đa hình xung quanh bất kỳ đối tượng có thể gọi. –

+0

@GuillaumeRacicot cảm ơn. – MxNx

8

Nói đúng ra bạn có thể không

Trích dẫn từ cpp reference

Biểu thức lambda là một biểu hiện prvalue có giá trị là (cho đến khi C++ 17) mà đối tượng kết quả là (vì C++ 17) một đối tượng tạm thời chưa được đặt tên của loại không tổng hợp không liên kết không tên duy nhất, được gọi là loại , được khai báo (cho mục đích của ADL) trong phạm vi khối nhỏ nhất, phạm vi lớp hoặc phạm vi không gian tên có chứa lambda expres sion

Vì vậy lambda là đối tượng tạm thời chưa đặt tên. Bạn có thể liên kết lambda với đối tượng giá trị l (ví dụ: std::function) và theo các quy tắc thông thường về khai báo biến, bạn có thể tách riêng khai báo và định nghĩa.

+0

Cảm ơn bạn đã phản hồi chi tiết. Tôi có thể hỏi về nguồn trích dẫn của bạn không? Là nó từ một đặc điểm kỹ thuật C + +? – MxNx

6

Tuyên bố chuyển tiếp không phải là cụm từ chính xác vì lambdas trong C++ là đối tượng chứ không phải hàm. Mã:

std::function<int(int)> bar; 

tuyên bố biến và bạn không bị buộc phải gán (loại này có giá trị mặc định là "con trỏ không có hàm"). Bạn có thể biên dịch ngay cả các cuộc gọi đến nó ...ví dụ: mã:

#include <functional> 
#include <iostream> 

int main(int argc, const char *argv[]) { 
    std::function<int(int)> bar; 
    std::cout << bar(21) << "\n"; 
    return 0; 
} 

sẽ biên dịch gọn gàng (nhưng tất nhiên sẽ hoạt động điên cuồng khi chạy).

Điều đó nói rằng bạn có thể gán một lambda cho một biến std::function tương thích và bổ sung thêm ví dụ:

bar = [](int x){ return x*2; }; 

ngay trước khi cuộc gọi sẽ cho kết quả trong một chương trình biên dịch tốt và tạo ra như đầu ra 42.

Một vài điều không rõ ràng có thể gây ngạc nhiên về lambdas trong C++ (nếu bạn biết các ngôn ngữ khác có khái niệm này) là

  • Mỗi lambda [..](...){...} có loại không tương thích khác nhau, ngay cả khi chữ ký hoàn toàn giống hệt nhau. Ví dụ bạn không thể khai báo một tham số kiểu lambda bởi vì cách duy nhất là sử dụng một cái gì đó như decltype([] ...) nhưng sau đó sẽ không có cách nào để gọi hàm như bất kỳ biểu mẫu []... nào khác tại một trang gọi sẽ không tương thích. Điều này được giải quyết bởi std::function vì vậy nếu bạn phải vượt qua lambda xung quanh hoặc lưu trữ chúng trong các thùng chứa, bạn phải sử dụng std::function.

  • Lambdas có thể thu thập địa phương theo giá trị (nhưng chúng là const trừ khi bạn khai báo lambda mutable) hoặc tham chiếu (nhưng đảm bảo tuổi thọ của đối tượng được tham chiếu sẽ không ngắn hơn thời gian của lambda là tùy thuộc vào lập trình viên). C++ không có bộ thu gom rác và đây là thứ cần thiết để giải quyết chính xác vấn đề "funarg upward" (bạn có thể làm việc xung quanh bằng cách bắt con trỏ thông minh, nhưng bạn phải chú ý đến các vòng tham chiếu để tránh rò rỉ).

  • Khác với các ngôn ngữ khác lambdas có thể được sao chép và khi bạn sao chép chúng, bạn sẽ chụp nhanh các biến giá trị theo giá trị nội bộ của chúng. Điều này có thể rất đáng ngạc nhiên đối với trạng thái có thể thay đổi và đây là lý do tôi cho rằng lý do mà giá trị theo giá trị được chụp là const theo mặc định.

Một cách để hợp lý hóa và ghi nhớ nhiều chi tiết về lambdas là mã như:

std::function<int(int)> timesK(int k) { 
    return [k](int x){ return x*k; }; 
} 

là cơ bản giống như

std::function<int(int)> timesK(int k) { 
    struct __Lambda6502 { 
     int k; 
     __Lambda6502(int k) : k(k) {} 
     int operator()(int x) { 
      return x * k; 
     } 
    }; 
    return __Lambda6502(k); 
} 

với một sự khác biệt tinh tế mà ngay cả lambda chụp tài liệu tham khảo có thể được sao chép (thông thường các lớp có chứa tham chiếu là thành viên không thể).

Các vấn đề liên quan