Tại sao std :: function không tham gia giải quyết quá tải?


17

Tôi biết rằng đoạn mã sau sẽ không được biên dịch.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Bởi vì std::functionđược cho là không xem xét giải quyết quá tải, như tôi đã đọc trong bài viết khác này .

Tôi không hiểu đầy đủ các hạn chế kỹ thuật đã buộc loại giải pháp này.

Tôi đã đọc về các giai đoạn dịch thuật và các mẫu trên cppreference, nhưng tôi không thể nghĩ ra bất kỳ lý do nào mà tôi không thể tìm thấy một ví dụ nào. Giải thích cho một người nửa giáo dân (vẫn còn mới đối với C ++), điều gì và trong giai đoạn dịch thuật nào khiến cho những điều trên không được biên dịch?


1
@Evg nhưng chỉ có một tình trạng quá tải sẽ hợp lý khi được chấp nhận trong ví dụ về OP. Trong ví dụ của bạn, cả hai sẽ thực hiện một trận đấu
idclev 463035818

Câu trả lời:


13

Điều này thực sự không có gì để làm với "các giai đoạn dịch". Đó hoàn toàn là về các nhà xây dựng của std::function.

Xem, std::function<R(Args)>không yêu cầu chức năng đã cho là chính xác của loạiR(Args) . Cụ thể, nó không yêu cầu rằng nó được cung cấp một con trỏ hàm. Nó có thể lấy bất kỳ loại có thể gọi được (con trỏ hàm thành viên, một số đối tượng có quá tải operator()) miễn là nó bất khả xâm phạm như thể nó lấy Argstham số và trả về một cái gì đó có thể chuyển đổi thành R (hoặc nếu Rvoid, nó có thể trả về bất cứ thứ gì).

Để làm điều đó, hàm tạo thích hợp của std::functionphải là một mẫu : template<typename F> function(F f);. Đó là, nó có thể có bất kỳ loại chức năng nào (tuân theo các hạn chế ở trên).

Biểu thức bazđại diện cho một tập hợp quá tải. Nếu bạn sử dụng biểu thức đó để gọi tập quá tải, thì tốt thôi. Nếu bạn sử dụng biểu thức đó làm tham số cho một hàm có một con trỏ hàm cụ thể, C ++ có thể giảm bớt tình trạng quá tải được đặt thành một cuộc gọi, do đó sẽ ổn.

Tuy nhiên, một khi hàm là một mẫu và bạn đang sử dụng suy luận đối số mẫu để tìm ra tham số đó là gì, C ++ không còn có khả năng xác định mức quá tải chính xác trong tập quá tải là gì. Vì vậy, bạn phải xác định nó trực tiếp.


Xin lỗi nếu tôi có điều gì đó sai, nhưng tại sao C ++ không có khả năng phân biệt giữa quá tải có sẵn? Bây giờ tôi thấy rằng std :: function <T> chấp nhận bất kỳ loại tương thích nào, không chỉ là một kết hợp chính xác, mà chỉ một trong hai quá tải baz () đó là bất khả xâm phạm như thể nó đã lấy các tham số đã chỉ định. Tại sao không thể định hướng?
TuRtoise

Bởi vì tất cả C ++ thấy là chữ ký tôi trích dẫn. Nó không biết những gì nó được cho là phù hợp với. Về cơ bản, bất cứ khi nào bạn sử dụng một bộ quá tải chống lại bất cứ điều gì không rõ ràng câu trả lời đúng rõ ràngtừ mã C ++ (mã là tuyên bố mẫu đó), ngôn ngữ buộc bạn phải đánh vần những gì bạn muốn nói.
Nicol Bolas

1
@TuRtoise: Tham số mẫu trên functionmẫu lớp là không liên quan . Vấn đề là tham số mẫu trên hàm tạo mà bạn đang gọi. Đó chỉ là typename F: aka, bất kỳ loại.
Nicol Bolas

1
@TuRtoise: Tôi nghĩ bạn đang hiểu nhầm điều gì đó. Đó là "chỉ F" bởi vì đó là cách các mẫu hoạt động. Từ chữ ký, hàm tạo đó có bất kỳ kiểu nào, do đó, bất kỳ nỗ lực nào để gọi nó bằng phép trừ đối số mẫu sẽ suy ra kiểu từ tham số. Bất kỳ nỗ lực để áp dụng khấu trừ cho một bộ quá tải là một lỗi biên dịch. Trình biên dịch không cố gắng suy luận mọi loại có thể từ tập hợp để xem loại nào hoạt động.
Nicol Bolas

