Lambda trở lại chính nó: điều này là hợp pháp?


124

Hãy xem xét chương trình khá vô dụng này:

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

Về cơ bản chúng tôi đang cố gắng tạo ra một lambda tự trả về.

  • MSVC biên dịch chương trình, và nó chạy
  • gcc biên dịch chương trình, và nó segfaults
  • clang từ chối chương trình với một tin nhắn:

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

Trình biên dịch nào đúng? Có một vi phạm ràng buộc tĩnh, UB, hoặc không?

Cập nhật sửa đổi nhỏ này được chấp nhận bởi 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);

Cập nhật 2 : Tôi hiểu cách viết functor tự trả về hoặc cách sử dụng bộ kết hợp Y, để đạt được điều này. Đây là một câu hỏi luật sư ngôn ngữ.

Cập nhật 3 : câu hỏi không phải là liệu có hợp pháp để lambda tự quay trở lại nói chung hay không, mà là về tính hợp pháp của cách làm cụ thể này.

Câu hỏi liên quan: C ++ lambda trở lại chính nó .


2
clang có vẻ tốt hơn tại thời điểm này, tôi tự hỏi nếu một cấu trúc như vậy thậm chí có thể đánh máy, nhiều khả năng nó kết thúc trong một cây vô hạn.
bipll

2
Việc bạn hỏi liệu nó có hợp pháp không, đây là câu hỏi của luật sư ngôn ngữ nhưng một số câu trả lời không thực sự áp dụng phương pháp đó ... điều quan trọng là phải lấy đúng thẻ
Shafik Yaghmour

2
@ShafikYaghmour Cảm ơn, đã quảng cáo một thẻ
n. 'đại từ' m.

1
@ArneVogel vâng, bản cập nhật sử dụng auto& selfgiúp loại bỏ vấn đề tham chiếu lơ lửng.
n. 'đại từ' m.

1
@TheGreatDuck lambdas C ++ không thực sự là biểu thức lambda lý thuyết. C ++ có các kiểu đệ quy tích hợp mà phép tính lambda đơn giản ban đầu không thể biểu thị, do đó, nó có thể có những thứ tương đồng với a: a-> a và các cấu trúc không thể khác.
n. 'đại từ' m.

Câu trả lời:


68

Chương trình không được định dạng (clang là đúng) mỗi [dcl.spec.auto] / 9 :

Nếu tên của một thực thể có loại giữ chỗ không suy giảm xuất hiện trong một biểu thức, chương trình sẽ không được định dạng. Tuy nhiên, khi một câu lệnh trả lại không bị loại bỏ đã được nhìn thấy trong một hàm, loại trả về được suy ra từ câu lệnh đó có thể được sử dụng trong phần còn lại của hàm, kể cả trong các câu lệnh trả về khác.

Về cơ bản, việc khấu trừ kiểu trả về của lambda bên trong phụ thuộc vào chính nó (thực thể được đặt tên ở đây là toán tử cuộc gọi) - vì vậy bạn phải cung cấp một kiểu trả về một cách rõ ràng. Trong trường hợp cụ thể này, điều đó là không thể, bởi vì bạn cần loại lambda bên trong nhưng không thể đặt tên cho nó. Nhưng có những trường hợp khác khi cố gắng ép lambdas đệ quy như thế này, điều đó có thể làm việc.

Ngay cả khi không có điều đó, bạn có một tài liệu tham khảo lơ lửng .


Hãy để tôi giải thích thêm, sau khi thảo luận với ai đó thông minh hơn nhiều (ví dụ TC) Có một sự khác biệt quan trọng giữa mã gốc (giảm nhẹ) và phiên bản mới được đề xuất (giảm tương tự):

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

Và đó là biểu hiện bên trong self(self)không phụ thuộc vào f1, mà self(self, p)phụ thuộc vào f2. Khi các biểu thức không phụ thuộc, chúng có thể được sử dụng ... háo hức ( [temp.res] / 8 , ví dụ như thế nào static_assert(false)là một lỗi cứng bất kể mẫu mà nó tìm thấy có được khởi tạo hay không).

