Câu trả lời được chấp nhận của Cort Ammon là tốt, nhưng tôi nghĩ rằng có một điểm quan trọng hơn cần làm về khả năng triển khai.
Giả sử tôi có hai đơn vị dịch khác nhau, "one.cpp" và "two.cpp".
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);
extern void foo(A1);
extern void foo(B1);
Hai quá tải foo
sử dụng cùng một mã định danh ( foo
) nhưng có tên khác nhau. (Trong Itanium ABI được sử dụng trên hệ thống POSIX-ish, các tên bị xáo trộn _Z3foo1A
và trong trường hợp cụ thể này là _Z3fooN1bMUliE_E
.)
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);
void foo(A2) {}
void foo(B2) {}
Trình biên dịch C ++ phải đảm bảo rằng tên bị void foo(A1)
xáo trộn của trong "two.cpp" giống với tên có dấu của extern void foo(A2)
trong "one.cpp", để chúng ta có thể liên kết hai tệp đối tượng với nhau. Đây là ý nghĩa vật lý của hai loại là "cùng một loại": về cơ bản nó là về khả năng tương thích ABI giữa các tệp đối tượng được biên dịch riêng biệt.
Trình biên dịch C ++ không bắt buộc phải đảm bảo rằng B1
và B2
là "cùng một loại." (Trên thực tế, cần phải đảm bảo rằng chúng là các loại khác nhau; nhưng điều đó không quan trọng ngay bây giờ.)
Trình biên dịch sử dụng cơ chế vật lý nào để đảm bảo điều đó A1
và A2
là "cùng một kiểu"?
Nó chỉ đơn giản là đào qua các typedef, và sau đó xem xét tên đầy đủ của loại. Đó là một loại lớp được đặt tên A
. (Chà, ::A
vì nó nằm trong không gian tên chung.) Vì vậy, nó là cùng một kiểu trong cả hai trường hợp. Điều đó dễ hiểu. Quan trọng hơn, nó dễ thực hiện . Để xem hai loại lớp có phải là cùng một loại hay không, bạn lấy tên của chúng và thực hiện a strcmp
. Để biến một loại lớp thành tên bị xáo trộn của một hàm, bạn viết số ký tự trong tên của nó, sau đó là các ký tự đó.
Vì vậy, các loại được đặt tên rất dễ nhầm lẫn.
Cơ chế vật lý nào mà trình biên dịch có thể sử dụng để đảm bảo rằng B1
và B2
là "cùng một kiểu", trong một thế giới giả định nơi C ++ yêu cầu chúng phải cùng kiểu?
Chà, nó không thể sử dụng tên của loại, bởi vì loại không có tên.
Có thể bằng cách nào đó nó có thể mã hóa văn bản của phần thân lambda. Nhưng điều đó sẽ hơi khó xử, bởi vì thực sự b
trong "one.cpp" khác biệt một cách tinh tế với b
trong "two.cpp": "one.cpp" có x+1
và "hai.cpp" có x + 1
. Vì vậy, chúng tôi sẽ phải đưa ra một quy tắc nói rằng sự khác biệt về khoảng trắng này không quan trọng, hoặc nó có (làm cho chúng trở thành các kiểu khác nhau), hoặc có thể nó có (có thể tính hợp lệ của chương trình được xác định bởi triển khai hoặc có thể là "không chuẩn xác không cần chẩn đoán"). Dù sao,A
Cách đơn giản nhất để thoát khỏi khó khăn là nói rằng mỗi biểu thức lambda tạo ra các giá trị của một kiểu duy nhất. Khi đó, hai kiểu lambda được xác định trong các đơn vị dịch khác nhau chắc chắn không phải là cùng một kiểu . Trong một đơn vị dịch duy nhất, chúng ta có thể "đặt tên" cho các loại lambda chỉ bằng cách đếm từ đầu mã nguồn:
auto a = [](){};
auto b = [](){};
auto f(int x) {
return [x](int y) { return x+y; };
}
auto g(float x) {
return [x](int y) { return x+y; };
}
Tất nhiên những cái tên này chỉ có ý nghĩa trong đơn vị dịch thuật này. TU $_0
này luôn là một loại khác với một số TU khác $_0
, mặc dù TU struct A
này luôn là cùng một loại với một số TU khác struct A
.
Nhân tiện, hãy lưu ý rằng ý tưởng "mã hóa văn bản của lambda" của chúng tôi có một vấn đề nhỏ khác: lambda $_2
và $_3
bao gồm chính xác cùng một văn bản , nhưng rõ ràng chúng không được coi là cùng một loại!
Nhân tiện, C ++ yêu cầu trình biên dịch biết cách xử lý văn bản của một biểu thức C ++ tùy ý , như trong
template<class T> void foo(decltype(T())) {}
template void foo<int>(int);
Nhưng C ++ không (chưa) yêu cầu trình biên dịch biết cách xử lý một câu lệnh C ++ tùy ý . decltype([](){ ...arbitrary statements... })
vẫn chưa được định hình ngay cả trong C ++ 20.
Cũng thông báo rằng thật dễ dàng để đưa ra một bí danh địa phương để một kiểu giấu tên sử dụng typedef
/ using
. Tôi có cảm giác rằng câu hỏi của bạn có thể nảy sinh từ việc cố gắng làm điều gì đó có thể được giải quyết như thế này.
auto f(int x) {
return [x](int y) { return x+y; };
}
using AdderLambda = decltype(f(0));
int of_one(AdderLambda g) { return g(1); }
int main() {
auto f1 = f(1);
assert(of_one(f1) == 2);
auto f42 = f(42);
assert(of_one(f42) == 43);
}
CHỈNH SỬA ĐỂ THÊM: Khi đọc một số nhận xét của bạn về các câu trả lời khác, có vẻ như bạn đang tự hỏi tại sao
int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);
Đó là bởi vì lambdas không chụp được mặc định có thể xây dựng. (Chỉ trong C ++ và C ++ 20, nhưng nó luôn đúng về mặt khái niệm .)
template<class T>
int default_construct_and_call(int x) {
T t;
return t(x);
}
assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);
Nếu bạn đã thử default_construct_and_call<decltype(&add1)>
, đây t
sẽ là một con trỏ hàm được khởi tạo mặc định và bạn có thể sẽ mặc định. Điều đó, giống như, không hữu ích.