แลมด้ากลับมาเอง: ถูกกฎหมายหรือไม่?


125

พิจารณาโปรแกรมที่ไร้ประโยชน์นี้:

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

โดยพื้นฐานแล้วเราพยายามสร้างแลมด้าที่คืนค่าตัวเอง

  • MSVC รวบรวมโปรแกรมและทำงาน
  • gcc รวบรวมโปรแกรมและ segfaults
  • เสียงดังปฏิเสธโปรแกรมด้วยข้อความ:

    error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined

คอมไพเลอร์ไหนถูก? มีการละเมิดข้อ จำกัด แบบคงที่ UB หรือไม่?

อัปเดตการปรับเปลี่ยนเล็กน้อยนี้ได้รับการยอมรับโดย clang:

  auto it = [&](auto& self, auto b) {
          std::cout << (a + b) << std::endl;
          return [&](auto p) { return self(self,p); };
  };
  it(it,4)(6)(42)(77)(999);

อัปเดต 2 : ฉันเข้าใจวิธีการเขียน functor ที่ส่งคืนตัวเองหรือวิธีใช้ Y combinator เพื่อให้บรรลุสิ่งนี้ นี่เป็นคำถามภาษาทนายมากกว่า

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

คำถามที่เกี่ยวข้อง: c ++ แลมบ์ดากลับมาเอง


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

2
คุณถามว่าถูกกฎหมายหรือไม่ซึ่งบอกว่านี่เป็นคำถามภาษาทนายความ แต่คำตอบหลายข้อไม่ได้ใช้แนวทางนั้นจริงๆ ... สิ่งสำคัญคือต้องทำให้แท็กถูกต้อง
Shafik Yaghmour

2
@ShafikYaghmour ขอบคุณแอดแท็ก
n. 'สรรพนาม' ม.

1
@ArneVogel ใช่สิ่งที่อัปเดตใช้auto& selfซึ่งช่วยขจัดปัญหาการอ้างอิงที่ห้อยลง
. 'สรรพนาม' ม.

1
@TheGreatDuck แลมด้า C ++ ไม่ใช่นิพจน์แลมด้าเชิงทฤษฎีจริงๆ C ++ มีชนิดเรียกซ้ำในตัวซึ่งแคลคูลัสแลมบ์ดาที่พิมพ์ง่ายแบบดั้งเดิมไม่สามารถแสดงออกได้ดังนั้นจึงสามารถมีไอโซมอร์ฟิกเป็น a: a-> a และโครงสร้างที่เป็นไปไม่ได้อื่น ๆ
. 'สรรพนาม' ม.

คำตอบ:


69

โปรแกรมมีรูปแบบไม่ถูกต้อง (เสียงดังถูก) ต่อ[dcl.spec.auto] / 9 :

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

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

แม้ว่าคุณจะไม่มีสิ่งนั้น แต่คุณก็มีข้อมูลอ้างอิงที่ห้อยอยู่


ให้ฉันอธิบายเพิ่มเติมหลังจากพูดคุยกับคนที่ฉลาดกว่ามาก (เช่น TC) มีความแตกต่างที่สำคัญระหว่างรหัสเดิม (ลดลงเล็กน้อย) และเวอร์ชันใหม่ที่เสนอ (ลดลงเช่นเดียวกัน):

auto f1 = [&](auto& self) {
  return [&](auto) { return self(self); } /* #1 */ ; /* #2 */
};
f1(f1)(0);

auto f2 = [&](auto& self, auto) {
  return [&](auto p) { return self(self,p); };
};
f2(f2, 0);

และนั่นคือการแสดงออกภายในself(self)ไม่ได้ขึ้นอยู่สำหรับf1แต่จะขึ้นอยู่สำหรับself(self, p) f2เมื่อนิพจน์ไม่ขึ้นกับนิพจน์สามารถใช้ ... อย่างกระตือรือร้น ( [temp.res] / 8เช่น howstatic_assert(false)ข้อผิดพลาดที่ยากจะเกิดขึ้นได้อย่างไรไม่ว่าเทมเพลตที่พบนั้นจะเป็นอินสแตนซ์หรือไม่ก็ตาม)