Đối với f1, một trình biên dịch (như, nói, tiếng kêu) có thể cố gắng khởi tạo điều này một cách háo hức. Bạn biết loại suy ra của lambda bên ngoài một khi bạn đến ;điểm đó #2ở trên (đó là loại lambda bên trong), nhưng chúng tôi đang cố gắng sử dụng nó sớm hơn (nghĩ về nó như tại điểm #1) - chúng tôi đang cố gắng để sử dụng nó trong khi chúng ta vẫn phân tích lambda bên trong, trước khi chúng ta biết loại thực sự là gì. Điều đó chạy afoul của dcl.spec.auto/9.

Tuy nhiên, vì f2, chúng ta không thể cố gắng khởi động một cách háo hức, vì nó phụ thuộc. Chúng tôi chỉ có thể khởi tạo tại điểm sử dụng, đến thời điểm đó chúng tôi biết tất cả mọi thứ.


Để thực sự làm một cái gì đó như thế này, bạn cần một tổ hợp y . Việc thực hiện từ bài báo:

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

Và điều bạn muốn là:

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

Làm thế nào bạn sẽ xác định loại trả lại rõ ràng? Tôi không thể tìm ra nó.
Rakete1111

@ Rakete1111 cái nào? Trong bản gốc, bạn không thể.
Barry

ồ được thôi. Tôi không phải là người bản địa, nhưng "vì vậy bạn phải cung cấp một kiểu trả về" dường như ngụ ý rằng có một cách, đó là lý do tại sao tôi hỏi :)
Rakete1111

4
@PedroA stackoverflow.com/users/2756719/tc là người đóng góp C ++. Anh ta cũng không phải là một AI, hoặc đủ tháo vát để thuyết phục một người cũng am hiểu về C ++ để tham dự cuộc họp nhỏ của LWG gần đây tại Chicago.
Casey

3
@Casey Hoặc có thể con người chỉ nói những điều mà AI đã nói với anh ta ... bạn không bao giờ biết;)
TC

34

Chỉnh sửa : Dường như có một số tranh cãi về việc liệu công trình này có hợp lệ theo thông số kỹ thuật của C ++ hay không. Ý kiến ​​trước có vẻ là nó không hợp lệ. Xem các câu trả lời khác để thảo luận kỹ hơn. Phần còn lại của câu trả lời này được áp dụng nếu việc xây dựng hợp lệ; mã được điều chỉnh bên dưới hoạt động với MSVC ++ và gcc và OP đã đăng thêm mã sửa đổi hoạt động với tiếng kêu.

Đây là hành vi không xác định, bởi vì lambda bên trong nắm bắt tham số selfbằng tham chiếu, nhưng selfđi ra khỏi phạm vi sau returndòng 7. Do đó, khi lambda trả về được thực thi sau đó, nó truy cập tham chiếu đến một biến đã vượt quá phạm vi.

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

Chạy chương trình với valgrindminh họa này:

==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.

Thay vào đó, bạn có thể thay đổi lambda bên ngoài để tự tham khảo thay vì theo giá trị, do đó tránh được một loạt các bản sao không cần thiết và cũng giải quyết vấn đề:

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

Những công việc này:

==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

Tôi không quen thuộc với lambdas chung, nhưng bạn không thể selftham khảo?
François Andrieux

@ FrançoisAndrieux Có, nếu bạn selftham chiếu, vấn đề này sẽ biến mất , nhưng Clang vẫn từ chối vì lý do khác
Justin

@ FrançoisAndrieux Thật vậy và tôi đã thêm nó vào câu trả lời, cảm ơn bạn!
TypeIA

Vấn đề với cách tiếp cận này là nó không loại bỏ các lỗi biên dịch có thể xảy ra. Vì vậy, có lẽ nó nên làm việc nhưng việc thực hiện bị hỏng.
Shafik Yaghmour

