ทำไมแลมบ์ดาถึงมีขนาด 1 ไบต์?


90

ฉันกำลังทำงานกับหน่วยความจำของ lambdas บางตัวใน C ++ แต่ฉันงงกับขนาดของมันเล็กน้อย

นี่คือรหัสทดสอบของฉัน:

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

คุณสามารถเรียกใช้ได้ที่นี่: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

ouptut คือ:

17
0x7d90ba8f626f
1

นี่แสดงให้เห็นว่าขนาดของแลมด้าของฉันคือ 1

  • เป็นไปได้อย่างไร?

  • อย่างน้อยแลมด้าไม่ควรเป็นตัวชี้การนำไปใช้งานหรือไม่?


17
มันถูกนำไปใช้เป็นวัตถุฟังก์ชัน (a structwith an operator())
george_ptr

14
และโครงสร้างว่างต้องมีขนาด 0 ไม่ได้ด้วยเหตุนี้ 1 ผลลัพธ์ ลองจับภาพบางสิ่งและดูว่าเกิดอะไรขึ้นกับขนาด
Mohamad Elghawi

2
ทำไมแลมด้าต้องเป็นตัวชี้ ??? มันเป็นวัตถุที่มีตัวดำเนินการโทร
Kerrek SB

7
Lambdas ใน C ++ มีอยู่ในเวลาคอมไพล์และการเรียกใช้จะเชื่อมโยง (หรือแม้กระทั่งอินไลน์) ในเวลาคอมไพล์หรือลิงก์ ดังนั้นจึงไม่จำเป็นต้องมีตัวชี้รันไทม์ในวัตถุ @KerrekSB ไม่ใช่การคาดเดาที่ผิดปกติที่จะคาดหวังว่าแลมบ์ดาจะมีตัวชี้ฟังก์ชันเนื่องจากภาษาส่วนใหญ่ที่ใช้แลมบ์ดานั้นมีไดนามิกมากกว่า C ++
Kyle Strand

2
@KerrekSB "เรื่องอะไร" - ในแง่ไหน? เหตุผลวัตถุปิดสามารถเว้นว่าง (มากกว่าที่มีตัวชี้ฟังก์ชัน) เป็นเพราะฟังก์ชั่นที่จะเรียกว่าเป็นที่รู้จักกันที่รวบรวมเวลาการเชื่อมโยง / นี่คือสิ่งที่ OP ดูเหมือนจะเข้าใจผิด ฉันไม่เห็นว่าความคิดเห็นของคุณชี้แจงสิ่งต่างๆอย่างไร
Kyle Strand

คำตอบ:


108

แลมบ์ดาในคำถามจริงมีรัฐไม่มี

ตรวจสอบ:

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

และถ้าเรามีlambda f;มันก็เป็นคลาสที่ว่างเปล่า ไม่เพียง แต่lambdaฟังก์ชันข้างต้นจะคล้ายกับแลมด้าของคุณเท่านั้น แต่ยังเป็นวิธีการนำแลมด้าของคุณไปใช้งาน (โดยทั่วไป)! (นอกจากนี้ยังต้องการตัวดำเนินการชี้โดยนัยเพื่อใช้งานตัวชี้และชื่อlambdaจะถูกแทนที่ด้วย pseudo-guid ที่สร้างโดยคอมไพเลอร์)

ใน C ++ วัตถุไม่ใช่ตัวชี้ เป็นสิ่งที่เกิดขึ้นจริง พวกเขาใช้พื้นที่ที่จำเป็นในการจัดเก็บข้อมูลในนั้นเท่านั้น ตัวชี้ไปยังวัตถุอาจมีขนาดใหญ่กว่าวัตถุ

แม้ว่าคุณอาจคิดว่าแลมด้านั้นเป็นตัวชี้ไปยังฟังก์ชัน แต่ก็ไม่ใช่ คุณไม่สามารถกำหนดauto f = [](){ return 17; };ให้เป็นฟังก์ชันอื่นหรือแลมด้าได้!

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

ดังกล่าวเป็นความผิดกฎหมาย มีห้องพักไม่มีคือfการจัดเก็บซึ่งฟังก์ชั่นเป็นไปได้เรียกว่า - ว่าข้อมูลจะถูกเก็บไว้ในประเภทของการfไม่ได้อยู่ในค่าของf!

หากคุณทำสิ่งนี้:

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

หรือสิ่งนี้:

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

