Thay đổi đột phá trong C ++ 20 hoặc hồi quy trong clang-trunk / gcc-trunk khi quá tải so sánh đẳng thức với giá trị trả về không Boolean?


11

Đoạn mã sau biên dịch tốt với clang-trunk ở chế độ c ++ 17 nhưng bị phá vỡ ở chế độ c ++ 2a (sắp tới c ++ 20):

// Meta struct describing the result of a comparison
struct Meta {};

struct Foo {
    Meta operator==(const Foo&) {return Meta{};}
    Meta operator!=(const Foo&) {return Meta{};}
};

int main()
{
    Meta res = (Foo{} != Foo{});
}

Nó cũng biên dịch tốt với gcc-trunk hoặc clang-9.0.0: https://godbolt.org/z/8GGT78

Lỗi với clang-trunk và -std=c++2a:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

Tôi hiểu rằng C ++ 20 sẽ làm cho nó chỉ có thể quá tải operator==và trình biên dịch sẽ tự động tạo operator!=bằng cách phủ định kết quả của operator==. Theo tôi hiểu, điều này chỉ hoạt động miễn là loại trả về bool.

Nguồn gốc của vấn đề là trong Eigen chúng ta khai báo một tập hợp các toán ==, !=, <, ... giữa Arraycác đối tượng hoặc Arrayvà vô hướng, mà trở lại (một biểu hiện của) một mảng của bool(mà sau đó có thể được truy cập yếu tố khôn ngoan, hoặc sử dụng khác ). Ví dụ,

#include <Eigen/Core>
int main()
{
  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();
}

Ngược lại với ví dụ của tôi ở trên, điều này thậm chí thất bại với gcc-trunk: https://godbolt.org/z/RWktKs . Tôi chưa quản lý để giảm điều này thành một ví dụ không phải Eigen, thất bại ở cả clang-trunk và gcc-trunk (ví dụ ở trên cùng là khá đơn giản).

Báo cáo vấn đề liên quan: https://gitlab.com/libeigen/eigen/issues/1833

Câu hỏi thực tế của tôi: Đây thực sự là một thay đổi đột phá trong C ++ 20 (và có khả năng làm quá tải các toán tử so sánh để trả về các đối tượng Meta) hay không, hay nhiều khả năng là hồi quy trong clang / gcc?


Câu trả lời:


5

Vấn đề Eigen dường như giảm xuống như sau:

using Scalar = double;

template<class Derived>
struct Base {
    friend inline int operator==(const Scalar&, const Derived&) { return 1; }
    int operator!=(const Scalar&) const;
};

struct X : Base<X> {};

int main() {
    X{} != 0.0;
}

Hai ứng cử viên cho biểu thức là

  1. ứng cử viên viết lại từ operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Mỗi [over.match.funcs] / 4 , operator!=không được nhập vào phạm vi của Xmột khai báo sử dụng , loại tham số đối tượng ẩn cho # 2 là const Base<X>&. Do đó, # 1 có trình tự chuyển đổi ngầm định tốt hơn cho đối số đó (khớp chính xác, thay vì chuyển đổi từ gốc sang cơ sở). Chọn # 1 sau đó hiển thị chương trình không định dạng.

Sửa lỗi có thể:

  • Thêm using Base::operator!=;vào Derived, hoặc
  • Thay đổi operator==để có một const Base&thay vì a const Derived&.

Có một lý do tại sao mã thực tế không thể trả lại booltừ một operator==? Bởi vì đó dường như là lý do duy nhất khiến mã không được định hình theo các quy tắc mới.
Nicol Bolas

4
Mã thực tế liên quan đến một operator==(Array, Scalar)mà không so sánh yếu tố khôn ngoan và trả lại Arraycủa bool. Bạn không thể biến nó thành một boolmà không phá vỡ mọi thứ khác.
TC

2
Điều này có vẻ hơi giống như một khiếm khuyết trong tiêu chuẩn. Các quy tắc để viết lại operator==không được cho là ảnh hưởng đến mã hiện có, nhưng chúng vẫn được thực hiện trong trường hợp này, bởi vì việc kiểm tra boolgiá trị trả về không phải là một phần của việc chọn ứng viên để viết lại.
Nicol Bolas

2
@NicolBolas: Nguyên tắc chung được tuân theo là kiểm tra xem bạn có thể làm gì không ( ví dụ: gọi toán tử), chứ không phải bạn có nên tránh việc thay đổi triển khai âm thầm ảnh hưởng đến việc giải thích mã khác hay không. Nó chỉ ra rằng các so sánh viết lại phá vỡ rất nhiều thứ, nhưng chủ yếu là những điều đã được nghi ngờ và dễ sửa chữa. Vì vậy, dù tốt hơn hay tồi tệ hơn, những quy tắc này vẫn được thông qua.
Davis Herring

