TL; DR
Trước khi bạn cố gắng đọc toàn bộ bài viết này, hãy biết rằng:
- một giải pháp cho vấn đề được trình bày đã được tìm thấy bởi chính tôi , nhưng tôi vẫn muốn biết liệu phân tích này có đúng không;
- Tôi đã đóng gói giải pháp vào một
fameta::counter
lớp giải quyết một vài vấn đề còn lại. Bạn có thể tìm thấy nó trên github ; - bạn có thể thấy nó tại nơi làm việc trên godbolt .
Mọi chuyện bắt đầu như thế nào
Kể từ khi Filip Roséen phát hiện / phát minh, vào năm 2015, ma thuật đen biên dịch máy đếm thời gian có trong C ++ , tôi đã bị ám ảnh nhẹ với thiết bị, vì vậy khi CWG quyết định rằng chức năng phải hoạt động, tôi vẫn thất vọng, nhưng vẫn hy vọng rằng tâm trí của họ có thể được thay đổi bằng cách hiển thị cho họ một vài trường hợp sử dụng hấp dẫn.
Sau đó, một vài năm trước tôi đã quyết định xem xét lại vấn đề này, để uberswitch es có thể được lồng vào nhau - một trường hợp sử dụng thú vị, theo tôi - chỉ để phát hiện ra rằng nó sẽ không hoạt động nữa với các phiên bản mới của các trình biên dịch có sẵn, mặc dù vấn đề 2118 là (và vẫn còn ) ở trạng thái mở: mã sẽ biên dịch, nhưng bộ đếm sẽ không tăng.
Vấn đề đã được báo cáo trên trang web của Roséen và gần đây cũng trên stackoverflow: C ++ có hỗ trợ bộ đếm thời gian biên dịch không?
Vài ngày trước tôi quyết định thử và giải quyết vấn đề một lần nữa
Tôi muốn hiểu những gì đã thay đổi trong trình biên dịch đã tạo ra C ++, dường như vẫn còn hiệu lực, không còn hoạt động nữa. Cuối cùng, tôi đã tìm kiếm rất nhiều người để nói về nó, nhưng không có kết quả. Vì vậy, tôi đã bắt đầu thử nghiệm và đưa ra một số kết luận, rằng tôi đang trình bày ở đây với hy vọng nhận được phản hồi từ những người hiểu biết nhiều hơn bản thân mình ở đây.
Dưới đây tôi đang trình bày mã gốc của Roséen vì sự rõ ràng. Để được giải thích về cách thức hoạt động, vui lòng tham khảo trang web của anh ấy :
template<int N>
struct flag {
friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
friend constexpr int adl_flag (flag<N>) {
return N;
}
static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
return N;
}
template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
return R;
}
int constexpr reader (float, flag<0>) {
return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
return R;
}
int main () {
constexpr int a = next ();
constexpr int b = next ();
constexpr int c = next ();
static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}
Với cả trình biên dịch g ++ và clang ++ gần đây, next()
luôn trả về 1. Đã thử nghiệm một chút, vấn đề ít nhất là với g ++ dường như là một khi trình biên dịch đánh giá các tham số mặc định của hàm chức năng lần đầu tiên các hàm được gọi, bất kỳ lệnh gọi tiếp theo nào các hàm này không kích hoạt đánh giá lại các tham số mặc định, do đó không bao giờ khởi tạo các hàm mới mà luôn đề cập đến các hàm được khởi tạo trước đó.
Câu hỏi đầu tiên
- Bạn có thực sự đồng ý với chẩn đoán này của tôi?
- Nếu có, hành vi mới này có bắt buộc theo tiêu chuẩn không? Là cái trước đó là một lỗi?
- Nếu không thì vấn đề là gì?
Hãy ghi nhớ những điều trên, tôi đã đưa ra một công việc xung quanh: đánh dấu mỗi lần gọi next()
bằng một id duy nhất tăng đơn điệu, để chuyển lên các calle, do đó không có cuộc gọi nào giống nhau, do đó buộc trình biên dịch phải đánh giá lại tất cả các đối số mỗi lần.
Có vẻ như là một gánh nặng để làm điều đó, nhưng nghĩ về nó, người ta chỉ có thể sử dụng các macro tiêu chuẩn __LINE__
hoặc __COUNTER__
giống như (bất cứ nơi nào có sẵn), ẩn trong một counter_next()
macro giống như chức năng.
Vì vậy, tôi đã đưa ra những điều sau đây, mà tôi trình bày ở dạng đơn giản nhất cho thấy vấn đề tôi sẽ nói về sau.
template <int N>
struct slot;
template <int N>
struct slot {
friend constexpr auto counter(slot<N>);
};
template <>
struct slot<0> {
friend constexpr auto counter(slot<0>) {
return 0;
}
};
template <int N, int I>
struct writer {
friend constexpr auto counter(slot<N>) {
return I;
}
static constexpr int value = I-1;
};
template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
return R;
};
template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
return R;
};
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();
Bạn có thể quan sát kết quả của những điều trên trên godbolt , mà tôi đã sàng lọc cho những kẻ lười biếng .
Và như bạn có thể thấy, với trunk g ++ và clang ++ cho đến 7.0.0, nó hoạt động! , bộ đếm tăng từ 0 lên 3 như mong đợi, nhưng với phiên bản clang ++ trên 7.0.0 thì không .
Để thêm sự xúc phạm đến thương tích, tôi thực sự đã xoay sở để tạo ra sự cố clang ++ lên đến phiên bản 7.0.0, chỉ bằng cách thêm một tham số "bối cảnh" vào hỗn hợp, sao cho bộ đếm thực sự bị ràng buộc với bối cảnh đó và, như vậy, có thể được khởi động lại bất cứ khi nào một bối cảnh mới được xác định, điều này mở ra khả năng sử dụng số lượng bộ đếm vô hạn. Với biến thể này, clang ++ ở trên phiên bản 7.0.0 không gặp sự cố, nhưng vẫn không mang lại kết quả như mong đợi. Sống trên godbolt .
Không biết bất kỳ manh mối nào về những gì đang diễn ra, tôi đã phát hiện ra trang web cppinsights.io , cho phép mọi người thấy cách thức và thời điểm các mẫu được khởi tạo. Sử dụng dịch vụ đó, điều tôi nghĩ đang xảy ra là clang ++ không thực sự xác định bất kỳ friend constexpr auto counter(slot<N>)
chức năng nào mỗi khi writer<N, I>
được khởi tạo.
Cố gắng gọi một cách rõ ràng counter(slot<N>)
cho bất kỳ N đã cho nào đã được khởi tạo dường như là cơ sở cho giả thuyết này.
Tuy nhiên, nếu tôi cố gắng khởi tạo một cách rõ ràng writer<N, I>
cho bất kỳ sự cho trước nào N
và I
điều đó đáng lẽ đã được khởi tạo, thì clang ++ phàn nàn về một định nghĩa lại friend constexpr auto counter(slot<N>)
.
Để kiểm tra ở trên, tôi đã thêm hai dòng nữa vào mã nguồn trước đó.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Bạn có thể nhìn thấy tất cả cho chính mình trên godbolt . Ảnh chụp màn hình bên dưới.
Vì vậy, có vẻ như clang ++ tin rằng nó đã xác định một cái gì đó mà nó tin rằng nó chưa được xác định , loại nào làm cho đầu bạn quay cuồng, phải không?
Câu hỏi thứ hai
- Là giải pháp C ++ hợp pháp của tôi , hay tôi đã quản lý để phát hiện ra một lỗi g ++ khác?
- Nếu nó hợp pháp, do đó tôi đã phát hiện ra một số lỗi clang ++ khó chịu?
- Hay tôi vừa đi sâu vào thế giới ngầm đen tối của Hành vi không xác định, nên bản thân tôi là người duy nhất đáng trách?
Trong mọi trường hợp, tôi sẽ nhiệt liệt chào đón bất kỳ ai muốn giúp tôi thoát khỏi cái hố thỏ này, giải thích những lời giải thích khó khăn nếu cần. : D
next()
hàm, tuy nhiên tôi thực sự không thể tìm ra cách thức hoạt động của nó. Trong mọi trường hợp, tôi đã đưa ra phản hồi cho vấn đề của riêng mình, tại đây: stackoverflow.com/a/60096865/566849