Helpex - Trao đổi & giúp đỡ Đăng nhập
24

Đầu tiên, hãy xem xét mã C ++ này:

#include <stdio.h>

struct foo_int {
    void print(int x) {
        printf("int %d\n", x);
    }    
};

struct foo_str {
    void print(const char* x) {
        printf("str %s\n", x);
    }    
};

struct foo : foo_int, foo_str {
    //using foo_int::print;
    //using foo_str::print;
};

int main() {
    foo f;
    f.print(123);
    f.print("abc");
}

Như mong đợi theo Tiêu chuẩn, điều này không thể biên dịch, vì printđược xem xét riêng biệt trong mỗi lớp cơ sở nhằm mục đích giải quyết quá tải, và do đó các lệnh gọi là không rõ ràng. Đây là trường hợp của Clang (4.0), gcc (6.3) và MSVC (17.0) - xem kết quả chốt chặn ở đây .

Bây giờ hãy xem xét đoạn mã sau, sự khác biệt duy nhất của đoạn mã đó là chúng tôi sử dụng operator()thay vì print:

#include <stdio.h>

struct foo_int {
    void operator() (int x) {
        printf("int %d\n", x);
    }    
};

struct foo_str {
    void operator() (const char* x) {
        printf("str %s\n", x);
    }    
};

struct foo : foo_int, foo_str {
    //using foo_int::operator();
    //using foo_str::operator();
};

int main() {
    foo f;
    f(123);
    f("abc");
}

Tôi hy vọng kết quả sẽ giống với trường hợp trước, nhưng không phải vậy - trong khi gcc vẫn phàn nàn, Clang và MSVC có thể biên dịch khoản phạt này!

Câu hỏi số 1: Ai đúng trong trường hợp này? Tôi mong đợi nó là gcc, nhưng thực tế là hai trình biên dịch không liên quan khác đưa ra một kết quả nhất quán khác nhau ở đây khiến tôi tự hỏi liệu tôi có thiếu thứ gì đó trong Tiêu chuẩn hay không và mọi thứ khác nhau đối với các toán tử khi chúng không được gọi bằng cú pháp hàm.

Cũng lưu ý rằng nếu bạn chỉ bỏ ghi chú một trong các usingkhai báo mà không phải khai báo kia, thì cả ba trình biên dịch sẽ không biên dịch được, bởi vì chúng sẽ chỉ xem xét hàm được đưa vào usingtrong quá trình giải quyết quá tải và do đó một trong các lệnh gọi sẽ không thành công do loại không phù hợp. Nhớ lấy điều này; chúng ta sẽ quay lại với nó sau.

Bây giờ hãy xem xét đoạn mã sau:

#include <stdio.h>

auto print_int = [](int x) {
    printf("int %d\n", x);
};
typedef decltype(print_int) foo_int;

auto print_str = [](const char* x) {
    printf("str %s\n", x);
};
typedef decltype(print_str) foo_str;

struct foo : foo_int, foo_str {
    //using foo_int::operator();
    //using foo_str::operator();
    foo(): foo_int(print_int), foo_str(print_str) {}
};

int main() {
    foo f;
    f(123);
    f("foo");
}

Một lần nữa, giống như trước đây, ngoại trừ bây giờ chúng tôi không định nghĩa operator()rõ ràng, nhưng thay vào đó lấy nó từ một kiểu lambda. Một lần nữa, bạn mong đợi kết quả nhất quán với đoạn mã trước đó; và điều này đúng đối với trường hợp cả hai usingkhai báo đều được ghi chú hoặc nếu cả hai đều được bỏ ghi chú . Nhưng nếu bạn chỉ nhận xét một mà không nhận xét khác, mọi thứ lại đột ngột khác : bây giờ chỉ có MSVC phàn nàn như tôi mong đợi, trong khi Clang và gcc đều cho rằng điều đó ổn - và sử dụng cả hai thành viên kế thừa để giải quyết quá tải, mặc dù chỉ có một được đưa vào bởi using!