สำหรับf1คอมไพเลอร์ (เช่นพูดเสียงดัง) สามารถพยายามสร้างอินสแตนซ์นี้อย่างกระตือรือร้น คุณรู้ประเภทของแลมด้าด้านนอกที่อนุมานได้เมื่อคุณไปถึง;จุดนั้นที่จุด#2ข้างบน (เป็นประเภทของแลมบ์ดาด้านใน) แต่เรากำลังพยายามใช้ให้เร็วกว่านั้น (คิดว่าตรงจุดนั้น#1 ) - เรากำลังพยายาม เพื่อใช้ในขณะที่เรายังแยกวิเคราะห์แลมด้าด้านในก่อนที่เราจะรู้ว่ามันเป็นประเภทใด ที่ทำงานผิดปกติของ dcl.spec.auto/9

อย่างไรก็ตามสำหรับf2เราไม่สามารถพยายามสร้างอินสแตนซ์อย่างกระตือรือร้นได้เนื่องจากขึ้นอยู่กับ เราสามารถสร้างอินสแตนซ์ได้ที่จุดใช้งานเท่านั้นโดยที่เรารู้ทุกอย่าง


เพื่อที่จะทำจริงๆบางอย่างเช่นนี้คุณต้องมีY-Combinator การใช้งานจากกระดาษ:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

และสิ่งที่คุณต้องการคือ:

auto it = y_combinator([&](auto self, auto b){
    std::cout << (a + b) << std::endl;
    return self;
});

คุณจะระบุประเภทการส่งคืนอย่างชัดเจนได้อย่างไร ฉันคิดไม่ออก
Rakete1111

@ Rakete1111 คนไหน? ในต้นฉบับทำไม่ได้
Barry

โอวตกลง. ฉันไม่ใช่คนพื้นเมือง แต่ "ดังนั้นคุณต้องระบุประเภทการคืนสินค้าอย่างชัดเจน" ดูเหมือนจะบอกเป็นนัยว่ามีวิธีนั่นคือเหตุผลที่ฉันถาม :)
Rakete1111

4
@PedroA stackoverflow.com/users/2756719/tcเป็นผู้สนับสนุน C ++ นอกจากนี้เขายังไม่ใช่ AI หรือมีไหวพริบพอที่จะโน้มน้าวให้มนุษย์ที่มีความรู้เกี่ยวกับ C ++ เข้าร่วมการประชุมขนาดเล็กของ LWG ในชิคาโกเมื่อเร็ว ๆ นี้
Casey

3
@Casey หรือบางทีมนุษย์ก็แค่พูดในสิ่งที่ AI บอกเขา ... คุณไม่มีทางรู้หรอก;)
TC

34

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

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

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self); // <-- using reference to 'self'
      };
  };
  it(it)(4)(6)(42)(77)(999); // <-- 'self' is now out of scope
}

การรันโปรแกรมโดยvalgrindแสดงสิ่งนี้:

==5485== Memcheck, a memory error detector
==5485== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5485== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5485== Command: ./test
==5485== 
9
==5485== Use of uninitialised value of size 8
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485== 
==5485== Invalid read of size 4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  Address 0x4fefffdc4 is not stack'd, malloc'd or (recently) free'd
==5485== 
==5485== 
==5485== Process terminating with default action of signal 11 (SIGSEGV)
==5485==  Access not within mapped region at address 0x4FEFFFDC4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  If you believe this happened as a result of a stack
==5485==  overflow in your program's main thread (unlikely but
==5485==  possible), you can try to increase the size of the
==5485==  main thread stack using the --main-stacksize= flag.
==5485==  The main thread stack size used in this run was 8388608.

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

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto& self) { // <-- self is now a reference
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

ใช้งานได้:

==5492== Memcheck, a memory error detector
==5492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5492== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5492== Command: ./test
==5492== 
9
11
47
82
1004

ฉันไม่คุ้นเคยกับ lambdas ทั่วไป แต่คุณไม่สามารถselfอ้างอิงได้หรือไม่?
François Andrieux

@ FrançoisAndrieuxใช่ถ้าคุณทำการselfอ้างอิงปัญหานี้จะหายไปแต่ Clang ยังคงปฏิเสธด้วยเหตุผลอื่น
Justin

@ FrançoisAndrieuxแน่นอนและฉันได้เพิ่มคำตอบนั้นขอบคุณ!
TypeIA

ปัญหาของแนวทางนี้คือไม่สามารถกำจัดจุดบกพร่องของคอมไพเลอร์ได้ ดังนั้นอาจจะใช้งานได้ แต่การนำไปใช้งานเสีย
Shafik Yaghmour

ขอบคุณฉันดูสิ่งนี้มาหลายชั่วโมงแล้วและไม่เห็นว่าselfมีการอ้างอิง!
. 'สรรพนาม' ม.

21

TL; DR;

เสียงดังลั่นถูกต้อง

ดูเหมือนว่าส่วนของมาตรฐานที่ทำให้รูปแบบนี้ไม่ดีคือ[dcl.spec.auto] p9 :

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

auto n = n; // error, n’s initializer refers to n
auto f();
void g() { &f; } // error, f’s return type is unknown

auto sum(int i) {
  if (i == 1)
    return i; // sum’s return type is int
  else
    return sum(i-1)+i; // OK, sum’s return type has been deduced
}

- ส่งตัวอย่าง]

งานต้นฉบับผ่าน

หากเราดูข้อเสนอA Proposal to Add Y Combinator ไปยัง Standard Libraryจะมีวิธีแก้ปัญหาที่ใช้งานได้:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

และมันบอกอย่างชัดเจนว่าตัวอย่างของคุณเป็นไปไม่ได้:

C ++ 11/14 lambdas ไม่สนับสนุนการเรียกซ้ำ: ไม่มีวิธีอ้างอิงวัตถุแลมบ์ดาจากเนื้อหาของฟังก์ชันแลมด้า

และอ้างอิงถึงประเด็นที่ Richard Smith กล่าวถึงข้อผิดพลาดที่ส่งเสียงดังทำให้คุณ :

ฉันคิดว่านี่น่าจะดีกว่าในฐานะฟีเจอร์ภาษาชั้นหนึ่ง ฉันหมดเวลาสำหรับการประชุม Pre-Kona แต่ฉันตั้งใจจะเขียนกระดาษเพื่ออนุญาตให้ตั้งชื่อแลมบ์ดา (กำหนดขอบเขตให้กับตัวของมันเอง):

auto x = []fib(int a) { return a > 1 ? fib(a - 1) + fib(a - 2) : a; };

ที่นี่ 'fib' เทียบเท่ากับ lambda's * นี้ (มีกฎพิเศษที่น่ารำคาญเพื่อให้สิ่งนี้ทำงานได้แม้ว่าประเภทการปิดของ lambda จะไม่สมบูรณ์)

แบร์รี่ชี้ให้ฉันดูข้อเสนอต่อเนื่องRecursive lambdasซึ่งอธิบายว่าเหตุใดจึงเป็นไปไม่ได้และใช้งานได้กับdcl.spec.auto#9ข้อ จำกัด และยังแสดงวิธีการเพื่อให้บรรลุสิ่งนี้ในวันนี้โดยไม่มี:

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

ตัวอย่าง:

  void read(Socket sock, OutputBuffer buff) {
  sock.readsome([&] (Data data) {
  buff.append(data);
  sock.readsome(/*current lambda*/);
}).get();

}

ความพยายามโดยธรรมชาติอย่างหนึ่งในการอ้างอิงแลมบ์ดาจากตัวมันเองคือการเก็บไว้ในตัวแปรและจับตัวแปรนั้นโดยการอ้างอิง:

 auto on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

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

อีกแนวทางหนึ่งคือการใช้ std :: function:

 std::function on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

วิธีการนี้รวบรวม แต่โดยทั่วไปจะแนะนำการลงโทษที่เป็นนามธรรม: ฟังก์ชัน std :: อาจได้รับการจัดสรรหน่วยความจำและโดยทั่วไปแล้วการเรียกแลมบ์ดาจะต้องมีการเรียกทางอ้อม

