Biên dịch thời gian biên dịch C ++, xem lại


28

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:

  1. 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;
  2. Tôi đã đóng gói giải pháp vào một fameta::counterlớ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 ;
  3. 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

  1. Bạn có thực sự đồng ý với chẩn đoán này của tôi?
  2. 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?
  3. 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 .

nhập mô tả hình ảnh ở đây

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 NIđ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.

clang ++ tin rằng nó đã định nghĩa một cái gì đó mà nó tin rằng nó chưa được xác định

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

  1. 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?
  2. Nếu nó hợp pháp, do đó tôi đã phát hiện ra một số lỗi clang ++ khó chịu?
  3. 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



2
Theo tôi nhớ, ủy ban tiêu chuẩn, mọi người có ý định rõ ràng không cho phép các cấu trúc thời gian biên dịch dưới bất kỳ hình thức, hình dạng hoặc hình thức nào không tạo ra kết quả chính xác giống nhau mỗi khi chúng được đánh giá (theo giả thuyết). Vì vậy, nó có thể là một lỗi biên dịch, nó có thể là một trường hợp "không định hình, không cần chẩn đoán" hoặc nó có thể là một cái gì đó mà tiêu chuẩn bỏ qua. Tuy nhiên, nó đi ngược lại "tinh thần của tiêu chuẩn". Tôi xin lỗi. Tôi cũng muốn biên dịch bộ đếm thời gian.
bolov

@HolyBlackCat Tôi phải thú nhận rằng tôi cảm thấy rất khó khăn để có được cái đầu của mình xung quanh mã đó. Có vẻ như nó có thể tránh được việc phải chuyển một cách rõ ràng một số tăng đơn điệu làm tham số cho 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
Fabio A.

@FabioA. Tôi cũng không hoàn toàn hiểu câu trả lời đó. Kể từ khi hỏi câu hỏi đó, tôi nhận ra rằng tôi không muốn chạm vào quầy constexpr nữa.
HolyBlackCat

Mặc dù đây là một thử nghiệm ít suy nghĩ thú vị, nhưng ai đó thực sự đã sử dụng mã đó sẽ phải hy vọng rằng nó sẽ không hoạt động trong các phiên bản tương lai của C ++, phải không? Theo nghĩa đó, kết quả tự xác định là một lỗi.
Aziuth

Câu trả lời:


5

Sau khi điều tra thêm, hóa ra tồn tại một sửa đổi nhỏ có thể được thực hiện cho next()chức năng, giúp mã hoạt động chính xác trên các phiên bản clang ++ trên 7.0.0, nhưng làm cho nó ngừng hoạt động đối với tất cả các phiên bản clang ++ khác.

Hãy xem mã sau đây, lấy từ giải pháp trước đây của tôi.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

Nếu bạn chú ý đến nó, những gì nó thực sự làm là cố gắng đọc giá trị được liên kết với slot<N>, thêm 1 vào nó và sau đó liên kết giá trị mới này với cùng slot<N> .

Khi slot<N>đã không có liên quan đến giá trị, giá trị gắn liền với slot<Y>được lấy thay vào đó, với Yviệc chỉ số cao nhất ít hơn Nnhư vậy slot<Y>có giá trị đi kèm.

Vấn đề với đoạn mã trên là, mặc dù nó hoạt động trên g ++, clang ++ (đúng, tôi sẽ nói?) Làm cho trả về reader(0, slot<N>()) vĩnh viễn bất cứ thứ gì nó trả về khi slot<N>không có giá trị liên quan. Đổi lại, điều này có nghĩa là tất cả các vị trí được liên kết hiệu quả với giá trị cơ sở 0.

Giải pháp là chuyển đổi mã trên thành mã này:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Thông báo slot<N>()đã được sửa đổi thành slot<N-1>(). Điều này có ý nghĩa: nếu tôi muốn liên kết một giá trị với slot<N>nó, điều đó có nghĩa là chưa có giá trị nào được liên kết, do đó, sẽ không có ý nghĩa gì khi cố gắng truy xuất nó. Ngoài ra, chúng tôi muốn tăng một bộ đếm và giá trị của bộ đếm được liên kết slot<N>phải là một cộng với giá trị được liên kết slot<N-1>.

Eureka!

Điều này phá vỡ các phiên bản clang ++ <= 7.0.0, mặc dù.

Kết luận

Dường như với tôi rằng giải pháp ban đầu tôi đã đăng có một lỗi về khái niệm, chẳng hạn như:

  • g ++ có một cách giải quyết / lỗi / thư giãn giúp loại bỏ lỗi của giải pháp của tôi và cuối cùng làm cho mã hoạt động được.
  • phiên bản clang ++> 7.0.0 chặt chẽ hơn và không thích lỗi trong mã gốc.
  • Các phiên bản clang ++ <= 7.0.0 có một lỗi khiến cho giải pháp sửa lỗi không hoạt động.

Tóm tắt tất cả, đoạn mã sau hoạt động trên tất cả các phiên bản của g ++ và clang ++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

Mã as-cũng hoạt động với msvc. Trình biên dịch icc không kích hoạt SFINAE khi sử dụng decltype(counter(slot<N>())), thích phàn nàn về việc không thể deduce the return type of function "counter(slot<N>)"it has not been defined. Tôi tin rằng đây là một lỗi , có thể được khắc phục bằng cách thực hiện SFINAE trên kết quả trực tiếp của counter(slot<N>). Điều này cũng hoạt động trên tất cả các trình biên dịch khác, nhưng g ++ quyết định đưa ra một số lượng lớn các cảnh báo rất khó chịu không thể tắt được. Vì vậy, cũng trong trường hợp này, #ifdefcó thể đến để giải cứu.

Các bằng chứng là trên godbolt , screnshotted dưới đây.

nhập mô tả hình ảnh ở đây


2
Tôi nghĩ rằng câu trả lời này đã đóng chủ đề, nhưng tôi vẫn muốn biết liệu tôi có đúng trong phân tích của mình không, vì vậy tôi sẽ đợi trước khi chấp nhận câu trả lời của mình là chính xác, hy vọng người khác sẽ đi qua và cho tôi manh mối tốt hơn hoặc một xác nhận. :)
Fabio A.
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.