คุณไม่ได้จัดเก็บแลมด้าโดยตรงอีกต่อไป ในทั้งสองกรณีนี้f = [](){ return -42; }เป็นกฎหมาย - ดังนั้นในกรณีเหล่านี้เรามีการจัดเก็บที่fทำงานเราจะกล่าวอ้างในค่าของ และsizeof(f)ไม่มีอีกต่อไป1แต่จะมากกว่าsizeof(int(*)())หรือใหญ่กว่า (โดยทั่วไปให้มีขนาดตัวชี้หรือใหญ่ขึ้นตามที่คุณคาดหวัง std::functionมีขนาดขั้นต่ำที่บ่งบอกโดยนัยตามมาตรฐาน (พวกเขาต้องสามารถจัดเก็บคำเรียกที่ "ภายในตัวเอง" ได้ถึงขนาดที่กำหนด) ซึ่ง อย่างน้อยก็มีขนาดใหญ่เท่ากับตัวชี้ฟังก์ชันในทางปฏิบัติ)

ในint(*f)()กรณีนี้คุณกำลังจัดเก็บตัวชี้ฟังก์ชันไปยังฟังก์ชันที่ทำงานราวกับว่าคุณเรียกแลมด้านั้น ใช้ได้เฉพาะกับ lambdas ไร้สัญชาติเท่านั้น ( []รายการที่มีรายการจับว่าง)

ในstd::function<int()> fกรณีนี้คุณกำลังสร้างstd::function<int()>อินสแตนซ์คลาส type-erasure ที่ (ในกรณีนี้) ใช้ตำแหน่งใหม่ในการจัดเก็บสำเนาของแลมบ์ดาขนาด -1 ในบัฟเฟอร์ภายใน (และหากมีการส่งแลมบ์ดาที่ใหญ่กว่า (ที่มีสถานะมากกว่า ) จะใช้การจัดสรรฮีป)

เดาว่าสิ่งเหล่านี้น่าจะเป็นสิ่งที่คุณคิดว่าเกิดขึ้น แลมด้าเป็นวัตถุที่มีการอธิบายประเภทด้วยลายเซ็น ใน C ++ มีการตัดสินใจที่จะสร้างabstractions ต้นทุนแลมบ์ดาสเป็นศูนย์สำหรับการใช้อ็อบเจ็กต์ฟังก์ชันแมนนวล วิธีนี้ช่วยให้คุณส่งแลมด้าไปยังstdอัลกอริทึม (หรือที่คล้ายกัน) และให้คอมไพเลอร์มองเห็นเนื้อหาได้อย่างสมบูรณ์เมื่อสร้างอินสแตนซ์เทมเพลตอัลกอริทึม ถ้าแลมด้ามีประเภทเหมือนstd::function<void(int)>เนื้อหาของมันจะไม่สามารถมองเห็นได้อย่างสมบูรณ์และอ็อบเจ็กต์ฟังก์ชันที่สร้างขึ้นด้วยมืออาจเร็วกว่า

เป้าหมายของการกำหนดมาตรฐาน C ++ คือการเขียนโปรแกรมระดับสูงโดยมีค่าใช้จ่ายเป็นศูนย์เหนือโค้ด C ที่สร้างขึ้นด้วยมือ

ตอนนี้คุณเข้าใจแล้วว่าคุณfเป็นคนไร้สัญชาติจริงๆแล้วควรมีคำถามอื่นในหัวของคุณนั่นคือแลมด้าไม่มีสถานะ ทำไมขนาดไม่มี0?


มีคำตอบสั้น ๆ

อ็อบเจ็กต์ทั้งหมดใน C ++ ต้องมีขนาดเล็กสุดเป็น 1 ภายใต้มาตรฐานและออบเจ็กต์ประเภทเดียวกันสองชิ้นไม่สามารถมีแอดเดรสเดียวกันได้ สิ่งเหล่านี้เชื่อมต่อกันเนื่องจากอาร์เรย์ของประเภทTจะมีการวางองค์ประกอบsizeof(T)แยก

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

เนื่องจากแลมบ์ดาเทียบเท่ากับคลาสที่มีแลมด้าที่มีoperator()ภาระงานมากเกินไปแลมบ์ดาไร้สัญชาติ (พร้อม[]รายการจับภาพ) จึงเป็นคลาสว่างทั้งหมด พวกเขามีของsizeof 1ในความเป็นจริงหากคุณได้รับมรดกจากพวกเขา (ซึ่งได้รับอนุญาต!) พวกเขาจะไม่ใช้พื้นที่ตราบใดที่ไม่ทำให้เกิดการชนกันของที่อยู่ประเภทเดียวกัน (ซึ่งเรียกว่าการเพิ่มประสิทธิภาพฐานว่าง)

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

