GCC9 có tránh được trạng thái vô giá trị của biến std :: không?


14

Gần đây tôi đã theo dõi một cuộc thảo luận Reddit dẫn đến một so sánh tốt về std::visittối ưu hóa giữa các trình biên dịch. Tôi nhận thấy như sau: https://godbolt.org/z/D2Q5ED

Cả GCC9 và Clang9 (tôi đoán rằng chúng có chung stdlib) không tạo mã để kiểm tra và ném ngoại lệ vô giá trị khi tất cả các loại đều đáp ứng một số điều kiện. Điều này dẫn đến cách codegen tốt hơn, do đó tôi đã nêu ra một vấn đề với MSVC STL và được trình bày với mã này:

template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}

Yêu cầu là, điều này làm cho bất kỳ biến thể nào trở nên vô giá trị, và đọc tài liệu nên:

Đầu tiên, phá hủy giá trị hiện có (nếu có). Sau đó, trực tiếp khởi tạo giá trị được chứa như thể xây dựng một giá trị loại T_Ivới các đối số std::forward<Args>(args)....Nếu một ngoại lệ được đưa ra, *thiscó thể trở thành valuless_by_exception.

Điều tôi không hiểu: Tại sao nó được nêu là "có thể"? Có hợp pháp để ở trong trạng thái cũ nếu toàn bộ hoạt động ném? Bởi vì đây là những gì GCC làm:

  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };

Và sau đó, nó (có điều kiện) làm một cái gì đó như:

T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);

Do đó về cơ bản, nó tạo ra tạm thời và nếu điều đó thành công các bản sao / di chuyển nó vào vị trí thực.

IMO đây là vi phạm "Đầu tiên, phá hủy giá trị hiện có" như tài liệu đã nêu. Khi tôi đọc tiêu chuẩn, sau đó sau khi v.emplace(...)giá trị hiện tại trong biến thể luôn bị hủy và loại mới là loại được đặt hoặc không có giá trị.

Tôi nhận được rằng điều kiện is_trivially_copyableloại trừ tất cả các loại có một hàm hủy có thể quan sát được. Vì vậy, điều này cũng có thể là: "biến thể as-if được khởi tạo lại với giá trị cũ" hoặc như vậy. Nhưng trạng thái của biến thể là một hiệu ứng có thể quan sát được. Vì vậy, tiêu chuẩn thực sự cho phép, điều emplaceđó không thay đổi giá trị hiện tại?

Chỉnh sửa để đáp lại một trích dẫn tiêu chuẩn:

Sau đó, khởi tạo giá trị được chứa như thể trực tiếp - không liệt kê - khởi tạo giá trị của loại TI với các đối số std​::​forward<Args>(args)....

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);thực sự được tính là một thực hiện hợp lệ của ở trên? Đây có phải là những gì có nghĩa là "như thể"?

Câu trả lời:


7

Tôi nghĩ phần quan trọng của tiêu chuẩn là đây:

Từ https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Modi ers ers

(...)

mẫu biến_alternative_t> & emplace (Args && ... args);

(...) Nếu một ngoại lệ được đưa ra trong quá trình khởi tạo giá trị được chứa, biến thể có thể không giữ giá trị

Nó nói "có thể" không "phải". Tôi hy vọng điều này là có chủ ý để cho phép triển khai như cách sử dụng bởi gcc.

Như bạn đã đề cập, điều này chỉ có thể nếu các hàm hủy của tất cả các lựa chọn thay thế là tầm thường và do đó không thể quan sát được vì phá hủy giá trị trước đó là bắt buộc.

Theo dõi câu hỏi:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

Liệu T tmp {std :: Forward (args) ...}; this-> value = std :: move (tmp); thực sự được tính là một thực hiện hợp lệ của ở trên? Đây có phải là những gì có nghĩa là "như thể"?

Có, bởi vì đối với các loại có thể sao chép tầm thường , không có cách nào để phát hiện sự khác biệt, do đó, việc triển khai thực hiện như thể giá trị được khởi tạo như mô tả. Điều này sẽ không hoạt động nếu loại không thể sao chép tầm thường.