Ồ, cảm ơn rất nhiều, tôi đoán giải pháp của bạn sẽ giải quyết vấn đề của chúng tôi (Tôi không có thời gian để cài đặt gcc / clang trunk với nỗ lực hợp lý vào lúc này, vì vậy tôi sẽ kiểm tra xem điều này có phá vỡ bất kỳ phiên bản trình biên dịch ổn định mới nhất nào không ).
chtz

11

Có, mã trong thực tế phá vỡ trong C ++ 20.

Biểu thức Foo{} != Foo{}có ba ứng cử viên trong C ++ 20 (trong khi chỉ có một trong C ++ 17):

Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed

Điều này xuất phát từ các quy tắc ứng cử viên được viết lại mới trong [over.match.oper] /3.4 . Tất cả những ứng cử viên đó đều khả thi, vì Foolập luận của chúng tôi thì không const. Để tìm ra ứng cử viên khả thi nhất, chúng tôi phải trải qua những lần bẻ khóa.

Các quy tắc có liên quan cho chức năng khả thi tốt nhất là, từ [over.match.best] / 2 :

Với các định nghĩa này, hàm khả thi F1được xác định là hàm tốt hơn hàm khả thi khác F2nếu với tất cả các đối số i, không phải là chuỗi chuyển đổi tồi tệ hơn , và sau đó ICSi(F1)ICSi(F2)

  • [... Rất nhiều trường hợp không liên quan cho ví dụ này ...] hoặc, nếu không phải vậy, thì
  • F2 là một ứng cử viên viết lại ([over.match.oper]) và F1 thì không
  • F1 và F2 là các ứng cử viên viết lại và F2 là một ứng cử viên được tổng hợp với thứ tự các tham số đảo ngược và F1 thì không

#2#3được viết lại các ứng cử viên, và #3đã đảo ngược thứ tự các tham số, trong khi #1không được viết lại. Nhưng để có được bộ bẻ khóa đó, trước tiên chúng ta cần vượt qua điều kiện ban đầu đó: đối với tất cả các đối số, các chuỗi chuyển đổi không tệ hơn.

#1là tốt hơn #2bởi vì tất cả các chuỗi chuyển đổi là như nhau (tầm thường, bởi vì các tham số chức năng là như nhau) và #2là một ứng cử viên viết lại trong khi #1không.

Nhưng ... cả hai cặp #1/ #3#2/ #3 bị mắc kẹt trong điều kiện đầu tiên đó. Trong cả hai trường hợp, tham số thứ nhất có trình tự chuyển đổi tốt hơn cho #1/ #2trong khi tham số thứ hai có trình tự chuyển đổi tốt hơn cho#3 (tham số constphải trải qua một trình constđộ bổ sung , do đó, nó có trình tự chuyển đổi kém hơn). constDép xỏ ngón này khiến chúng ta không thể thích một trong hai.

Kết quả là, toàn bộ độ phân giải quá tải là mơ hồ.

Theo tôi hiểu, điều này chỉ hoạt động miễn là loại trả về bool .

Điều đó không đúng. Chúng tôi xem xét vô điều kiện các ứng cử viên viết lại và đảo ngược. Quy tắc chúng tôi có là, từ [over.match.oper] / 9 :

Nếu một operator==ứng cử viên viết lại được chọn bởi độ phân giải quá tải cho một toán tử @, kiểu trả về của nó sẽ là cv bool

Đó là, chúng tôi vẫn xem xét các ứng cử viên này. Nhưng nếu ứng cử viên khả thi nhất là một operator==người trả lại, hãy nói,Meta - kết quả về cơ bản giống như khi ứng cử viên đó bị xóa.

Chúng tôi không muốn ở trong tình trạng giải quyết quá tải sẽ phải xem xét loại trả về. Và trong mọi trường hợp, thực tế là mã ở đây trả về Metalà không quan trọng - vấn đề cũng sẽ tồn tại nếu nó được trả vềbool .


Rất may, sửa chữa ở đây là dễ dàng:

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};

Một khi bạn thực hiện cả hai toán tử so sánh const, không còn mơ hồ nữa. Tất cả các tham số đều giống nhau, vì vậy tất cả các chuỗi chuyển đổi đều giống nhau. #1bây giờ sẽ đánh bại #3bằng cách không viết lại và #2bây giờ sẽ đánh bại #3bằng cách không bị đảo ngược - điều làm cho #1ứng cử viên khả thi nhất. Kết quả tương tự mà chúng tôi đã có trong C ++ 17, chỉ cần thêm một vài bước để đạt được điều đó.


" Chúng tôi không muốn ở trong trạng thái mà độ phân giải quá tải sẽ phải xem xét loại trả về. " Để rõ ràng, trong khi bản thân độ phân giải quá tải không xem xét loại trả về, các hoạt động viết lại tiếp theo sẽ làm . Mã của một người không được định dạng nếu độ phân giải quá tải sẽ chọn viết lại ==và kiểu trả về của hàm đã chọn không bool. Nhưng việc loại bỏ này không xảy ra trong quá trình giải quyết quá tải.
Nicol Bolas

