Loại trừ xấu khi truyền con trỏ hàm quá tải và các đối số của nó


8

Tôi đang cố gắng cung cấp một trình bao bọc xung quanh std::invokeđể thực hiện công việc suy ra loại chức năng ngay cả khi chức năng bị quá tải.
(Tôi đã hỏi một câu hỏi liên quan ngày hôm qua cho phiên bản con trỏ phương thức và phương thức).

Khi hàm có một đối số, mã này (C ++ 17) hoạt động như mong đợi trong điều kiện quá tải thông thường:

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

Việc giảm sự phình mã rõ ràng là cần thiết cho nhiều đối số đầu vào, nhưng đó là một vấn đề riêng biệt.

Nếu người dùng có quá tải chỉ khác nhau bởi const / tham chiếu, như vậy:

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

Sau đó Invokesuy ra hàm không chính xác, với đầu ra g ++:

(int &)
(const int &)

(int &&)
(const int &)

Và kêu vang ++:

(int &)
(const int &)

(int &&)
(int &&)

(Cảm ơn geza vì đã chỉ ra rằng kết quả đầu ra của clang là khác nhau).

Vì vậy, Invokecó hành vi không xác định.

Tôi nghi ngờ rằng siêu lập trình sẽ là cách để tiếp cận vấn đề này. Bất kể, có thể xử lý loại khấu trừ chính xác tại Invoketrang web?


Sản lượng dự kiến ​​là gì? Có phải (int &) (int &&)?
LF

@LF, vâng. Đó là đầu ra của Foo, vì vậy chúng cũng phải là đầu ra của Gọi.
Elliott

1
Đối với tôi, clang cho một kết quả khác: nó in (int &&)hai lần cho trường hợp thứ hai.
geza

2
Nó phải có một cái gì đó để làm với Ssuy luận. Cố gắng nhận xét const T &phiên bản Invokevà thông báo lỗi. Ngoài ra nếu đối số được cung cấp rõ ràng ( Invoke<void>(&Foo, num)) phiên bản chính xác được gọi.
aparpara

2
Đây là một lý thuyết cho trường hợp đầu tiên: khi trình biên dịch xem xét non-const Invoke, nó có thể khởi tạo nó bằng cả const và non-const Foo. Và nó không kiểm tra loại trả về ( S) giống nhau cho cả hai, vì vậy nó nói rằng nó không thể suy ra S. Vì vậy, nó bỏ qua mẫu này. Trong khi khởi tạo const chỉ Invokecó thể được thực hiện với const Foo, vì vậy nó có thể suy ra Strong trường hợp này. Do đó trình biên dịch sử dụng mẫu này.
geza

Câu trả lời:


1

Học thuyết

Đối với mỗi mẫu hàm Invoke, việc khấu trừ đối số mẫu (phải thành công cho độ phân giải quá tải để xem xét nó) xem xét từng cái Foo để xem liệu nó có thể suy ra tuy nhiên nhiều tham số mẫu (ở đây, hai) cho một tham số hàm ( func) liên quan hay không. Khấu trừ tổng thể chỉ có thể thành công nếu chính xác một Footrận đấu (vì nếu không thì không có cách nào để suy luận S). (Điều này ít nhiều đã được nêu trong các ý kiến.)

Cái đầu tiên (khác theo giá trị) Invokekhông bao giờ tồn tại: nó có thể suy ra từ bất kỳ Foos nào. Tương tự, constquá tải thứ hai (không tham chiếu trực tiếp) chấp nhận hai Foos đầu tiên . Lưu ý rằng những điều này áp dụng bất kể đối số khác với Invoke(cho arg)!

const T&Quá tải thứ ba ( ) chọn Fooquá tải tương ứng và suy ra T= int; cái cuối cùng làm điều tương tự với sự quá tải cuối cùng (trong đó T&&là một tham chiếu giá trị bình thường), và do đó loại bỏ các đối số giá trị mặc dù tham chiếu phổ quát của nó (suy ra Tint&(hoặc const int&) trong trường hợp đó và mâu thuẫn với funcsuy luận của nó).

Trình biên dịch

Nếu đối số cho arglà một rvalue (và, như thường lệ, không phải là const), cả hai chính đáng Invokequá tải thành công tại khấu trừ, và T&&tình trạng quá tải nên giành chiến thắng (vì nó liên kết với một tài liệu tham khảo rvalue một rvalue ).

Đối với trường hợp từ các ý kiến:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

Không có sự khấu trừ nào xảy ra &Barkể từ khi một mẫu hàm có liên quan, do đó Tđược suy luận thành công (như int) trong mọi trường hợp. Sau đó, khấu trừ xảy ra một lần nữa cho từng trường hợp để xác định Barchuyên môn (nếu có) để sử dụng, suy luận Unhư thất bại , int&, const int&, và int&tương ứng. Các int&trường hợp là giống hệt nhau và rõ ràng tốt hơn, vì vậy cuộc gọi là mơ hồ.

Vì vậy, Clang ở ngay đây. (Nhưng không có hành vi không xác định của người Viking ở đây.)

Giải pháp

Tôi không có câu trả lời chung cho bạn; do các loại tham số nhất định có thể chấp nhận nhiều cặp giá trị loại / giá trị const, nên sẽ không dễ dàng mô phỏng độ phân giải quá tải một cách chính xác trong tất cả các trường hợp như vậy. Đã có những đề xuất để thống nhất các bộ quá tải theo cách này hay cách khác; bạn có thể xem xét một trong những kỹ thuật hiện tại dọc theo các dòng đó (như lambda chung cho mỗi tên hàm mục tiêu).

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.