Câu hỏi số 2: Ai đúng trong trường hợp này? Một lần nữa, tôi muốn nó là MSVC, nhưng tại sao cả Clang và gcc lại không đồng ý? Và, quan trọng hơn, tại sao đoạn mã này lại khác với đoạn mã trước? Tôi mong đợi kiểu lambda hoạt động giống hệt như kiểu được xác định thủ công với quá tải operator()...

24 hữu ích 0 bình luận 1.1k xem chia sẻ
7

Barry được # 1 đúng. Số 2 của bạn đã gặp phải trường hợp góc cạnh: lambdas nongeneric không bắt được có một chuyển đổi ngầm thành con trỏ hàm, được sử dụng trong trường hợp không khớp. Đó là, đã cho

struct foo : foo_int, foo_str {
    using foo_int::operator();
    //using foo_str::operator();
    foo(): foo_int(print_int), foo_str(print_str) {}
} f;

using fptr_str = void(*)(const char*);

f("hello")tương đương với f.operator fptr_str()("hello"), chuyển đổi foothành một con trỏ thành hàm và gọi nó. Nếu bạn biên dịch tại, -O0bạn thực sự có thể thấy lệnh gọi hàm chuyển đổi trong hợp ngữ trước khi nó được tối ưu hóa. Đặt init-capture vào print_strvà bạn sẽ thấy lỗi vì chuyển đổi ngầm sẽ biến mất.

Để biết thêm, hãy xem [over.call.object] .

7 hữu ích 5 bình luận chia sẻ
4

Quy tắc tra cứu tên trong các lớp cơ sở của một lớp Cchỉ xảy ra nếu Cchính nó không chứa trực tiếp tên là [class.member.lookup] / 6 :

Các bước sau xác định kết quả của việc hợp nhất tập hợp tra cứu S(f,Bi) vào trung gian S(f,C):

  • Nếu mỗi thành viên subobject của S (f, Bi) là một subobject lớp cơ sở của ít nhất một trong các thành viên subobject của S (f, C) hoặc nếu S (f, Bi) trống thì S (f, C ) không thay đổi và quá trình hợp nhất đã hoàn tất. Ngược lại, nếu mỗi thành viên subobject của S (f, C) là một subobject lớp cơ sở của ít nhất một trong các thành viên subobject của S (f, Bi) hoặc nếu S (f, C) trống thì S mới (f, C) là một bản sao của S (f, Bi).

  • Ngược lại, nếu các tập khai báo của S (f, Bi) và S (f, C) khác nhau, thì việc hợp nhất là không rõ ràng : S (f, C) mới là một tập hợp tra cứu với tập khai báo không hợp lệ và hợp nhất của subobject các bộ. Trong các lần hợp nhất tiếp theo, tập hợp khai báo không hợp lệ được coi là khác với bất kỳ tập hợp nào khác.

  • Nếu không, S (f, C) mới là một tập hợp tra cứu với tập hợp các khai báo được chia sẻ và sự kết hợp của các tập hợp đối tượng.

Nếu chúng ta có hai lớp cơ sở, mỗi lớp khai báo cùng một tên, mà lớp dẫn xuất không mang theo khai báo sử dụng, thì việc tra cứu tên đó trong lớp dẫn xuất sẽ chạy sau dấu đầu dòng thứ hai đó và việc tra cứu sẽ không thành công. Tất cả các ví dụ của bạn về cơ bản đều giống nhau về vấn đề này.

Câu hỏi số 1: Ai đúng trong trường hợp này?

gcc là đúng. Sự khác biệt duy nhất giữa printoperator()là tên mà chúng tôi đang tìm kiếm.

Câu hỏi số 2: Ai đúng trong trường hợp này?

Đây là câu hỏi tương tự như câu hỏi số 1 - ngoại trừ chúng tôi có lambdas (cung cấp cho bạn các loại lớp không được đặt tên với tình trạng quá tải operator()) thay vì các loại lớp rõ ràng. Mã phải được định dạng sai vì lý do tương tự. Ít nhất đối với gcc, đây là lỗi 58820 .

4 hữu ích 3 bình luận chia sẻ
0