Cảm ơn bạn, tôi đã xem cái này hàng giờ và không thấy nó selfbị bắt bởi tham chiếu!
n. 'đại từ' m.

21

TL; DR;

tiếng kêu là chính xác.

Có vẻ như phần của tiêu chuẩn khiến cho hình dạng xấu này là [dcl.spec.auto] p9 :

Nếu tên của một thực thể có loại giữ chỗ không suy giảm xuất hiện trong một biểu thức, chương trình sẽ không được định dạng. Tuy nhiên, khi một câu lệnh trả lại không bị loại bỏ đã được nhìn thấy trong một hàm, loại trả về được suy ra từ câu lệnh đó có thể được sử dụng trong phần còn lại của hàm, kể cả trong các câu lệnh trả về khác. [ Thí dụ:

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
}

Ví dụ

Tác phẩm gốc thông qua

Nếu chúng tôi xem xét đề xuất Đề xuất thêm Bộ kết hợp Y vào Thư viện chuẩn, nó sẽ cung cấp giải pháp hoạt động:

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

và nó nói rõ ràng ví dụ của bạn là không thể:

C ++ 11/14 lambdas không khuyến khích đệ quy: không có cách nào để tham chiếu đối tượng lambda từ cơ thể của hàm lambda.

và nó đề cập đến một vấn đề mà Richard Smith ám chỉ về lỗi mà tiếng kêu đang gây ra cho bạn :

Tôi nghĩ rằng điều này sẽ tốt hơn như là một tính năng ngôn ngữ hạng nhất. Tôi đã hết thời gian cho cuộc họp trước Kona, nhưng tôi đang có ý định viết một tờ giấy để cho phép đặt tên cho lambda (nằm trong cơ thể của chính nó):

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

Ở đây, 'fib' tương đương với lambda * này (với một số quy tắc đặc biệt khó chịu để cho phép nó hoạt động mặc dù kiểu đóng của lambda chưa hoàn chỉnh).

Barry chỉ cho tôi đề xuất tiếp theo Lambdas đệ quy giải thích tại sao điều này là không thể và hoạt động xung quanh các dcl.spec.auto#9hạn chế và cũng cho thấy các phương pháp để đạt được điều này ngày hôm nay mà không cần nó:

Lambdas là một công cụ hữu ích để tái cấu trúc mã cục bộ. Tuy nhiên, đôi khi chúng tôi muốn sử dụng lambda từ bên trong chính nó, để cho phép đệ quy trực tiếp hoặc cho phép đóng cửa được đăng ký như là một sự tiếp tục. Điều này đáng ngạc nhiên là khó hoàn thành tốt trong C ++ hiện tại.

Thí dụ:

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

}

Một nỗ lực tự nhiên để tham chiếu lambda từ chính nó là lưu trữ nó trong một biến và nắm bắt biến đó bằng cách tham chiếu:

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

Tuy nhiên, điều này là không thể do tính tuần hoàn ngữ nghĩa : loại biến tự động không được suy ra cho đến khi biểu thức lambda được xử lý, điều đó có nghĩa là biểu thức lambda không thể tham chiếu biến.

Một cách tiếp cận tự nhiên khác là sử dụng hàm std :::

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

Cách tiếp cận này biên dịch, nhưng thường đưa ra một hình phạt trừu tượng: hàm std :: có thể phải chịu cấp phát bộ nhớ và việc gọi lambda thường sẽ yêu cầu một cuộc gọi gián tiếp.

Đối với một giải pháp không chi phí, thường không có cách tiếp cận nào tốt hơn là xác định rõ ràng một loại lớp cục bộ.


@ Cheersandhth.-Alf Tôi cuối cùng đã tìm thấy trích dẫn tiêu chuẩn sau khi đọc bài báo vì vậy nó không liên quan vì trích dẫn tiêu chuẩn cho thấy rõ lý do tại sao không tiếp cận hoạt động
Shafik Yaghmour

