ทำไม std :: function ไม่เข้าร่วมในการแก้ปัญหาโอเวอร์โหลด?


17

ฉันรู้ว่ารหัสต่อไปนี้จะไม่รวบรวม

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;
}

เนื่องจากstd::functionมีการกล่าวว่าไม่ต้องพิจารณาความละเอียดที่มากเกินไปเช่นที่ฉันอ่านในโพสต์นี้

ฉันไม่เข้าใจข้อ จำกัด ทางเทคนิคที่บังคับให้ใช้โซลูชันประเภทนี้อย่างเต็มที่

ฉันอ่านเกี่ยวกับขั้นตอนการแปลและเทมเพลตใน cppreference แต่ฉันไม่สามารถคิดได้ว่ามีเหตุผลใดที่ฉันไม่สามารถหาตัวอย่างตัวอย่างได้ อธิบายให้คนธรรมดาสามัญคนหนึ่ง (ยังใหม่กับ C ++) อะไรที่และในระหว่างขั้นตอนการแปลที่ทำให้ข้างต้นล้มเหลวในการรวบรวม?


1
@Evg แต่มีเพียงโอเวอร์โหลดเดียวเท่านั้นที่เหมาะสมที่จะได้รับการยอมรับในตัวอย่าง OPs ในตัวอย่างของคุณทั้งคู่จะจับคู่กัน
idclev 463035818

คำตอบ:


13

สิ่งนี้ไม่มีอะไรเกี่ยวข้องกับ "ขั้นตอนการแปล" มันเป็นเรื่องของการก่อสร้างstd::functionมันเป็นอย่างหมดจดเกี่ยวกับการก่อสร้างของ

เห็นstd::function<R(Args)>ไม่จำเป็นต้องมีฟังก์ชั่นที่ได้รับคือตรงR(Args)ประเภท โดยเฉพาะอย่างยิ่งไม่จำเป็นต้องให้ตัวชี้ฟังก์ชัน มันสามารถใช้ชนิดใดก็ได้ callable (ตัวชี้ฟังก์ชั่นสมาชิกวัตถุบางอย่างที่มีเกินoperator()) ตราบใดที่มันสามารถเรียกว่าถ้ามันใช้Argsพารามิเตอร์และส่งกลับสิ่งที่แปลงเป็น R (หรือถ้าRเป็นvoidก็สามารถกลับอะไร)

ในการทำเช่นนั้น Constructor ที่เหมาะสมstd::functionต้องเป็นเทมเพลต : template<typename F> function(F f);. นั่นคือมันสามารถใช้ฟังก์ชั่นประเภทใดก็ได้ (ขึ้นอยู่กับข้อ จำกัด ข้างต้น)

นิพจน์แสดงbazถึงชุดโอเวอร์โหลด หากคุณใช้นิพจน์นั้นเพื่อเรียกชุดโอเวอร์โหลดก็ถือว่าใช้ได้ หากคุณใช้นิพจน์นั้นเป็นพารามิเตอร์ของฟังก์ชันที่ใช้ตัวชี้ฟังก์ชันเฉพาะ C ++ สามารถลดการโอเวอร์โหลดที่ตั้งค่าให้เป็นการเรียกครั้งเดียวได้

อย่างไรก็ตามเมื่อฟังก์ชันเป็นเทมเพลตและคุณกำลังใช้การหักล้างอาร์กิวเมนต์เทมเพลตเพื่อหาว่าพารามิเตอร์นั้นคืออะไร C ++ จะไม่มีความสามารถในการกำหนดว่าโอเวอร์โหลดที่ถูกต้องในชุดโอเวอร์โหลดคืออะไร ดังนั้นคุณต้องระบุโดยตรง


ให้อภัยฉันถ้าฉันทำอะไรผิด แต่ทำไมซีพลัสพลัสถึงไม่มีความสามารถในการแยกแยะระหว่างโอเวอร์โหลดที่มีอยู่ ฉันเห็นแล้วว่า std :: function <T> ยอมรับประเภทที่เข้ากันได้ไม่เพียง แต่การจับคู่ที่ตรงกัน แต่มีเพียงหนึ่งในสอง baz () ที่เกินพิกัดเท่านั้นที่สามารถเรียกใช้งานได้ราวกับว่าใช้พารามิเตอร์ที่ระบุ ทำไมจึงเป็นไปไม่ได้ที่จะทำให้เข้าใจผิด?
TuRtoise