Nó thực sự chỉ được định hình sai nếu loại trả về là thứ không hỗ trợ toán tử! ...
Chris Dodd

1
@ChrisDodd Không, nó phải chính xác cv bool(và trước khi thay đổi này, yêu cầu là chuyển đổi theo ngữ cảnh thành bool- vẫn không !)
Barry

Thật không may, điều này không giải quyết được vấn đề thực tế của tôi, nhưng đó là do tôi không cung cấp MRE thực sự mô tả vấn đề của tôi. Tôi sẽ chấp nhận điều này và khi tôi có thể giảm bớt vấn đề của mình một cách chính xác, tôi sẽ hỏi một câu hỏi mới ...
chtz

2
Có vẻ như một sự giảm bớt thích hợp cho vấn đề ban đầu là gcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2 liệt kê mức độ quá tải hợp lệ trong một bộ được ưu tiên. Mục 2.8 cho chúng ta biết điều đó F1tốt hơn F2nếu (trong số nhiều thứ khác):

F2là một ứng cử viên viết lại ([over.match.oper]) và F1không

Ví dụ ở đó cho thấy một cách rõ ràng operator<được gọi ngay cả khi operator<=>có.

[over.match.oper] /3.4.3 cho chúng tôi biết rằng ứng cử viên operator==trong trường hợp này là một ứng cử viên viết lại.

Tuy nhiên , các nhà khai thác của bạn quên một điều quan trọng: họ nên là các constchức năng. Và làm cho chúng không constgây ra các khía cạnh trước đây của giải quyết quá tải để đi vào hoạt động. Cả chức năng là một kết hợp chính xác, như phi const-to- constchuyển đổi cần phải xảy ra cho các đối số khác nhau. Điều đó gây ra sự mơ hồ trong câu hỏi.

Một khi bạn thực hiện chúng const, Clang thân biên dịch .

Tôi không thể nói với phần còn lại của Eigen, vì tôi không biết mã, nó rất lớn và do đó không thể phù hợp với MCVE.


2
Chúng tôi chỉ nhận được bản bẻ khóa mà bạn đã liệt kê nếu có các chuyển đổi tốt như nhau cho tất cả các đối số. Nhưng không có: do thiếu const, các ứng cử viên không đảo ngược có trình tự chuyển đổi tốt hơn cho đối số thứ hai và ứng cử viên đảo ngược có trình tự chuyển đổi tốt hơn cho đối số đầu tiên.
Richard Smith

@RichardSmith: Vâng, đó là loại phức tạp mà tôi đang nói đến. Nhưng tôi không muốn thực sự phải trải qua và đọc / tiếp thu những quy tắc đó;)
Nicol Bolas

Thật vậy, tôi đã quên consttrong ví dụ tối thiểu. Tôi khá chắc chắn rằng Eigen sử dụng constở mọi nơi (hoặc bên ngoài các định nghĩa lớp, cũng với các consttài liệu tham khảo), nhưng tôi cần kiểm tra. Tôi cố gắng phá vỡ cơ chế tổng thể mà Eigen sử dụng đến một ví dụ tối thiểu, khi tôi tìm thấy thời gian.
chtz

-1

Chúng tôi có vấn đề tương tự với các tệp tiêu đề Goopax của chúng tôi. Biên dịch sau đây với clang-10 và -std = c ++ 2a tạo ra lỗi biên dịch.

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type
{
  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
};

int main()
{
  gpu_int a;
  gpu_bool b = (a == 0);
}

Cung cấp các toán tử bổ sung này dường như để giải quyết vấn đề:

template<typename T>
class gpu_type
{
  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
};

1
Đây có phải là điều không hữu ích để làm trước đó không? Nếu không, làm thế nào có a == 0thể biên dịch ?
Nicol Bolas

Đây thực sự không phải là một vấn đề tương tự. Như Nicol đã chỉ ra, điều này đã không được biên dịch trong C ++ 17. Nó tiếp tục không biên dịch trong C ++ 20, chỉ vì một lý do khác.
Barry

Tôi quên đề cập: Chúng tôi cũng cung cấp các toán tử thành viên: gpu_bool gpu_type<T>::operator==(T a) const;gpu_bool gpu_type<T>::operator!=(T a) const;với C ++ - 17, điều này hoạt động tốt. Nhưng bây giờ với clang-10 và C ++ - 20, chúng không được tìm thấy nữa và thay vào đó trình biên dịch cố gắng tạo các toán tử riêng của nó bằng cách hoán đổi các đối số, và nó thất bại, vì kiểu trả về thì không bool.
Ingo Josopait
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.