สำหรับโซลูชันค่าใช้จ่ายที่เป็นศูนย์มักไม่มีแนวทางใดที่ดีไปกว่าการกำหนดประเภทคลาสโลคัลอย่างชัดเจน


@ Cheersandhth. -Alf ฉันพบคำพูดมาตรฐานหลังจากอ่านบทความดังนั้นจึงไม่เกี่ยวข้องเนื่องจากคำพูดมาตรฐานทำให้ชัดเจนว่าทำไมแนวทางทั้งสองจึงไม่ได้ผล
Shafik Yaghmour

"" ถ้าชื่อของเอนทิตีที่มีประเภทตัวยึดตำแหน่งที่ไม่ได้ลดทอนปรากฏในนิพจน์แสดงว่าโปรแกรมมีรูปแบบที่ไม่ถูกต้อง "ฉันไม่เห็นเหตุการณ์นี้เกิดขึ้นในโปรแกรมแม้ว่าselfดูเหมือนจะไม่เป็นเช่นนั้น
n. 'สรรพนาม' ม.

@nm นอกเหนือจากคำที่เป็นไปได้แล้วตัวอย่างดูเหมือนจะสมเหตุสมผลกับถ้อยคำและฉันเชื่อว่าตัวอย่างนี้แสดงให้เห็นถึงปัญหาอย่างชัดเจน ฉันไม่คิดว่าจะสามารถช่วยได้มากกว่านี้
Shafik Yaghmour

13

ดูเหมือนว่าเสียงดังพูดถูก พิจารณาตัวอย่างที่เรียบง่าย:

auto it = [](auto& self) {
    return [&self]() {
      return self(self);
    };
};
it(it);

มาดูกันว่าเป็นคอมไพเลอร์ (บิต):

  • ประเภทitคือLambda1กับตัวดำเนินการโทรแม่แบบ
  • it(it); เรียกใช้การสร้างอินสแตนซ์ของตัวดำเนินการโทร
  • ประเภทการส่งคืนของตัวดำเนินการเรียกเทมเพลตคือautoดังนั้นเราจึงต้องอนุมานมัน
  • Lambda1เราจะกลับมาแลมบ์ดาจับพารามิเตอร์แรกประเภท
  • แลมบ์ดานั้นมีตัวดำเนินการโทรด้วยซึ่งจะส่งคืนประเภทของการเรียกร้อง self(self)
  • หมายเหตุ: self(self)เป็นสิ่งที่เราเริ่มต้นด้วย!

ดังนั้นจึงไม่สามารถอนุมานประเภทได้


ประเภทการกลับมาของเป็นเพียงLambda1::operator() Lambda2จากนั้นภายในนิพจน์แลมบ์ดาภายในนั้นชนิดการส่งคืนของself(self)การเรียกของLambda1::operator()ก็เป็นที่รู้จักเช่นLambda2กัน อาจเป็นไปได้ว่ากฎที่เป็นทางการนั้นมีส่วนช่วยในการหักเงินเล็กน้อยนั้น แต่ตรรกะที่นำเสนอในที่นี้ไม่ได้ ตรรกะที่นี่เป็นเพียงการยืนยัน หากกฎที่เป็นทางการยืนขวางนั่นแสดงว่าเป็นข้อบกพร่องของกฎระเบียบ
ไชโยและ hth - Alf

@ Cheersandhth. -Alf ฉันยอมรับว่าประเภทการส่งคืนคือ Lambda2 แต่คุณรู้ว่าคุณไม่สามารถมีตัวดำเนินการโทรที่ไม่ได้รับการแก้ไขได้เพียงเพราะนี่คือสิ่งที่คุณเสนอ: ชะลอการหักประเภทการส่งคืนของผู้ให้บริการโทรของ Lambda2 แต่คุณไม่สามารถเปลี่ยนกฎสำหรับสิ่งนี้ได้เนื่องจากเป็นพื้นฐานที่ค่อนข้างดี
Rakete1111

9

รหัสของคุณใช้ไม่ได้ แต่สิ่งนี้ทำ:

template<class F>
struct ycombinator {
  F f;
  template<class...Args>
  auto operator()(Args&&...args){
    return f(f, std::forward<Args>(args)...);
  }
};
template<class F>
ycombinator(F) -> ycombinator<F>;