Phân tích của bạn về mã đầu tiên không chính xác. Không có giải pháp quá tải.

Các tra cứu tên quá trình xảy ra hoàn toàn trước khi giải quyết tình trạng quá tải. Tra cứu tên xác định phạm vi mà một biểu thức id được giải quyết.

Nếu một phạm vi duy nhất được tìm thấy thông qua các quy tắc tra cứu tên, thì quá trình giải quyết quá tải sẽ bắt đầu: tất cả các trường hợp của tên đó trong phạm vi đó tạo thành tập hợp quá tải.

Nhưng trong mã của bạn, tra cứu tên không thành công. Tên không được khai báo trong foo, vì vậy các lớp cơ sở được tìm kiếm. Nếu tên được tìm thấy trong nhiều hơn một lớp cơ sở ngay lập tức thì chương trình không hợp lệ và thông báo lỗi mô tả nó là một tên không rõ ràng.


Các quy tắc tra cứu tên không có trường hợp đặc biệt cho các toán tử quá tải. Bạn sẽ thấy rằng mã:

f.operator()(123);

không thành công vì lý do tương tự như f.printkhông thành công. Tuy nhiên, có một vấn đề khác trong mã thứ hai của bạn. f(123)KHÔNG được định nghĩa là luôn luôn có nghĩa f.operator()(123);. Trên thực tế, định nghĩa trong C ++ 14 là trong [over.call]:

operator()sẽ là một hàm thành viên không tĩnh với số lượng tham số tùy ý. Nó có thể có các đối số mặc định. Nó thực hiện cú pháp gọi hàm

hậu tố-biểu thức (lựa chọn biểu thức-danh sách)

trong đó biểu thức hậu tố đánh giá một đối tượng lớp và danh sách biểu thức có thể trống phù hợp với danh sách tham số của một operator()hàm thành viên của lớp. Do đó, một lời gọi x(arg1,...)được hiểu là x.operator()(arg1, ...)đối với một đối tượng lớp x kiểu T nếu T::operator()(T1, T2, T3)tồn tại và nếu toán tử được chọn làm hàm đối sánh tốt nhất bởi cơ chế giải quyết quá tải (13.3.3).

Điều này thực sự có vẻ là một đặc điểm kỹ thuật không chính xác đối với tôi vì vậy tôi có thể hiểu các trình biên dịch khác nhau sắp ra mắt với các kết quả khác nhau. T1, T2, T3 là gì? Nó có nghĩa là các loại đối số? (Tôi nghi ngờ là không). T1, T2, T3 là gì khi operator()tồn tại nhiều hàm, chỉ nhận một đối số?

T::operator()dù sao thì "nếu tồn tại" có nghĩa là gì? Nó có thể có nghĩa là bất kỳ điều nào sau đây:

  1. operator()được khai báo trong T.
  2. Việc tra cứu không đủ tiêu chuẩn operator()trong phạm vi Tthành công và thực hiện giải quyết quá tải trên tập hợp tra cứu đó với các đối số đã cho sẽ thành công.
  3. Tra cứu đủ điều kiện T::operator()trong ngữ cảnh gọi thành công và thực hiện giải quyết quá tải trên tập hợp tra cứu đó với các đối số đã cho thành công.
  4. Thứ gì khác?

Để tiếp tục từ đây (dù sao đối với tôi), tôi muốn hiểu tại sao tiêu chuẩn không chỉ đơn giản nói điều đó f(123)có nghĩa là f.operator()(123);, tiêu chuẩn trước là không hợp lệ nếu và chỉ khi tiêu chuẩn sau là sai. Động cơ đằng sau từ ngữ thực tế có thể tiết lộ ý định và do đó hành vi của trình biên dịch phù hợp với ý định.

0 hữu ích 5 bình luận chia sẻ
loading
Không tìm thấy câu trả lời bạn tìm kiếm? Duyệt qua các câu hỏi được gắn thẻ c++ lambda language-lawyer multiple-inheritance overload-resolution , hoặc hỏi câu hỏi của bạn.

Có thể bạn quan tâm

loading