"" Nếu tên của một thực thể có loại giữ chỗ không suy giảm xuất hiện trong một biểu thức, thì chương trình không được định dạng "Mặc dù vậy, tôi không thấy sự xuất hiện của điều này trong chương trình. Có selfvẻ như không phải là một thực thể đó.
n. 'đại từ' m.

@nm bên cạnh các từ ngữ có thể có, các ví dụ dường như có ý nghĩa với từ ngữ và tôi tin rằng các ví dụ chứng minh vấn đề rõ ràng. Tôi không nghĩ rằng tôi có thể thêm nhiều hơn hiện tại để giúp đỡ.
Shafik Yaghmour

13

Có vẻ như tiếng kêu đúng. Hãy xem xét một ví dụ đơn giản:

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

Chúng ta hãy đi qua nó như một trình biên dịch (một chút):

  • Loại itLambda1với một toán tử cuộc gọi mẫu.
  • it(it); kích hoạt khởi tạo của toán tử cuộc gọi
  • Kiểu trả về của toán tử gọi mẫu là auto, vì vậy chúng ta phải suy ra nó.
  • Chúng tôi đang trả lại một lambda chụp tham số đầu tiên của loại Lambda1.
  • Lambda đó cũng có một toán tử cuộc gọi trả về kiểu của lệnh gọi self(self)
  • Lưu ý: self(self)chính xác là những gì chúng ta bắt đầu với!

Như vậy, loại không thể được suy luận.


Kiểu trả về Lambda1::operator()đơn giản Lambda2. Sau đó, trong biểu thức lambda bên trong đó, kiểu trả về của self(self), một cuộc gọi của Lambda1::operator(), cũng được biết đến Lambda2. Có thể các quy tắc chính thức cản trở việc thực hiện suy luận tầm thường đó, nhưng logic được trình bày ở đây thì không. Logic ở đây chỉ là một sự khẳng định. Nếu các quy tắc chính thức cản trở, thì đó là một lỗ hổng trong các quy tắc chính thức.
Chúc mừng và hth. - Alf

@ Cheersandh. Nhưng bạn không thể thay đổi các quy tắc cho việc này, vì nó khá cơ bản.
Rakete1111

9

Chà, mã của bạn không hoạt động. Nhưng điều này không:

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

Mã kiểm tra:

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

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

Mã của bạn là cả UB và không định hình không cần chẩn đoán. Thật là buồn cười; nhưng cả hai có thể được cố định độc lập.

Đầu tiên, UB:

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

đây là UB vì bên ngoài lấy selftheo giá trị, sau đó chụp bên trong selfbằng tham chiếu, sau đó tiến hành trả lại sau khi outerchạy xong. Vì vậy, segfaulting chắc chắn là ok.

Cách khắc phục:

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

Các mã vẫn còn hình thành. Để thấy điều này, chúng ta có thể mở rộng 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);

điều này ngay lập tức __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;
};

Vì vậy, tiếp theo chúng ta phải xác định loại trả về __outer_lambda__::operator().

Chúng tôi đi qua nó từng dòng. Đầu tiên chúng ta tạo __inner_lambda__kiểu:

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

Bây giờ, hãy nhìn vào đó - kiểu trả về của nó là self(self), hoặc __outer_lambda__(__outer_lambda__ const&). Nhưng chúng tôi đang cố gắng suy luận kiểu trả về __outer_lambda__::operator()(__outer_lambda__).

Bạn không được phép làm điều đó.

Mặc dù trên thực tế, kiểu trả về __outer_lambda__::operator()(__outer_lambda__)không thực sự phụ thuộc vào kiểu trả về __inner_lambda__::operator()(int), C ++ không quan tâm khi khấu trừ các kiểu trả về; nó chỉ đơn giản là kiểm tra từng dòng mã.