เพราะ C ++ เห็นเป็นลายเซ็นต์ที่ฉันยกมา ไม่ทราบว่าควรจะเปรียบเทียบอะไร โดยพื้นฐานแล้วเมื่อใดก็ตามที่คุณใช้ชุดโอเวอร์โหลดเทียบกับสิ่งใดก็ตามที่ไม่ชัดเจนว่าคำตอบที่ถูกต้องคืออะไรอย่างชัดเจนจากรหัส C ++ (รหัสที่มีการประกาศเทมเพลตนั้น) ภาษาบังคับให้คุณสะกดคำว่าคุณหมายถึงอะไร
Nicol Bolas

1
@TuRtoise: พารามิเตอร์แม่แบบในfunctionแม่แบบเรียนที่ไม่เกี่ยวข้อง สิ่งสำคัญคือพารามิเตอร์แม่แบบในตัวสร้างที่คุณกำลังเรียก ซึ่งเป็นเพียงtypename F: aka ประเภทใด
Nicol Bolas

1
@ TuRtoise: ฉันคิดว่าคุณเข้าใจผิดบางอย่าง มันคือ "just F" เพราะนั่นเป็นวิธีที่เทมเพลตทำงาน จากลายเซ็นตัวสร้างฟังก์ชั่นนั้นจะใช้รูปแบบใด ๆ ดังนั้นความพยายามใด ๆ ที่จะเรียกมันด้วยการลดอาร์กิวเมนต์เทมเพลตจะเป็นการอนุมานประเภทจากพารามิเตอร์ ความพยายามในการใช้การหักเงินกับชุดเกินนั้นเป็นข้อผิดพลาดในการคอมไพล์ คอมไพเลอร์จะไม่พยายามอนุมานประเภทที่เป็นไปได้ทั้งหมดจากชุดเพื่อดูว่าประเภทใดใช้งานได้
Nicol Bolas

1
"ความพยายามใด ๆ ที่จะนำการหักเงินไปใช้กับชุดเกินเป็นข้อผิดพลาดในการคอมไพล์. คอมไพเลอร์ไม่ได้พยายามอนุมานทุกประเภทที่เป็นไปได้จากชุดเพื่อดูว่าอันไหนที่ทำงานได้" สิ่งที่ฉันหายไปขอบคุณ :)
TuRtoise

7

ความละเอียดเกินพิกัดเกิดขึ้นเฉพาะเมื่อ (a) คุณกำลังเรียกชื่อของฟังก์ชัน / โอเปอเรเตอร์หรือ (b) กำลังส่งไปยังตัวชี้ (ไปยังฟังก์ชันหรือฟังก์ชันสมาชิก) ด้วยลายเซ็นที่ชัดเจน

ไม่เกิดขึ้นที่นี่

std::functionใช้วัตถุใด ๆ ที่เข้ากันได้กับลายเซ็นของมัน ไม่ใช้ตัวชี้ฟังก์ชันโดยเฉพาะ (แลมบ์ดาไม่ใช่ฟังก์ชั่นมาตรฐานและฟังก์ชั่นมาตรฐานไม่ใช่แลมบ์ดา)

ตอนนี้อยู่ในรูปแบบฟังก์ชัน homebrew ของฉันสำหรับลายเซ็นR(Args...)ฉันก็ยอมรับR(*)(Args...)อาร์กิวเมนต์ (การจับคู่แบบตรง) ด้วยเหตุผลนี้ แต่หมายความว่ายกระดับลายเซ็น "จับคู่แบบตรงทั้งหมด" ด้านบนลายเซ็น "ที่เข้ากันได้"

ปัญหาหลักคือชุดโอเวอร์โหลดไม่ใช่วัตถุ C ++ คุณสามารถตั้งชื่อชุดโอเวอร์โหลดได้ แต่คุณไม่สามารถผ่านมันไป "natively"

ตอนนี้คุณสามารถสร้างชุดหลอกของฟังก์ชันเช่นนี้:

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

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

สิ่งนี้จะสร้างอ็อบเจกต์ C ++ เดียวที่สามารถทำการแก้ปัญหาโอเวอร์โหลดในชื่อฟังก์ชันได้

