Tại sao lambda có kích thước 1 byte?


89

Tôi đang làm việc với bộ nhớ của một số lambdas trong C ++, nhưng tôi hơi khó hiểu với kích thước của chúng.

Đây là mã thử nghiệm của tôi:

#include <iostream>
#include <string>

int main()
{
  auto f = [](){ return 17; };
  std::cout << f() << std::endl;
  std::cout << &f << std::endl;
  std::cout << sizeof(f) << std::endl;
}

Bạn có thể chạy nó tại đây: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

Ouptut là:

17
0x7d90ba8f626f
1

Điều này cho thấy rằng kích thước lambda của tôi là 1.

  • Sao có thể như thế được?

  • Ít nhất thì lambda không phải là một con trỏ để thực hiện nó?


17
nó được triển khai dưới dạng một đối tượng hàm (a structvới an operator())
george_ptr

14
Và một cấu trúc trống không thể có kích thước 0 do đó kết quả là 1. Hãy thử chụp một cái gì đó và xem điều gì xảy ra với kích thước.
Mohamad Elghawi

2
Tại sao lambda phải là một con trỏ ??? Đó là một đối tượng có một tổng đài
Kerrek SB

7
Lambdas trong C ++ tồn tại tại thời gian biên dịch và các lời gọi được liên kết (hoặc thậm chí được nội tuyến) tại thời gian biên dịch hoặc liên kết. Do đó, không cần con trỏ thời gian chạy trong chính đối tượng. @KerrekSB Không phải là một phỏng đoán phi thường khi mong đợi rằng một lambda sẽ chứa một con trỏ hàm, vì hầu hết các ngôn ngữ triển khai lambda đều năng động hơn C ++.
Kyle Strand

2
@KerrekSB "vấn đề gì" - theo nghĩa nào? Các lý do một đối tượng đóng cửa thể để trống (chứ không phải chứa một con trỏ hàm) là bởi vì các chức năng được gọi là được biết đến tại thời gian biên dịch / link. Đây là những gì OP dường như đã hiểu nhầm. Tôi không thấy bình luận của bạn làm sáng tỏ mọi thứ như thế nào.
Kyle Strand

Câu trả lời:


107

Lambda được đề cập thực sự không có trạng thái .

Xem xét:

struct lambda {
  auto operator()() const { return 17; }
};

Và nếu chúng ta có lambda f;, nó là một lớp trống. Ở trên không chỉ giống về mặt lambdachức năng với lambda của bạn, mà (về cơ bản) cách lambda của bạn được triển khai! (Nó cũng cần một toán tử con trỏ hàm ẩn cho hàm và tên lambdasẽ được thay thế bằng một số giả hướng dẫn do trình biên dịch tạo ra)

Trong C ++, các đối tượng không phải là con trỏ. Chúng là những thứ thực tế. Họ chỉ sử dụng hết dung lượng cần thiết để lưu trữ dữ liệu trong đó. Một con trỏ tới một đối tượng có thể lớn hơn một đối tượng.

Mặc dù bạn có thể nghĩ lambda đó như một con trỏ đến một hàm, nhưng không phải vậy. Bạn không thể gán lại hàm auto f = [](){ return 17; };cho một hàm hoặc lambda khác!

 auto f = [](){ return 17; };
 f = [](){ return -42; };

trên là bất hợp pháp . Không có chỗ ở fđể lưu trữ chức năng sẽ được gọi là - đó thông tin được lưu trữ trong các loại của f, không tính vào giá trị của f!

Nếu bạn đã làm điều này:

int(*f)() = [](){ return 17; };

hoặc cái này:

std::function<int()> f = [](){ return 17; };

bạn không còn lưu trữ lambda trực tiếp nữa. Trong cả hai trường hợp này, f = [](){ return -42; }đều hợp pháp - vì vậy trong những trường hợp này, chúng tôi đang lưu trữ hàm chúng tôi đang gọi trong giá trị của f. Và sizeof(f)không còn nữa 1, mà là sizeof(int(*)())hoặc lớn hơn (về cơ bản, có kích thước con trỏ hoặc lớn hơn, như bạn mong đợi. std::functionCó kích thước tối thiểu được ngụ ý theo tiêu chuẩn (chúng phải có khả năng lưu trữ các vùng gọi "bên trong chính chúng" lên đến một kích thước nhất định) ít nhất là lớn bằng một con trỏ hàm trong thực tế).

Trong int(*f)()trường hợp này, bạn đang lưu trữ một con trỏ hàm đến một hàm hoạt động như thể bạn đã gọi lambda đó. Điều này chỉ hoạt động cho lambdas không trạng thái (những người có []danh sách chụp trống ).

Trong std::function<int()> ftrường hợp này, bạn đang tạo một phiên bản lớp xóa kiểu std::function<int()>(trong trường hợp này) sử dụng vị trí mới để lưu trữ bản sao của lambda size-1 trong bộ đệm nội bộ (và nếu một lambda lớn hơn được chuyển vào (với nhiều trạng thái hơn ), sẽ sử dụng phân bổ đống).