self(self)được sử dụng trước khi chúng tôi suy luận nó. Ill hình thành chương trình.

Chúng ta có thể vá điều này bằng cách ẩn self(self)cho đến sau này:

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

và bây giờ mã là chính xác và biên dịch. Nhưng tôi nghĩ rằng đây là một chút hack; chỉ cần sử dụng ycombinator.


Có thể (IDK) mô tả này là chính xác cho các quy tắc chính thức về lambdas. Nhưng về mặt viết lại mẫu, kiểu trả về của khuôn mẫu của lambda bên trong operator(), nói chung không thể được suy luận cho đến khi nó được khởi tạo (bằng cách được gọi với một số đối số của một số loại). Và do đó, một máy viết tay giống như viết lại thành mã dựa trên mẫu hoạt động độc đáo.
Chúc mừng và hth. - Alf

@cheers mã của bạn là khác nhau; bên trong là một lớp mẫu trong mã của bạn, nhưng nó không nằm trong mã OP của tôi hoặc. Và đó là vấn đề, vì các phương thức lớp mẫu bị trì hoãn khởi tạo cho đến khi được gọi.
Yakk - Adam Nevraumont

Một lớp được định nghĩa trong một hàm templated, tương đương với một lớp templated bên ngoài hàm đó. Việc xác định nó bên ngoài chức năng là cần thiết cho mã trình diễn khi nó có chức năng thành viên templated, bởi vì các quy tắc C ++ không cho phép một mẫu thành viên trong một lớp do người dùng định nghĩa cục bộ. Hạn chế chính thức đó không giữ cho bất cứ điều gì trình biên dịch tự tạo.
Chúc mừng và hth. - Alf

7

Thật dễ dàng để viết lại mã theo các lớp mà trình biên dịch sẽ, hoặc đúng hơn, nên tạo ra cho các biểu thức lambda.

Khi đã xong, vấn đề chính chỉ là tham chiếu lơ lửng và trình biên dịch không chấp nhận mã có phần bị thách thức trong bộ phận lambda.

Việc viết lại cho thấy rằng không có phụ thuộc tròn.

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

Một phiên bản được tạo khuôn mẫu đầy đủ để phản ánh cách mà lambda bên trong trong mã gốc, ghi lại một mục thuộc loại templated:

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

Tôi đoán rằng đây là khuôn mẫu trong bộ máy nội bộ, rằng các quy tắc chính thức được thiết kế để cấm. Nếu họ cấm xây dựng ban đầu.


Hãy xem, vấn đề là template< class > class Inner;mẫu của operator()... đã được khởi tạo? Vâng, từ sai. Bằng văn bản? ... trong Outer::operator()<Outer>khi trước khi loại trả về của toán tử bên ngoài được suy ra. Và Inner<Outer>::operator()có một cuộc gọi đến Outer::operator()<Outer>chính nó. Và điều đó không được phép. Bây giờ, hầu hết các trình biên dịch không chú ý đến self(self)vì chúng chờ để suy ra kiểu trả về Outer::Inner<Outer>::operator()<int>khi intđược truyền vào. Sensible. Nhưng nó bỏ lỡ sự hình thành xấu của mã.
Yakk - Adam Nevraumont

Vâng, tôi nghĩ rằng họ phải chờ để suy ra kiểu trả về của mẫu hàm cho đến khi mẫu hàm đó Innner<T>::operator()<U>, được khởi tạo. Sau khi tất cả các loại trả lại có thể phụ thuộc vào Uđây. Nó không, nhưng nói chung.
Chúc mừng và hth. - Alf

chắc chắn rồi; nhưng bất kỳ biểu thức nào có loại được xác định bằng khấu trừ loại hoàn trả không hoàn chỉnh vẫn là bất hợp pháp. Chỉ có một số trình biên dịch là lười biếng và không kiểm tra cho đến sau này, bởi vì điểm này luôn luôn hoạt động.
Yakk - Adam Nevraumont
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.