เราจะขยายมาโครให้มากขึ้น:

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

ซึ่งน่ารำคาญที่จะเขียน เวอร์ชั่นที่เรียบง่ายมีประโยชน์น้อยกว่าอยู่ที่นี่:

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

เรามีแลมบ์ดาที่รับการโต้แย้งจำนวนหนึ่งแล้วจึงส่งต่อให้สมบูรณ์ bazสมบูรณ์

แล้ว:

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

โรงงาน เราเลื่อนการแก้ปัญหาการโอเวอร์โหลดลงในแลมบ์ดาที่เราเก็บไว้funแทนที่จะส่งผ่านfunการตั้งค่าโอเวอร์โหลดโดยตรง (ซึ่งไม่สามารถแก้ไขได้)

มีข้อเสนออย่างน้อยหนึ่งข้อในการกำหนดการดำเนินการในภาษา C ++ ที่แปลงชื่อฟังก์ชั่นเป็นวัตถุชุดเกินพิกัด จนกว่าข้อเสนอมาตรฐานดังกล่าวจะอยู่ในมาตรฐานOVERLOADS_OFแมโครจะมีประโยชน์

คุณสามารถไปอีกขั้นหนึ่งและสนับสนุนการทำงานของตัวชี้แบบคาสต์

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)...); };
  }
};

แต่นั่นเริ่มที่จะได้รับป้าน

ตัวอย่างสด

#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

ปัญหานี่คือไม่มีอะไรบอกคอมไพเลอร์วิธีการทำหน้าที่ตัวชี้สลาย ถ้าคุณมี

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;
}

จากนั้นโค้ดจะใช้งานได้ตั้งแต่ตอนนี้คอมไพเลอร์รู้ว่าฟังก์ชันใดที่คุณต้องการเนื่องจากมีรูปแบบที่คุณกำหนด

เมื่อคุณใช้std::functionคุณเรียกมันเป็นฟังก์ชั่นวัตถุตัวสร้างซึ่งมีรูปแบบ

template< class F >
function( F f );

และเนื่องจากเป็นเทมเพลตจึงจำเป็นต้องอนุมานประเภทของวัตถุที่ส่งผ่าน เนื่องจากbazเป็นฟังก์ชั่นโอเวอร์โหลดจึงไม่มีประเภทเดียวที่สามารถอนุมานได้เพื่อให้การลบแม่แบบล้มเหลวและคุณได้รับข้อผิดพลาด คุณจะต้องใช้

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

เพื่อบังคับชนิดเดียวและอนุญาตให้มีการหัก


"เนื่องจาก baz เป็นฟังก์ชันโอเวอร์โหลดไม่มีประเภทเดียวที่สามารถอนุมานได้" แต่เนื่องจาก C ++ 14 "คอนสตรัคนี้ไม่ได้มีส่วนร่วมในการแก้ปัญหาโอเวอร์โหลดเว้นแต่ f คือ Callable สำหรับอาร์กิวเมนต์ประเภท Args ... และส่งคืนชนิด R" ฉัน ไม่แน่ใจ แต่ฉันก็ใกล้จะคาดหวังว่ามันจะเพียงพอที่จะแก้ไขความคลุมเครือ
idclev 463035818

3
@ ชื่อก่อนหน้านี้ _463035818 แต่เพื่อตรวจสอบว่ามันต้องอนุมานประเภทก่อนและมันไม่สามารถเพราะมันเป็นชื่อที่มากเกินไป
NathanOliver

1

ณ จุดที่คอมไพเลอร์ตัดสินใจว่าโอเวอร์โหลดใดที่จะผ่านเข้าไปในคอนstd::functionสตรัคเตอร์ทั้งหมดที่รู้ก็คือstd::functionสตรัคเตอร์นั้นมีเทมเพลตเพื่อใช้งานทุกประเภท มันไม่มีความสามารถในการลองโอเวอร์โหลดทั้งสองและพบว่าอันแรกไม่ได้คอมไพล์ แต่อันที่สองทำ

วิธีการแก้ไขปัญหานี้คือการบอกคอมไพเลอร์ว่าโอเวอร์โหลดที่คุณต้องการด้วยstatic_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.