Theo dự đoán, những thứ như thế này có thể là những gì bạn nghĩ đang diễn ra. Lambda đó là một đối tượng có kiểu được mô tả bằng chữ ký của nó. Trong C ++, người ta quyết định làm cho lambdas bằng không chi phí trừu tượng so với việc triển khai đối tượng hàm thủ công. Điều này cho phép bạn chuyển lambda vào một stdthuật toán (hoặc tương tự) và nội dung của nó hiển thị đầy đủ với trình biên dịch khi nó khởi tạo mẫu thuật toán. Nếu lambda có kiểu như thế std::function<void(int)>, nội dung của nó sẽ không hiển thị đầy đủ và đối tượng hàm được tạo thủ công có thể nhanh hơn.

Mục tiêu của tiêu chuẩn hóa C ++ là lập trình cấp cao với chi phí bằng không đối với mã C thủ công.

Bây giờ bạn hiểu rằng fthực tế của bạn là không trạng thái, sẽ có một câu hỏi khác trong đầu bạn: lambda không có trạng thái. Tại sao nó không có kích thước 0?


Có câu trả lời ngắn gọn.

Tất cả các đối tượng trong C ++ phải có kích thước tối thiểu là 1 theo tiêu chuẩn và hai đối tượng cùng loại không thể có cùng địa chỉ. Chúng được kết nối với nhau, bởi vì một mảng kiểu Tsẽ có các phần tử được đặt sizeof(T)cách nhau.

Bây giờ, vì nó không có trạng thái, đôi khi nó có thể chiếm không gian. Điều này không thể xảy ra khi nó ở "một mình", nhưng trong một số ngữ cảnh, nó có thể xảy ra. std::tuplevà mã thư viện tương tự khai thác thực tế này. Đây là cách nó làm việc:

Vì lambda tương đương với một lớp có operator()quá tải, lambda không trạng thái (với một []danh sách chụp) là tất cả các lớp trống. Họ có sizeofcủa 1. Trên thực tế, nếu bạn kế thừa từ chúng (được phép!), Chúng sẽ không chiếm dung lượng miễn là nó không gây ra xung đột địa chỉ cùng loại . (Đây được gọi là tối ưu hóa cơ sở trống).

template<class T>
struct toy:T {
  toy(toy const&)=default;
  toy(toy &&)=default;
  toy(T const&t):T(t) {}
  toy(T &&t):T(std::move(t)) {}
  int state = 0;
};

template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }

những sizeof(make_toy( []{std::cout << "hello world!\n"; } ))sizeof(int)(tốt, ở trên là bất hợp pháp vì bạn không thể tạo ra một lambda trong một bối cảnh không đánh giá: bạn phải tạo một tên auto toy = make_toy(blah);thì làm sizeof(blah), nhưng đó chỉ là tiếng ồn). sizeof([]{std::cout << "hello world!\n"; })vẫn là 1(trình độ tương tự).

Nếu chúng tôi tạo một loại đồ chơi khác:

template<class T>
struct toy2:T {
  toy2(toy2 const&)=default;
  toy2(T const&t):T(t), t2(t) {}
  T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }

này có hai bản sao của lambda. Vì họ không thể chia sẻ cùng một địa chỉ, sizeof(toy2(some_lambda))2!


6
Nit: Một con trỏ hàm có thể nhỏ hơn khoảng trống *. Hai ví dụ lịch sử: Đầu tiên là các máy được đánh địa chỉ từ trong đó sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * và char * cần thêm một số bit để giữ khoảng trống trong một từ) Thứ hai là mô hình bộ nhớ 8086 trong đó void * / int * là phân đoạn + bù đắp và có thể bao gồm tất cả bộ nhớ, nhưng các hàm được trang bị trong một phân đoạn 64K ( vì vậy một con trỏ hàm chỉ có 16 bit).
Martin Bonner ủng hộ Monica

1
@martin true. Đã thêm ()vào.
Yakk - Adam Nevraumont

50

Một lambda không phải là một con trỏ hàm.

Lambda là một thể hiện của một lớp. Mã của bạn gần tương đương với:

class f_lambda {
public:

  auto operator() { return 17; }
};

f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;

Lớp nội bộ đại diện cho lambda không có thành viên lớp, do đó nó sizeof()là 1 (nó không thể là 0, vì những lý do đã được nêu đầy đủ ở nơi khác ).

Nếu lambda của bạn nắm bắt một số biến, chúng sẽ tương đương với các thành viên trong lớp và bạn sizeof()sẽ chỉ ra tương ứng.


3
Bạn có thể liên kết đến "nơi khác", điều này giải thích tại sao sizeof()không thể là 0?
user1717828

26