sizeof(make_toy( []{std::cout << "hello world!\n"; } ))คือsizeof(int)(ดีดังกล่าวข้างต้นเป็นสิ่งผิดกฎหมายเพราะคุณไม่สามารถสร้างแลมบ์ดาในบริบทที่ไม่ใช่การประเมิน: คุณต้องสร้างชื่อauto toy = make_toy(blah);แล้วทำsizeof(blah)แต่ที่เป็นเพียงเสียง) sizeof([]{std::cout << "hello world!\n"; })ยังคงเป็น1(คุณสมบัติที่คล้ายกัน)

ถ้าเราสร้างของเล่นประเภทอื่น:

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

นี่มีแลมด้าสองสำเนา เนื่องจากไม่สามารถใช้ที่อยู่เดียวกันsizeof(toy2(some_lambda))ได้2!


6
Nit: ตัวชี้ฟังก์ชันอาจมีขนาดเล็กกว่าโมฆะ * สองตัวอย่างในอดีต: คำแรกที่จ่าหน้าถึงเครื่องโดยที่ sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *) (เป็นโมฆะ * และถ่าน * ต้องการบิตพิเศษเพื่อเก็บค่าชดเชยภายในคำ) ประการที่สองโมเดลหน่วยความจำ 8086 โดยที่ void * / int * เป็นเซ็กเมนต์ + ออฟเซ็ตและสามารถครอบคลุมหน่วยความจำทั้งหมดได้ แต่ฟังก์ชันที่ติดตั้งไว้ภายในส่วน 64K เดียว ( ตัวชี้ฟังก์ชันจึงมีเพียง 16 บิต)
Martin Bonner สนับสนุน Monica

1
@martin จริง. ()เพิ่มพิเศษ
Yakk - Adam Nevraumont

50

แลมบ์ดาไม่ใช่ตัวชี้ฟังก์ชัน

แลมบ์ดาเป็นตัวอย่างของคลาส รหัสของคุณเทียบเท่ากับ:

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;

คลาสภายในที่แสดงถึงแลมบ์ดาไม่มีสมาชิกคลาสจึงsizeof()เป็น 1 (ไม่สามารถเป็น 0 ได้ด้วยเหตุผลที่ระบุไว้อย่างเพียงพอที่อื่น )

หากแลมบ์ดาของคุณจับตัวแปรบางตัวตัวแปรเหล่านั้นจะเทียบเท่ากับสมาชิกชั้นเรียนและคุณsizeof()จะระบุตามนั้น


3
คุณสามารถเชื่อมโยงไปยัง "ที่อื่น" ซึ่งอธิบายว่าเหตุใดจึงsizeof()ไม่สามารถเป็น 0 ได้
user1717828

26

คอมไพเลอร์ของคุณมากหรือน้อยแปลแลมบ์ดาเป็นประเภทโครงสร้างต่อไปนี้:

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

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

1เนื่องจากโครงสร้างที่ไม่มีสมาชิกไม่คงที่ก็มีขนาดเท่ากันเป็นโครงสร้างที่ว่างเปล่าซึ่งเป็น

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

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

ซึ่งจะแปลเป็น

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

ตั้งแต่ struct สร้างขณะนี้ความต้องการในการจัดเก็บไม่คงที่สมาชิกสำหรับการจับภาพที่ขนาดของมันจะเติบโตint sizeof(int)ขนาดจะใหญ่ขึ้นเรื่อย ๆ เมื่อคุณจับสิ่งของได้มากขึ้น

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


12

แลมบ์ดาไม่ควรเป็นตัวชี้ไปที่การใช้งาน?

ไม่จำเป็น. ตามมาตรฐานขนาดของคลาสที่ไม่ซ้ำกันและไม่มีชื่อจะถูกกำหนดให้ใช้งานได้ ตัดตอนมาจาก[expr.prim.lambda] , C ++ 14 (ของฉันเน้น):