รหัสทดสอบ:

ycombinator bob = {[x=0](auto&& self)mutable{
  std::cout << ++x << "\n";
  ycombinator ret = {self};
  return ret;
}};

bob()()(); // prints 1 2 3

รหัสของคุณเป็นทั้ง UB และรูปแบบที่ไม่ถูกต้องไม่จำเป็นต้องมีการวินิจฉัย ซึ่งเป็นเรื่องตลก; แต่ทั้งสองอย่างสามารถแก้ไขได้อย่างอิสระ

อันดับแรก UB:

auto it = [&](auto self) { // outer
  return [&](auto b) { // inner
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};
it(it)(4)(5)(6);

นี่คือ UB เนื่องจากด้านนอกใช้selfตามค่าจากนั้นจับภายในselfโดยการอ้างอิงจากนั้นดำเนินการส่งคืนหลังจากouterเสร็จสิ้นการทำงาน ดังนั้น segfaulting ก็โอเคแน่นอน

การแก้ไข:

[&](auto self) {
  return [self,&a](auto b) {
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};

รหัสยังคงมีรูปแบบไม่ถูกต้อง เพื่อดูสิ่งนี้เราสามารถขยาย lambdas:

struct __outer_lambda__ {
  template<class T>
  auto operator()(T self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      T self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};
__outer_lambda__ it{a};
it(it);

อินสแตนซ์นี้__outer_lambda__::operator()<__outer_lambda__>:

  template<>
  auto __outer_lambda__::operator()(__outer_lambda__ self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};

ดังนั้นเราต้องกำหนดประเภทผลตอบแทนของ__outer_lambda__::operator().

เราผ่านมันทีละบรรทัด ก่อนอื่นเราสร้าง__inner_lambda__ประเภท:

    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };

ตอนนี้ดูมี - ประเภทผลตอบแทนที่เป็นหรือself(self) __outer_lambda__(__outer_lambda__ const&)แต่เรากำลังพยายามสรุปประเภทผลตอบแทนของ__outer_lambda__::operator()(__outer_lambda__).

คุณไม่ได้รับอนุญาตให้ทำเช่นนั้น

แม้ว่าในความเป็นจริงแล้วประเภทการส่งคืนของ__outer_lambda__::operator()(__outer_lambda__)ไม่ได้ขึ้นอยู่กับประเภทผลตอบแทนของ__inner_lambda__::operator()(int)C ++ ไม่สนใจเมื่อหักล้างประเภทผลตอบแทน เพียงแค่ตรวจสอบโค้ดทีละบรรทัด

และself(self)ถูกนำมาใช้ก่อนที่เราจะอนุมานได้ โปรแกรมที่ไม่ดี

เราสามารถแก้ไขได้โดยซ่อนไว้self(self)จนกว่าจะถึงภายหลัง:

template<class A, class B>
struct second_type_helper { using result=B; };

template<class A, class B>
using second_type = typename second_type_helper<A,B>::result;

int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [self,&a](auto b) {
        std::cout << (a + b) << std::endl;
        return self(second_type<decltype(b), decltype(self)&>(self) );
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

และตอนนี้รหัสถูกต้องและรวบรวม แต่ฉันคิดว่านี่เป็นการแฮ็คเล็กน้อย เพียงแค่ใช้ ycombinator


อาจเป็นไปได้ว่า (IDK) คำอธิบายนี้ถูกต้องสำหรับกฎอย่างเป็นทางการเกี่ยวกับ lambdas แต่ในแง่ของการเขียนเทมเพลตใหม่ประเภทการส่งคืนของเทมเพลตภายในของแลมบ์ดาoperator()ไม่สามารถอนุมานได้โดยทั่วไปจนกว่าจะมีการสร้างอินสแตนซ์ (โดยถูกเรียกด้วยอาร์กิวเมนต์บางประเภท) ดังนั้นการเขียนซ้ำเหมือนเครื่องด้วยตนเองเป็นรหัสตามเทมเพลตจึงทำงานได้ดี
ไชโยและ hth - Alf

@cheers รหัสของคุณแตกต่างกัน inner เป็นคลาสเทมเพลตในโค้ดของคุณ แต่ไม่ได้อยู่ในโค้ดของฉันหรือ OP และนั่นก็มีความสำคัญเนื่องจากเมธอดคลาสเทมเพลตจะล่าช้าในการสร้างอินสแตนซ์จนกว่าจะถูกเรียก
Yakk - Adam Nevraumont

คลาสที่กำหนดภายในฟังก์ชันเทมเพลตเทียบเท่ากับคลาสเท็มเพลตนอกฟังก์ชันนั้น การกำหนดมันนอกฟังก์ชันเป็นสิ่งที่จำเป็นสำหรับโค้ดสาธิตเมื่อมีฟังก์ชันสมาชิกที่เป็นเทมเพลตเนื่องจากกฎ C ++ ไม่อนุญาตเทมเพลตสมาชิกในคลาสที่ผู้ใช้กำหนดภายในเครื่อง ข้อ จำกัด อย่างเป็นทางการนั้นไม่ได้มีไว้สำหรับสิ่งที่คอมไพเลอร์สร้างขึ้นเอง
ไชโยและ hth - Alf

7

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

เมื่อเสร็จแล้วเป็นที่ชัดเจนว่าปัญหาหลักเป็นเพียงการอ้างอิงที่ห้อยลงและคอมไพเลอร์ที่ไม่ยอมรับรหัสนั้นค่อนข้างถูกท้าทายในแผนกแลมบ์ดา

การเขียนซ้ำแสดงให้เห็นว่าไม่มีการอ้างอิงแบบวงกลม

#include <iostream>

struct Outer
{
    int& a;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner( a, self );    //! Original code has dangling ref here.
    }

    struct Inner
    {
        int& a;
        Outer& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Outer& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

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

#include <iostream>

struct Outer
{
    int& a;

    template< class > class Inner;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner<Arg>( a, self );    //! Original code has dangling ref here.
    }

    template< class Self >
    struct Inner
    {
        int& a;
        Self& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Self& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

ฉันเดาว่ามันเป็นเทมเพลตนี้ในเครื่องจักรภายในซึ่งกฎอย่างเป็นทางการได้รับการออกแบบมาเพื่อห้าม หากพวกเขาห้ามสิ่งก่อสร้างเดิม


เห็นปัญหาคือtemplate< class > class Inner;แม่แบบoperator()คือ ... สร้างอินสแตนซ์? ดีคำผิด เขียน? ... ในช่วงOuter::operator()<Outer>ก่อนที่จะอนุมานประเภทการส่งคืนของตัวดำเนินการภายนอก และInner<Outer>::operator()มีสายเรียกเข้าOuter::operator()<Outer>นั่นเอง และไม่ได้รับอนุญาต ตอนนี้คอมไพเลอร์ส่วนใหญ่ไม่ทราบself(self)เพราะพวกเขารอที่จะอนุมานชนิดการส่งคืนของOuter::Inner<Outer>::operator()<int>เมื่อintถูกส่งผ่านไปใน. เหมาะสม แต่มันคิดถึงความไม่ดีของรหัส
Yakk - Adam Nevraumont

ฉันคิดว่าพวกเขาต้องรอเพื่ออนุมานประเภทการส่งคืนของเทมเพลตฟังก์ชันจนกว่าเทมเพลตฟังก์ชันInnner<T>::operator()<U>นั้นจะถูกสร้างอินสแตนซ์ หลังจากที่ประเภทผลตอบแทนทั้งหมดอาจขึ้นอยู่กับที่Uนี่ มันไม่ได้ แต่โดยทั่วไป
ไชโยและ hth - Alf

แน่ใจ; แต่การแสดงออกใด ๆ ที่ประเภทถูกกำหนดโดยการหักประเภทการส่งคืนที่ไม่สมบูรณ์ยังคงผิดกฎหมาย มีเพียงคอมไพเลอร์บางตัวเท่านั้นที่ขี้เกียจและไม่ตรวจสอบจนกว่าจะถึงเวลาต่อมาซึ่งจุดนี้จะใช้งานได้ตลอดไป
Yakk - Adam Nevraumont
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.