Hấp dẫn. Tôi đã cập nhật câu hỏi với một yêu cầu tiếp theo / làm rõ. Cái gốc là: Bản sao / di chuyển có được phép không? Tôi rất bối rối bởi might/maytừ ngữ vì tiêu chuẩn không nêu rõ sự thay thế là gì.
Ngọn lửa

Chấp nhận điều này cho báo giá tiêu chuẩn và there is no way to detect the difference.
Ngọn lửa

5

Vì vậy, tiêu chuẩn thực sự cho phép, điều emplaceđó không thay đổi giá trị hiện tại?

Đúng. emplacesẽ cung cấp bảo đảm cơ bản không rò rỉ (nghĩa là tôn trọng tuổi thọ của vật thể khi xây dựng và phá hủy tạo ra các tác dụng phụ có thể quan sát được), nhưng khi có thể, nó được phép cung cấp bảo đảm mạnh (nghĩa là trạng thái ban đầu được giữ khi hoạt động thất bại).

variantđược yêu cầu hành xử tương tự như một liên minh - các lựa chọn thay thế được phân bổ trong một khu vực lưu trữ được phân bổ phù hợp. Nó không được phép phân bổ bộ nhớ động. Do đó, việc thay đổi kiểu emplacekhông có cách nào để giữ đối tượng ban đầu mà không gọi một nhà xây dựng di chuyển bổ sung - nó phải phá hủy nó và xây dựng đối tượng mới thay cho đối tượng đó. Nếu việc xây dựng này thất bại, thì biến thể phải chuyển sang trạng thái vô giá trị đặc biệt. Điều này ngăn chặn những điều kỳ lạ như phá hủy một vật thể không tồn tại.

Tuy nhiên, đối với các loại nhỏ có thể sao chép nhỏ, có thể cung cấp sự đảm bảo mạnh mẽ mà không cần quá nhiều chi phí (thậm chí là tăng hiệu suất để tránh kiểm tra, trong trường hợp này). Do đó, việc thực hiện nó. Đây là tuân thủ tiêu chuẩn: việc triển khai vẫn cung cấp sự đảm bảo cơ bản theo yêu cầu của tiêu chuẩn, chỉ theo cách thân thiện hơn với người dùng.

Chỉnh sửa để đáp lại một trích dẫn tiêu chuẩn:

Sau đó, khởi tạo giá trị được chứa như thể trực tiếp - không liệt kê - khởi tạo giá trị của loại TI với các đối số std​::​forward<Args>(args)....

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);thực sự được tính là một thực hiện hợp lệ của ở trên? Đây có phải là những gì có nghĩa là "như thể"?

Có, nếu việc gán di chuyển không tạo ra hiệu ứng có thể quan sát được, đó là trường hợp đối với các loại có thể sao chép tầm thường.


Tôi hoàn toàn đồng ý với lý luận logic. Tôi chỉ không chắc chắn điều này thực sự nằm trong tiêu chuẩn? Bạn có thể sao lưu điều này với bất cứ điều gì?
Ngọn lửa

@Flamefire Hmm ... Nói chung, các chức năng tiêu chuẩn cung cấp sự đảm bảo cơ bản (trừ khi có gì đó không đúng với những gì người dùng cung cấp) và std::variantkhông có lý do gì để phá vỡ điều đó. Tôi đồng ý rằng điều này có thể được làm rõ hơn theo cách diễn đạt của tiêu chuẩn, nhưng về cơ bản đây là cách các phần khác của thư viện chuẩn hoạt động. Và FYI, P0088 là đề xuất ban đầu.
LF

Cảm ơn. Có một đặc điểm kỹ thuật rõ ràng hơn bên trong: if an exception is thrown during the call toT’s constructor, valid()will be false;Vì vậy, điều đó đã cấm "tối ưu hóa" này
Flamefire

Đúng. Thông số kỹ thuật của emplaceP0088 dướiException safety
Flamefire

@Flamefire Có vẻ là một sự khác biệt giữa đề xuất ban đầu và phiên bản được bình chọn. Phiên bản cuối cùng đã thay đổi thành từ "có thể".
LF
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.