ประเภทของแลมบ์ดานิพจน์ (ซึ่งเป็นชนิดของอ็อบเจ็กต์การปิดด้วย) เป็นประเภทคลาส nonunion ที่ไม่ซ้ำกันซึ่งเรียกว่าประเภทการปิดซึ่งมีการอธิบายคุณสมบัติไว้ด้านล่าง

[... ]

การใช้งานอาจกำหนดประเภทการปิดแตกต่างไปจากที่อธิบายไว้ด้านล่างหากสิ่งนี้ไม่เปลี่ยนแปลงพฤติกรรมที่สังเกตได้ของโปรแกรมนอกเหนือจากการเปลี่ยนแปลง :

- ขนาดและ / หรือการจัดตำแหน่งของประเภทปิด ,

- ประเภทการปิดสามารถคัดลอกได้เล็กน้อยหรือไม่ (ข้อ 9)

- ประเภทการปิดเป็นคลาสเลย์เอาต์มาตรฐาน (ข้อ 9) หรือไม่

- ประเภทการปิดเป็นคลาส POD หรือไม่ (ข้อ 9)

ในกรณีของคุณ - สำหรับคอมไพเลอร์ที่คุณใช้คุณจะได้ขนาด 1 ซึ่งไม่ได้หมายความว่ามันคงที่ อาจแตกต่างกันไประหว่างการใช้งานคอมไพเลอร์ที่แตกต่างกัน


คุณแน่ใจหรือไม่ว่าบิตนี้ใช้ได้ แลมด้าที่ไม่มีกลุ่มจับภาพไม่ใช่ "การปิด" จริงๆ (มาตรฐานอ้างถึง lambdas กลุ่มดักจับที่ว่างเปล่าว่า "ปิด" หรือไม่)
Kyle Strand

1
ใช่. นี่คือสิ่งที่มาตรฐานระบุว่า " การประเมินค่าแลมบ์ดานิพจน์ส่งผลให้เกิด prvalue ชั่วคราวชั่วคราวนี้เรียกว่าวัตถุปิด " การจับภาพหรือไม่เป็นวัตถุปิดเพียงอย่างเดียวนั้นจะเป็นโมฆะของค่าที่เพิ่มขึ้น
legends2k

ฉันไม่ได้ลงคะแนน แต่อาจเป็นไปได้ว่าผู้โหวตไม่คิดว่าคำตอบนี้มีค่าเพราะมันไม่ได้อธิบายว่าทำไมจึงเป็นไปได้ (จากมุมมองทางทฤษฎีไม่ใช่มุมมองมาตรฐาน) ที่จะใช้ lambdas โดยไม่รวมตัวชี้รันไทม์ไปที่ ฟังก์ชันโทร - โอเปอเรเตอร์ (ดูการสนทนาของฉันกับ KerrekSB ภายใต้คำถาม)
Kyle Strand

7

จากhttp://en.cppreference.com/w/cpp/language/lambda :

นิพจน์แลมบ์ดาสร้างอ็อบเจ็กต์ชั่วคราวที่ไม่มีชื่อ prvalue ของประเภทคลาส non-union non-aggregate ที่ไม่ซ้ำกันซึ่งเรียกว่าประเภทการปิดซึ่งถูกประกาศ (สำหรับวัตถุประสงค์ของ ADL) ในขอบเขตบล็อกที่เล็กที่สุดขอบเขตคลาสหรือขอบเขตเนมสเปซที่มี การแสดงออกของแลมบ์ดา

หากนิพจน์แลมบ์ดาจับสิ่งใด ๆ โดยการคัดลอก (โดยปริยายด้วยประโยคการจับ [=] หรือโดยชัดแจ้งด้วยการจับที่ไม่มีอักขระ & เช่น [a, b, c]) ประเภทการปิดจะมีข้อมูลที่ไม่คงที่ที่ไม่มีชื่อ สมาชิกที่ประกาศตามลำดับที่ไม่ระบุซึ่งถือสำเนาของเอนทิตีทั้งหมดที่ถูกจับ

สำหรับเอนทิตีที่จับโดยการอ้างอิง (ด้วยการจับค่าเริ่มต้น [&] หรือเมื่อใช้อักขระ & เช่น [& a, & b, & c]) จะไม่ระบุหากมีการประกาศสมาชิกข้อมูลเพิ่มเติมในประเภทการปิด

จากhttp://en.cppreference.com/w/cpp/language/sizeof

เมื่อนำไปใช้กับประเภทคลาสว่างจะส่งกลับ 1 เสมอ

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