การสร้างรหัสแลมบ์ดา C ++ พร้อมการเริ่มต้นใน C ++ 14


9

ฉันกำลังพยายามที่จะเข้าใจ / อธิบายรหัสโค้ดที่สร้างขึ้นเมื่อส่งผ่านการจับไปยัง lambdas โดยเฉพาะอย่างยิ่งในการจับภาพเริ่มต้นทั่วไปที่เพิ่มใน C ++ 14

ให้ตัวอย่างรหัสต่อไปนี้ด้านล่างนี้เป็นความเข้าใจปัจจุบันของฉันในสิ่งที่คอมไพเลอร์จะสร้าง

กรณีที่ 1: การดักจับตามค่า / การดักจับเริ่มต้นตามค่า

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

จะถือเอา:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

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

กรณีที่ 2: การดักจับโดยการอ้างอิง / การดักจับเริ่มต้นโดยการอ้างอิง

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

จะถือเอา:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

พารามิเตอร์เป็นการอ้างอิงและสมาชิกเป็นการอ้างอิงดังนั้นจึงไม่มีการคัดลอก ดีสำหรับประเภทเช่นเวกเตอร์ ฯลฯ

กรณีที่ 3:

การเริ่มต้นการจับภาพทั่วไป

auto lambda = [x = 33]() { std::cout << x << std::endl; };

สถานะที่อยู่ภายใต้ของฉันคือสิ่งนี้คล้ายกับกรณีที่ 1 ในแง่ที่ว่ามันถูกคัดลอกไปยังสมาชิก

ฉันเดาว่าคอมไพเลอร์สร้างรหัสคล้ายกับ ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

นอกจากนี้ถ้าฉันมีต่อไปนี้:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

ตัวสร้างจะมีลักษณะอย่างไร มันย้ายไปยังสมาชิกด้วยหรือไม่


1
@ rafix07 ในกรณีนั้นรหัสความเข้าใจที่สร้างขึ้นจะไม่ได้รวบรวม (มันพยายามที่จะคัดลอกเริ่มต้นสมาชิก ptr ที่ไม่ซ้ำกันจากการโต้แย้ง) cppinsights มีประโยชน์ในการรับส่วนทั่วไป แต่ก็ไม่สามารถตอบคำถามนี้ได้อย่างชัดเจน
Max Langhof

คุณคิดว่ามีการแปลแลมบ์ดาเป็น functors เป็นขั้นตอนแรกของการรวบรวมหรือคุณแค่มองหาโค้ดที่เทียบเท่า (เช่น. พฤติกรรมเดียวกัน)? วิธีที่คอมไพเลอร์สร้างโค้ด (และโค้ดที่สร้าง) จะขึ้นอยู่กับคอมไพเลอร์เวอร์ชั่นสถาปัตยกรรมแฟล็กและอื่น ๆ ดังนั้นคุณจะขอแพลตฟอร์มเฉพาะหรือไม่ ถ้าไม่คำถามของคุณไม่สามารถตอบได้จริงๆ นอกเหนือจากรหัสที่สร้างขึ้นจริงอาจมีประสิทธิภาพมากกว่าฟังก์ชั่นที่คุณแสดงรายการ (เช่นตัวสร้างแบบอินไลน์หลีกเลี่ยงการคัดลอกที่ไม่จำเป็น ฯลฯ )
Sander De Dycker

2
หากคุณกำลังสนใจในสิ่งที่มาตรฐาน c ++ ได้กล่าวเกี่ยวกับมันหมายถึง[expr.prim.lambda] มันมากเกินไปที่จะสรุปที่นี่เป็นคำตอบ
Sander De Dycker

คำตอบ:


2

คำถามนี้ไม่สามารถตอบได้อย่างสมบูรณ์ในรหัส คุณอาจจะสามารถเขียนรหัส "เทียบเท่า" ได้บ้าง แต่ไม่ได้ระบุมาตรฐานดังกล่าว

เมื่อออกนอกเส้นทางมา[expr.prim.lambda]เริ่มกันเลย สิ่งแรกที่ควรทราบคือตัวสร้างจะกล่าวถึงเฉพาะใน[expr.prim.lambda.closure]/13:

ประเภทการปิดที่สัมพันธ์กับlambda-expressionไม่มี constructor เริ่มต้นหากlambda-expressionมีlambda-captureและ default constructor เริ่มต้นเป็นอย่างอื่น มันมีตัวสร้างการคัดลอกเริ่มต้นและตัวสร้างการย้ายเริ่มต้น ([class.copy.ctor]) มีตัวดำเนินการกำหนดค่าการคัดลอกที่ถูกลบหากlambda-expressionมีlambda-captureและตัวดำเนินการคัดลอกและย้ายที่เป็นค่าเริ่มต้นเป็นอย่างอื่น ([class.copy.assign]) [ หมายเหตุ:ฟังก์ชั่นสมาชิกพิเศษเหล่านี้มีการกำหนดโดยนัยตามปกติและอาจถูกกำหนดเป็นลบ - บันทึกท้าย ]

ดังนั้นทันทีที่ค้างคาวควรมีความชัดเจนว่าคอนสตรัคเตอร์ไม่ได้เป็นทางการตามที่กำหนดไว้ในการถ่ายภาพวัตถุ คุณสามารถเข้าใกล้ได้ (ดูคำตอบ cppinsights.io) แต่รายละเอียดแตกต่างกัน (โปรดสังเกตว่ารหัสในคำตอบนั้นสำหรับกรณีที่ 4 ไม่ได้รวบรวม)


นี่เป็นมาตราฐานหลักที่จำเป็นในการหารือกรณีที่ 1:

[expr.prim.lambda.capture]/10

[... ]
สำหรับแต่ละเอนทิตีที่บันทึกโดยการคัดลอกสมาชิกข้อมูลที่ไม่คงที่ที่ไม่มีชื่อจะถูกประกาศในประเภทการปิด คำสั่งประกาศของสมาชิกเหล่านี้ไม่ได้ระบุไว้ ประเภทของสมาชิกข้อมูลดังกล่าวเป็นประเภทอ้างอิงถ้ากิจการเป็นการอ้างอิงไปยังวัตถุการอ้างอิง lvalue ไปยังประเภทฟังก์ชั่นอ้างอิงถ้ากิจการเป็นการอ้างอิงไปยังฟังก์ชั่นหรือประเภทของกิจการที่ถูกจับที่สอดคล้องกันเป็นอย่างอื่น สมาชิกของสหภาพที่ไม่ระบุชื่อจะไม่ถูกคัดลอก

[expr.prim.lambda.capture]/11

idทุกนิพจน์ภายในคำสั่งผสมของแลมบ์ดา - นิพจน์ที่เป็นการใช้ประโยชน์จากเอนทิตีที่ดักจับโดยการคัดลอกจะถูกแปลงเป็นการเข้าถึงสมาชิกข้อมูลที่ไม่มีชื่อที่สอดคล้องกันของประเภทการปิด [ ... ]

[expr.prim.lambda.capture]/15

เมื่อประเมินแลมบ์ดานิพจน์เอนทิตีที่ถูกจับโดยการคัดลอกจะถูกใช้เพื่อกำหนดค่าเริ่มต้นให้กับสมาชิกข้อมูลที่ไม่คงที่ที่สอดคล้องกันของวัตถุปิดที่เกิดขึ้นและสมาชิกข้อมูลที่ไม่คงที่ที่สอดคล้องกับการเริ่มต้น ระบุโดย initializer ที่สอดคล้องกัน (ซึ่งอาจเป็นการคัดลอกหรือการกำหนดค่าเริ่มต้นโดยตรง) [ ... ]

ลองนำสิ่งนี้ไปใช้กับกรณีของคุณ 1:

กรณีที่ 1: การดักจับตามค่า / การดักจับเริ่มต้นตามค่า

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

ประเภทปิดแลมบ์ดานี้จะมีชื่อไม่คงที่ข้อมูลสมาชิก (ขอเรียกว่า__x) ประเภทint(ตั้งแต่xจะไม่อ้างอิงหรือฟังก์ชั่น) และการเข้าถึงภายในร่างกายแลมบ์ดาจะเปลี่ยนการเข้าถึงx __xเมื่อเราประเมินการแสดงออกแลมบ์ดา (เช่นเมื่อกำหนดไปlambda) เราโดยตรงการเริ่มต้น ด้วย__xx

กล่าวโดยย่อจะมีเพียงหนึ่งสำเนาเท่านั้น Constructor ของประเภทการปิดไม่เกี่ยวข้องและเป็นไปไม่ได้ที่จะแสดงสิ่งนี้ใน "ปกติ" C ++ (โปรดทราบว่าประเภทการปิดไม่ได้เป็นประเภทรวม )


การอ้างอิงที่เกี่ยวข้องกับ[expr.prim.lambda.capture]/12:

เอนทิตีถูกจับโดยการอ้างอิงถ้ามันถูกจับโดยปริยายหรืออย่างชัดเจน แต่ไม่ถูกจับโดยการคัดลอก มันไม่ได้ระบุว่าสมาชิกข้อมูลที่ไม่คงที่ไม่มีชื่อเพิ่มเติมจะประกาศในประเภทการปิดสำหรับหน่วยงานที่บันทึกโดยอ้างอิง [ ... ]

มีอีกย่อหน้าเกี่ยวกับการอ้างอิงการอ้างอิง แต่เราไม่ได้ทำอย่างนั้น

ดังนั้นสำหรับกรณีที่ 2:

กรณีที่ 2: การดักจับโดยการอ้างอิง / การดักจับเริ่มต้นโดยการอ้างอิง

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

เราไม่ทราบว่ามีการเพิ่มสมาชิกประเภทการปิดหรือไม่ xในร่างกายแลมบ์ดาอาจจะหมายถึงxภายนอกโดยตรง นี่ขึ้นอยู่กับคอมไพเลอร์เพื่อคิดออกและมันจะทำในภาษากลางบางรูปแบบ (ซึ่งแตกต่างจากคอมไพเลอร์เป็นคอมไพเลอร์) ไม่ใช่การแปลงซอร์สของรหัส C ++


เริ่มการดักจับมีรายละเอียดใน[expr.prim.lambda.capture]/6:

การเริ่มต้นการจับภาพจะทำงานราวกับว่ามันประกาศและจับตัวแปรของรูปแบบauto init-capture ;ที่มีภูมิภาคการประกาศเป็นคำสั่งผสมของแลมบ์ดานิพจน์ยกเว้นว่า:

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

ให้ดูที่กรณีที่ 3:

กรณีที่ 3: การจับภาพเริ่มต้นทั่วไป

auto lambda = [x = 33]() { std::cout << x << std::endl; };

ตามที่ระบุไว้ลองจินตนาการว่านี่เป็นตัวแปรที่ถูกสร้างขึ้นauto x = 33;และคัดลอกโดยชัดเจน ตัวแปรนี้เป็นเพียง "มองเห็นได้" ภายในร่างกายแลมบ์ดา ดังที่กล่าวไว้[expr.prim.lambda.capture]/15ก่อนหน้านี้การกำหนดค่าเริ่มต้นของสมาชิกประเภทปิด ( __xสำหรับลูกหลาน) ที่สอดคล้องกันนั้นเริ่มต้นจากการประเมินค่าของการแสดงออกแลมบ์ดา

สำหรับการหลีกเลี่ยงข้อสงสัย: นี่ไม่ได้หมายความว่าจะมีการเตรียมใช้งานสองครั้งที่นี่ auto x = 33;เป็น "ราวกับว่า" จะได้รับมรดกความหมายของการจับภาพที่เรียบง่ายและการเริ่มต้นอธิบายคือการปรับเปลี่ยนความหมายเหล่านั้น การเริ่มต้นเพียงครั้งเดียวเกิดขึ้น

นอกจากนี้ยังครอบคลุมถึงกรณีที่ 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

สมาชิกประเภทการปิดจะเริ่มต้นได้โดย__p = std::move(unique_ptr_var)เมื่อมีการประเมินการแสดงออกของแลมบ์ดา (เช่นเมื่อlถูกกำหนดให้) การเข้าถึงในร่างกายแลมบ์ดาจะกลายเป็นเข้าถึงp__p


TL; DR: เฉพาะการคัดลอก / การกำหนดค่าเริ่มต้น / การเคลื่อนย้ายที่น้อยที่สุดเท่านั้นที่ทำได้ (ดังที่คาดหวัง / คาดหวัง) ฉันจะสมมติว่า lambdas ไม่ได้ระบุไว้ในแง่ของการแปลงแหล่งที่มา (ต่างจากน้ำตาล syntactic อื่น ๆ ) อย่างแน่นอนเพราะการแสดงสิ่งต่าง ๆ ในแง่ของการก่อสร้างจะทำให้การดำเนินการฟุ่มเฟือย

ฉันหวังว่าสิ่งนี้จะทำให้เกิดความกลัวในคำถาม :)


9

กรณีที่ 1 [x](){} : Constructor ที่สร้างขึ้นจะยอมรับอาร์กิวเมนต์โดยconstการอ้างอิงที่มีคุณสมบัติเหมาะสมเพื่อหลีกเลี่ยงการทำสำเนาที่ไม่จำเป็น:

__some_compiler_generated_name(const int& x) : x_{x}{}

กรณีที่ 2 [x&](){} : สมมติฐานของคุณที่นี่ถูกต้องถูกxส่งผ่านและจัดเก็บโดยการอ้างอิง


กรณีที่ 3 [x = 33](){} : ถูกต้องอีกครั้งxจะเริ่มต้นโดยค่า


กรณีที่ 4 [p = std::move(unique_ptr_var)] : ตัวสร้างจะมีลักษณะเช่นนี้:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

ดังนั้นใช่unique_ptr_varคือ "ย้ายเข้า" การปิด โปรดดูรายการ 32 ของ Scott Meyer ใน C ++ ที่มีประสิทธิภาพทันสมัย ​​("ใช้การจับภาพเริ่มต้นเพื่อย้ายวัตถุเข้าสู่การปิด")


"- มีconstคุณสมบัติ" ทำไม
cpplearner

@cpplearner Mh เป็นคำถามที่ดี ฉันเดาว่าฉันแทรกเพราะหนึ่งในจิตอัตโนมัติเหล่านั้นเตะใน ^^ อย่างน้อยconstก็ไม่สามารถทำร้ายที่นี่เนื่องจากความกำกวม / การจับคู่ที่ดีขึ้นเมื่อไม่ใช่ - constอื่น ๆ อย่างไรก็ตามคุณคิดว่าฉันควรจะลบconstหรือไม่
lubgr

ฉันคิดว่า const ควรอยู่ต่อไปจะเป็นอย่างไรถ้าข้อโต้แย้งที่ส่งผ่านไปยังแท้จริงแล้วคือ const?
Aconcagua

คุณกำลังบอกว่ามีสิ่งปลูกสร้างสองอย่าง (หรือคัดลอก) เกิดขึ้นที่นี่?
Max Langhof

ขออภัยฉันหมายถึงในกรณีที่ 4 (สำหรับการเคลื่อนไหว) และกรณีที่ 1 (สำหรับสำเนา) ส่วนที่คัดลอกมาจากคำถามของฉันไม่สมเหตุสมผลตามข้อความของคุณ (แต่ฉันถามคำถามเหล่านั้น)
Max Langhof

5

ไม่จำเป็นต้องคาดเดาน้อยลงโดยใช้cppinsights.io cppinsights.io

กรณีที่ 1:
รหัส

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

คอมไพเลอร์สร้าง

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

กรณีที่ 2:
รหัส

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

คอมไพเลอร์สร้าง

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

กรณีที่ 3:
รหัส

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

คอมไพเลอร์สร้าง

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

กรณีที่ 4 (ไม่เป็นทางการ):
รหัส

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

คอมไพเลอร์สร้าง

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

และฉันเชื่อว่ารหัสสุดท้ายนี้จะตอบคำถามของคุณ การย้ายเกิดขึ้น แต่ไม่ใช่ [ทางเทคนิค] ในตัวสร้าง

จับตัวเองไม่ได้constแต่คุณจะเห็นว่าoperator()ฟังก์ชั่นคือ mutableธรรมชาติถ้าคุณจำเป็นต้องปรับเปลี่ยนการจับคุณทำเครื่องหมายแลมบ์ดาเป็น


รหัสที่คุณแสดงสำหรับกรณีสุดท้ายไม่ได้รวบรวม ข้อสรุป "การย้ายเกิดขึ้น แต่ไม่สามารถสนับสนุน [ทางเทคนิค] ในตัวสร้าง" ได้
Max Langhof

หลักจรรยาบรรณของเคส 4 ส่วนใหญ่จะรวบรวมบน Mac ของฉันอย่างแน่นอน ฉันรู้สึกประหลาดใจที่รหัสที่ขยายเพิ่มจาก cppinsightsไม่ได้รวบรวม เว็บไซต์นี้มีความน่าเชื่อถือมากสำหรับฉัน ฉันจะแจ้งปัญหากับพวกเขา แก้ไข: ฉันยืนยันว่ารหัสที่สร้างขึ้นไม่ได้รวบรวม ที่ไม่ชัดเจนหากไม่มีการแก้ไขนี้
sweenish

1
ลิงก์ไปที่ปัญหาในกรณีที่สนใจ: github.com/andreasfertig/cppinsights/issues/258 ฉันยังคงแนะนำไซต์สำหรับสิ่งต่าง ๆ เช่นการทดสอบ SFINAE และไม่ว่าจะมีการปลดเปลื้องโดยปริยายหรือไม่
sweenish
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.