1
"Bất kỳ nỗ lực nào để áp dụng khấu trừ cho một tập hợp quá tải đều là một lỗi biên dịch. Trình biên dịch không cố gắng suy luận mọi loại có thể từ tập hợp để xem cái nào hoạt động." Chính xác những gì tôi đã thiếu, cảm ơn bạn :)
TuRtoise

7

Quá trình phân giải quá tải chỉ xảy ra khi (a) bạn đang gọi tên của hàm / toán tử hoặc (b) đang truyền nó tới một con trỏ (đến hàm hoặc hàm thành viên) bằng chữ ký rõ ràng.

Không có gì xảy ra ở đây.

std::functionlấy bất kỳ đối tượng tương thích với chữ ký của nó. Nó không có một con trỏ hàm cụ thể. (lambda không phải là hàm std và hàm std không phải là lambda)

Bây giờ trong các biến thể chức năng homebrew của tôi, đối với chữ ký, R(Args...)tôi cũng chấp nhậnR(*)(Args...) đối số (khớp chính xác) cho chính xác lý do này. Nhưng nó có nghĩa là nó nâng chữ ký "khớp chính xác" trên chữ ký "tương thích".

Vấn đề cốt lõi là một bộ quá tải không phải là một đối tượng C ++. Bạn có thể đặt tên cho một tập hợp quá tải, nhưng bạn không thể vượt qua nó "nguyên bản".

Bây giờ, bạn có thể tạo một tập hợp giả quá tải của một hàm như thế này:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

điều này tạo ra một đối tượng C ++ duy nhất có thể thực hiện phân giải quá tải trên tên hàm.

Mở rộng các macro, chúng tôi nhận được:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

đó là khó chịu để viết. Một phiên bản đơn giản hơn, ít hữu ích hơn một chút ở đây:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

chúng ta có một lambda có bất kỳ số lượng đối số nào, sau đó hoàn hảo chuyển tiếp chúng tới baz.

Sau đó:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

làm. Chúng tôi trì hoãn độ phân giải quá tải vào lambda mà chúng tôi lưu trữ fun, thay vì vượt quafun trực tiếp bộ quá tải (điều này không thể giải quyết).

Đã có ít nhất một đề xuất để xác định một hoạt động trong ngôn ngữ C ++ để chuyển đổi tên hàm thành một đối tượng tập quá tải. Cho đến khi một đề xuất tiêu chuẩn như vậy là trong tiêu chuẩn, OVERLOADS_OFmacro là hữu ích.

Bạn có thể tiến thêm một bước và hỗ trợ con trỏ cast-to-tương thích-hàm-con trỏ.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

nhưng điều đó đang bắt đầu trở nên khó hiểu

Ví dụ sống .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }

5

Vấn đề ở đây là không có gì cho trình biên dịch biết cách thực hiện chức năng để phân rã con trỏ. Nếu bạn có

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Sau đó, mã sẽ hoạt động kể từ bây giờ trình biên dịch biết chức năng bạn muốn vì có một loại cụ thể mà bạn đang gán.

Khi bạn sử dụng, std::functionbạn gọi hàm tạo đối tượng hàm của nó có dạng

template< class F >
function( F f );

và vì nó là một khuôn mẫu, nên nó cần suy ra loại đối tượng được truyền. vì bazlà một hàm quá tải nên không có loại nào có thể được suy ra nên việc khấu trừ mẫu không thành công và bạn gặp lỗi. Bạn sẽ phải sử dụng

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

để có được một loại duy nhất và cho phép khấu trừ.


"vì baz là một hàm bị quá tải nên không có loại nào có thể suy ra được" nhưng vì C ++ 14 "nên hàm tạo này không tham gia vào độ phân giải quá tải trừ khi f có thể gọi được cho các loại đối số Đối số ... và trả về loại R" Tôi là không chắc chắn, nhưng tôi đã gần như mong đợi rằng điều này sẽ đủ để giải quyết sự mơ hồ
idclev 463035818

3
@ trước đây là Unknown_463035818 Nhưng để xác định điều đó, trước tiên cần phải suy ra một loại và không thể vì đó là một tên quá tải.
NathanOliver

1

Tại thời điểm trình biên dịch đang quyết định quá tải nào sẽ chuyển vào hàm std::functiontạo, tất cả những gì nó biết là hàm std::functiontạo được tạo khuôn mẫu để lấy bất kỳ kiểu nào. Nó không có khả năng thử cả hai quá tải và thấy rằng cái đầu tiên không biên dịch nhưng cái thứ hai thì không.

Cách để giải quyết vấn đề này là nói rõ cho trình biên dịch biết mức quá tải mà bạn muốn với static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
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.