Trình biên dịch của bạn ít nhiều sẽ dịch lambda sang kiểu cấu trúc sau:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Vì cấu trúc đó không có thành viên không tĩnh, nên nó có cùng kích thước với cấu trúc trống 1.

Điều đó thay đổi ngay sau khi bạn thêm danh sách chụp không trống vào lambda của mình:

int i = 42;
auto f = [i]() { return i; };

Cái nào sẽ dịch sang

struct _SomeInternalName {
    int i;
    _SomeInternalName(int outer_i) : i(outer_i) {}
    int operator()() { return i; }
};


int main()
{
     int i = 42;
     _SomeInternalName f(i);
     std::cout << f() << std::endl;
}

Vì cấu trúc được tạo bây giờ cần lưu trữ một intthành viên không tĩnh để thu thập, kích thước của nó sẽ tăng lên sizeof(int). Kích thước sẽ tiếp tục tăng lên khi bạn chụp được nhiều thứ hơn.

(Vui lòng ví cấu trúc tương tự với một hạt muối. Mặc dù đó là một cách hay để lý luận về cách lambdas hoạt động bên trong, nhưng đây không phải là bản dịch theo nghĩa đen của những gì trình biên dịch sẽ làm)


12

Có phải lambda, tại mimumum, là một con trỏ để thực hiện nó?

Không cần thiết. Theo tiêu chuẩn, kích thước của lớp không tên, duy nhất được xác định bởi thực thi . Trích từ [expr.prim.lambda] , C ++ 14 (mỏ nhấn mạnh):

Kiểu của biểu thức lambda (cũng là kiểu của đối tượng bao đóng) là một kiểu lớp nonunion duy nhất, chưa được đặt tên - được gọi là kiểu bao đóng - có các thuộc tính được mô tả bên dưới.

[...]

Việc triển khai có thể xác định kiểu đóng khác với kiểu được mô tả bên dưới miễn là điều này không làm thay đổi hành vi quan sát được của chương trình ngoài việc thay đổi :

- kích thước và / hoặc sự liên kết của kiểu đóng cửa ,

- liệu kiểu đóng cửa có thể sao chép một cách tầm thường hay không (Điều 9),

- liệu kiểu đóng có phải là lớp bố trí tiêu chuẩn (Điều 9) hay không

- kiểu đóng có phải là lớp POD hay không (Khoản 9)

Trong trường hợp của bạn - đối với trình biên dịch bạn sử dụng - bạn nhận được kích thước là 1, điều đó không có nghĩa là nó cố định. Nó có thể khác nhau giữa các triển khai trình biên dịch khác nhau.


Bạn có chắc chắn điều này áp dụng? Một lambda không có nhóm chụp không thực sự là một "vùng đóng". (Có phải tiêu chuẩn gọi lambdas của nhóm bắt trống là "đóng cửa" không?)
Kyle Strand

1
Có, nó có. Đây là những gì tiêu chuẩn nói " Việc đánh giá một biểu thức lambda dẫn đến một giá trị tạm thời. Tạm thời này được gọi là đối tượng đóng. ", Có nắm bắt hay không, đó là một đối tượng đóng, chỉ là một đối tượng sẽ bị vô hiệu hóa giá trị tăng.
Legends2k

Tôi không phản đối, nhưng có thể người ủng hộ không nghĩ câu trả lời này có giá trị vì nó không giải thích tại sao có thể (từ góc độ lý thuyết, không phải từ góc độ tiêu chuẩn) để triển khai lambdas mà không bao gồm con trỏ thời gian chạy tới chức năng điều hành cuộc gọi. (Xem cuộc thảo luận của tôi với KerrekSB dưới câu hỏi.)
Kyle Strand

7

Từ http://en.cppreference.com/w/cpp/language/lambda :

Biểu thức lambda xây dựng một đối tượng tạm thời prvalue chưa được đặt tên của loại lớp không tổng hợp không liên hiệp, không được đặt tên duy nhất, được gọi là loại bao đóng , đượ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 chứa biểu thức lambda.

Nếu biểu thức lambda nắm bắt bất kỳ thứ gì bằng cách sao chép (hoặc ngầm định với mệnh đề capture [=] hoặc rõ ràng với một lệnh capture không bao gồm ký tự &, ví dụ: [a, b, c]), thì kiểu đóng bao gồm dữ liệu không tĩnh không được đặt tên các thành viên , được khai báo theo thứ tự không xác định, giữ bản sao của tất cả các thực thể đã bị bắt.

Đối với các thực thể được ghi lại bằng tham chiếu (với chụp mặc định [&] hoặc khi sử dụng ký tự &, ví dụ: [& a, & b, & c]), không xác định được nếu các thành viên dữ liệu bổ sung được khai báo trong kiểu đóng

Từ http://en.cppreference.com/w/cpp/language/sizeof

Khi được áp dụng cho một loại lớp trống, luôn trả về 1.

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.