อะไรคือกฎพื้นฐานและสำนวนสำหรับผู้ให้บริการมากไป?


2141

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

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


63
หากเราจะดำเนินการต่อด้วยแท็ก C ++ - คำถามที่พบบ่อยนี่คือวิธีการจัดรูปแบบรายการ
John Dibling

ฉันได้เขียนบทความสั้น ๆ สำหรับชุมชนภาษาเยอรมัน C ++ เกี่ยวกับการบรรทุกเกินพิกัด: ส่วนที่ 1: การบรรทุกเกินพิกัดใน C ++ครอบคลุมความหมายการใช้งานทั่วไปและความชำนาญพิเศษสำหรับผู้ประกอบการทั้งหมด มีการทับซ้อนกับคำตอบของคุณที่นี่ แต่ก็มีข้อมูลเพิ่มเติม ส่วนที่ 2 และ 3 ทำแบบฝึกหัดสำหรับใช้ Boost.Operators คุณต้องการให้ฉันแปลและเพิ่มเป็นคำตอบหรือไม่
Arne Mertz

โอ้และยังมีการแปลภาษาอังกฤษด้วย: พื้นฐานและการปฏิบัติทั่วไป
Arne Mertz

คำตอบ:


1042

ผู้ประกอบการทั่วไปที่จะเกินพิกัด

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

ผู้ประกอบการที่ได้รับมอบหมาย

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

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

ผู้ประกอบการ Bitshift (ใช้สำหรับสตรีม I / O)

ตัวดำเนินการ bitshift <<และ>>แม้ว่ายังคงใช้ในการเชื่อมต่อฮาร์ดแวร์สำหรับฟังก์ชันการจัดการบิตที่สืบทอดจาก C ได้กลายเป็นที่แพร่หลายมากขึ้นเป็นตัวดำเนินการอินพุตและเอาต์พุตสตรีมที่โอเวอร์โหลดในแอพพลิเคชันส่วนใหญ่ สำหรับคำแนะนำมากไปในฐานะผู้ดำเนินการจัดการบิตดูส่วนด้านล่างของตัวดำเนินการทางคณิตศาสตร์แบบไบนารี สำหรับการนำรูปแบบที่กำหนดเองของคุณเองและตรรกะการแยกวิเคราะห์เมื่อวัตถุของคุณใช้กับ iostreams ให้ดำเนินการต่อ

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

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // write obj to stream

  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // read obj from stream

  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);

  return is;
}

เมื่อใช้งานการoperator>>ตั้งค่าสถานะของสตรีมด้วยตนเองเป็นสิ่งจำเป็นเฉพาะเมื่อการอ่านตัวเองสำเร็จ แต่ผลลัพธ์ไม่ใช่สิ่งที่คาดหวัง

ผู้ประกอบการโทรฟังก์ชั่น

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

นี่คือตัวอย่างของไวยากรณ์:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

การใช้งาน:

foo f;
int a = f("hello");

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

ผู้ประกอบการเปรียบเทียบ

ผู้ประกอบการเปรียบเทียบไบนารีมัดควรตามกฎของหัวแม่มือที่จะนำมาใช้เป็นฟังก์ชั่นที่ไม่ใช่สมาชิก1 การปฏิเสธคำนำหน้า unary !ควรใช้ (ตามกฎเดียวกัน) ในฐานะสมาชิกฟังก์ชัน (แต่โดยทั่วไปแล้วไม่ใช่ความคิดที่ดีที่จะโอเวอร์โหลด)

อัลกอริธึมของไลบรารีมาตรฐาน (เช่นstd::sort()) และประเภท (เช่นstd::map) จะคาดหวังoperator<ให้แสดงอยู่เสมอ อย่างไรก็ตามผู้ใช้ประเภทของคุณจะคาดหวังว่าผู้ให้บริการรายอื่นทั้งหมดจะอยู่ด้วยเช่นกันดังนั้นหากคุณกำหนดoperator<ให้แน่ใจว่าได้ปฏิบัติตามกฎพื้นฐานที่สามของผู้ประกอบการมากไปและยังกำหนดผู้ประกอบการเปรียบเทียบแบบบูลอื่น ๆ วิธีที่เป็นที่ยอมรับในการใช้งานคือ:

inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return  operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}

สิ่งสำคัญที่ควรทราบที่นี่คือผู้ดำเนินการเหล่านี้เพียงสองคนเท่านั้นที่ทำอะไรจริง ๆ คนอื่น ๆ กำลังส่งต่อข้อโต้แย้งไปยังทั้งสองคนนี้เพื่อทำงานจริง

ไวยากรณ์สำหรับการโหลดตัวดำเนินการบูลีนไบนารีบูลีนที่เหลืออยู่ ( ||, &&) เป็นไปตามกฎของตัวดำเนินการเปรียบเทียบ แต่ก็เป็นมากไม่น่าที่คุณจะพบกรณีการใช้งานที่เหมาะสมสำหรับทั้ง2

1 เช่นเดียวกับกฎของหัวแม่มือบางครั้งอาจมีเหตุผลที่จะทำลายสิ่งนี้เช่นกัน ถ้าเป็นเช่นนั้นอย่าลืมว่าตัวถูกดำเนินการทางด้านซ้ายของตัวดำเนินการเปรียบเทียบแบบไบนารีซึ่งสำหรับฟังก์ชันของสมาชิกจะ*thisต้องเป็นconstเช่นกัน ดังนั้นตัวดำเนินการเปรียบเทียบที่นำมาใช้เป็นฟังก์ชันสมาชิกจะต้องมีลายเซ็นนี้:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(สังเกตconstที่ท้าย)

2 ควรสังเกตว่าเวอร์ชั่นในตัว||และ&&ใช้ซีแมนทิกส์ช็อตคัท ในขณะที่ผู้ใช้กำหนดคน (เพราะพวกเขาเป็นน้ำตาล syntactic สำหรับวิธีการโทร) ไม่ใช้ความหมายทางลัด ผู้ใช้จะคาดหวังว่าโอเปอเรเตอร์เหล่านี้จะมีซีแมนทิกส์ช็อตคัทและรหัสของพวกเขาอาจขึ้นอยู่กับมันดังนั้นจึงไม่แนะนำให้กำหนดอย่างแน่นอน

ตัวดำเนินการทางคณิตศาสตร์

ตัวดำเนินการทางคณิตศาสตร์แบบเอกนารี

โอเปอเรเตอร์การเพิ่มและการลดค่า unary มีทั้งคำนำหน้าและรสชาติของคำนำหน้า หากต้องการบอกอีกตัวหนึ่งตัวแปร postfix จะใช้อาร์กิวเมนต์ดัมมี int เพิ่มเติม หากคุณเพิ่มหรือลดจำนวนมากเกินควรใช้ทั้งคำนำหน้าและเวอร์ชัน postfix เสมอ นี่คือการใช้งานแบบบัญญัติของการเพิ่มขึ้นการลดลงเป็นไปตามกฎเดียวกัน:

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

โปรดทราบว่าชุดตัวเลือก postfix นั้นมีการใช้งานในรูปแบบของคำนำหน้า นอกจากนี้โปรดทราบว่า postfix จะทำสำเนาเพิ่มเติม 2

การบรรทุกเกินพิกัดลบและบวกไม่ธรรมดามากและอาจหลีกเลี่ยงได้ดีที่สุด หากจำเป็นพวกเขาอาจจะโอเวอร์โหลดเป็นฟังก์ชันสมาชิก

2 โปรดทราบว่าตัวแปร postfix ทำงานได้มากกว่าและดังนั้นจึงมีประสิทธิภาพน้อยกว่าการใช้งานกว่าตัวแปร prefix นี่เป็นเหตุผลที่ดีที่จะชอบการเพิ่มค่า prefix มากกว่าการเพิ่ม postfix ในขณะที่คอมไพเลอร์สามารถเพิ่มประสิทธิภาพการทำงานเพิ่มเติมของการเพิ่ม postfix สำหรับชนิดในตัวพวกเขาอาจไม่สามารถทำแบบเดียวกันกับประเภทที่ผู้ใช้กำหนด (ซึ่งอาจเป็นสิ่งที่ดูเป็นผู้ทำรายการ) เมื่อคุณคุ้นเคยi++ก็จะยากที่จะจำ++iแทนเมื่อiไม่มีประเภทในตัว (รวมทั้งคุณต้องเปลี่ยนรหัสเมื่อเปลี่ยนประเภท) ดังนั้นจึงเป็นการดีกว่าที่จะสร้างนิสัยของเสมอ ใช้การเพิ่มคำนำหน้ายกเว้นว่าจำเป็นต้องใช้ postfix อย่างชัดเจน

ตัวดำเนินการทางคณิตศาสตร์แบบไบนารี

สำหรับผู้ประกอบการเลขคณิตไบนารีอย่าลืมที่จะปฏิบัติตามกฎพื้นฐานตัวที่สามของการบรรทุกเกินพิกัด: หากคุณให้บริการ+นอกจากนี้+=หากคุณจัดหาให้-อย่าละเว้น-=ฯลฯ แอนดรูว์นิกกล่าวว่าเป็นคนแรกที่สังเกตว่าการมอบหมายผสม ผู้ประกอบการสามารถใช้เป็นฐานสำหรับคู่ที่ไม่ได้ประกอบของพวกเขา นั่นคือผู้ประกอบการ+จะดำเนินการในแง่ของ+=, -มีการใช้งานในแง่ของ-=ฯลฯ

ตามกฎของเราหัวแม่มือ+และสหายของมันควรจะไม่ใช่สมาชิกในขณะที่คู่หูที่ได้รับมอบหมายผสมของพวกเขา ( +=ฯลฯ ) เปลี่ยนอาร์กิวเมนต์ซ้ายของพวกเขาควรจะเป็นสมาชิก นี่คือรหัสที่เป็นแบบอย่างสำหรับ+=และ+; ผู้ประกอบการเลขคณิตไบนารีอื่น ๆ ควรนำมาใช้ในลักษณะเดียวกัน:

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}

operator+=ส่งคืนผลลัพธ์ตามการอ้างอิงขณะที่operator+ส่งคืนสำเนาผลลัพธ์ แน่นอนว่าการส่งคืนการอ้างอิงมักจะมีประสิทธิภาพมากกว่าการส่งคืนสำเนา แต่ในกรณีที่operator+ไม่มีวิธีแก้ไขการคัดลอก เมื่อคุณเขียนa + bคุณคาดหวังว่าผลลัพธ์จะเป็นค่าใหม่ซึ่งเป็นสาเหตุที่operator+ต้องส่งคืนค่าใหม่ 3 นอกจากนี้ยังทราบว่าoperator+จะใช้ตัวถูกดำเนินการด้านซ้ายโดยการคัดลอกมากกว่าโดยอ้างอิง const เหตุผลนี้เป็นเช่นเดียวกับเหตุผลที่ให้operator=การโต้แย้งต่อสำเนา

ตัวดำเนินการจัดการบิต~ & | ^ << >>ควรนำมาใช้ในลักษณะเดียวกับตัวดำเนินการทางคณิตศาสตร์ อย่างไรก็ตาม (ยกเว้นสำหรับการบรรทุกเกินพิกัด<<และ>>สำหรับเอาท์พุทและอินพุต) มีกรณีการใช้งานที่สมเหตุสมผลน้อยมากสำหรับการใช้งานมากไป

3 อีกครั้งบทเรียนที่ได้จากนี้คือa += bโดยทั่วไปมีประสิทธิภาพมากกว่าa + bและควรเป็นที่ต้องการถ้าเป็นไปได้

การห้อยอาร์เรย์

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

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

นอกเสียจากว่าคุณไม่ต้องการให้ผู้ใช้ในชั้นเรียนของคุณสามารถเปลี่ยนองค์ประกอบข้อมูลที่ส่งคืนโดยoperator[](ในกรณีนี้คุณสามารถละเว้นตัวแปรที่ไม่ใช่ const) คุณควรให้ทั้งสองตัวแปรของโอเปอเรเตอร์

ถ้า value_type เป็นที่รู้จักกันในการอ้างถึงชนิดของบิวด์อินตัวแปร const ของโอเปอเรเตอร์ควรส่งคืนสำเนาแทนการอ้างอิง const:

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

ตัวดำเนินการสำหรับประเภทที่เหมือนตัวชี้

สำหรับการกำหนดตัววนซ้ำหรือพอยน์เตอร์สมาร์ทของคุณเองคุณจะต้องโอเวอร์โหลดตัวดำเนินการ dereference ของคำนำหน้า unary *และตัวดำเนินการเข้าถึงสมาชิกของตัวชี้ไบนารี infix ->:

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

โปรดทราบว่าสิ่งเหล่านี้เช่นกันมักจะต้องมีทั้งรุ่น const และรุ่นที่ไม่ใช่แบบ const สำหรับ->โอเปอเรเตอร์หากvalue_typeเป็นประเภทclass(หรือstructหรือunion) อีกประเภทหนึ่งoperator->()จะถูกเรียกซ้ำจนกว่าจะoperator->()ส่งคืนค่าประเภทที่ไม่ใช่คลาส

ผู้ให้บริการ address-of unary ไม่ควรรับภาระมากเกินไป

สำหรับoperator->*()ดูคำถามนี้ มันไม่ค่อยได้ใช้และทำให้เกิดการโอเวอร์โหลดบ่อยครั้ง ในความเป็นจริงแม้ตัววนซ้ำจะไม่ทำงานหนักเกินไป


ดำเนินการต่อไปยังผู้ให้บริการการแปลง


89
operator->()เป็นจริงอย่างยิ่งแปลก ไม่จำเป็นต้องส่งคืนvalue_type*- ในความเป็นจริงมันสามารถคืนคลาสประเภทอื่นได้หากประเภทคลาสนั้นมีoperator->()แล้วซึ่งจะถูกเรียกในภายหลัง การเรียกซ้ำแบบนี้operator->()จะดำเนินต่อไปจนกว่าจะมีvalue_type*ชนิดส่งคืนเกิดขึ้น บ้า! :)
j_random_hacker

2
มันไม่ได้เกี่ยวกับประสิทธิภาพ มันเกี่ยวกับเราไม่สามารถทำมันด้วยวิธีดั้งเดิม - สำนวนในบางกรณี (มาก): เมื่อคำจำกัดความของตัวถูกดำเนินการทั้งสองจำเป็นต้องไม่เปลี่ยนแปลงในขณะที่เราคำนวณผลลัพธ์ และอย่างที่ฉันบอกมีสองตัวอย่างคลาสสิก: การคูณเมทริกซ์และการคูณพหุนาม เราสามารถกำหนด*ในแง่ของ*=แต่มันจะน่าอึดอัดใจเพราะหนึ่งในการดำเนินการครั้งแรกของความ*=ตั้งใจที่จะสร้างวัตถุใหม่ผลของการคำนวณ จากนั้นหลังจากที่ห่วงสำหรับ IJK *thisเราจะสลับวัตถุชั่วคราวนี้กับ กล่าวคือ 1. คัดลอก, 2. ผู้ปฏิบัติงาน *, 3.swap
Luc Hermitte

6
ฉันไม่เห็นด้วยกับตัวดำเนินการตัวชี้เหมือนรุ่น const / non-const ของคุณเช่น `const value_type & ตัวดำเนินการ * () const;` - นี่จะเหมือนกับการT* constคืนค่าconst T&การพิจารณาคดีซึ่งไม่ใช่กรณี หรือในคำอื่น ๆ : ตัวชี้ const ไม่ได้หมายความถึง const Pointee ในความเป็นจริงมันไม่ได้เป็นการเลียนแบบT const *- ซึ่งเป็นเหตุผลสำหรับconst_iteratorเนื้อหาทั้งหมดในห้องสมุดมาตรฐาน สรุป: ลายเซ็นควรเป็นreference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
One comment: การใช้งานตัวดำเนินการทางคณิตศาสตร์แบบไบนารีที่แนะนำนั้นไม่มีประสิทธิภาพเท่าที่ควร ผู้ประกอบการ Se Boost ส่วนหัวหมายเหตุที่คล้ายคลึงกัน: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetryอีกหนึ่งสำเนาสามารถหลีกเลี่ยงได้หากคุณใช้สำเนาโลคัลของพารามิเตอร์แรกให้ทำ + = และส่งคืน สำเนาในเครื่อง สิ่งนี้ทำให้การเพิ่มประสิทธิภาพ NRVO
Manu343726

3
ที่ผมกล่าวถึงในการสนทนา, L <= Rนอกจากนี้ยังสามารถแสดงเป็นแทน!(R < L) !(L > R)อาจบันทึกเลเยอร์พิเศษของอินไลน์ในนิพจน์ที่ยากต่อการปรับให้เหมาะสม (และเป็นวิธีที่ Boost.Operators นำไปใช้)
TemplateRex

494

กฎพื้นฐานสามข้อของผู้ประกอบการมากไปใน C ++

เมื่อมาถึงผู้ประกอบการบรรทุกเกินพิกัดใน C ++ มีสามกฎพื้นฐานที่คุณควรทำตาม เช่นเดียวกับกฎดังกล่าวทั้งหมดมีข้อยกเว้นอย่างแน่นอน บางครั้งผู้คนเบี่ยงเบนไปจากพวกเขาและผลลัพธ์ไม่ได้เป็นรหัสที่ไม่ดี แต่การเบี่ยงเบนเชิงบวกดังกล่าวมีน้อยและอยู่ห่างไกล อย่างน้อยที่สุด 99 จาก 100 การเบี่ยงเบนที่ฉันได้เห็นไม่ยุติธรรม อย่างไรก็ตามอาจมีเพียง 999 จาก 1,000 ดังนั้นคุณควรปฏิบัติตามกฎต่อไปนี้

  1. เมื่อใดก็ตามที่ความหมายของผู้ปฏิบัติงานไม่ชัดเจนและไม่มีปัญหาก็ไม่ควรมีการใช้งานมากเกินไป ให้ระบุชื่อฟังก์ชั่นที่ดีแทน
    โดยทั่วไปกฎข้อแรกและสำคัญที่สุดสำหรับการบรรทุกเกินพิกัดผู้ประกอบการที่เป็นหัวใจของมันพูดว่า:ไม่ได้ทำมัน นั่นอาจดูแปลกเพราะมีสิ่งที่ต้องรู้มากมายเกี่ยวกับการบรรทุกเกินพิกัดและบทความมากมายบทในหนังสือและข้อความอื่น ๆ ที่เกี่ยวข้องทั้งหมดนี้ แต่ถึงแม้จะมีหลักฐานที่เห็นได้ชัด แต่ก็มีเพียงไม่กี่กรณีที่ผู้ประกอบการทำงานหนักเกินความเหมาะสม. เหตุผลก็คือจริง ๆ แล้วมันยากที่จะเข้าใจความหมายที่อยู่เบื้องหลังแอปพลิเคชันของตัวดำเนินการเว้นแต่ว่าการใช้ตัวดำเนินการในโดเมนแอปพลิเคชันเป็นที่รู้จักกันดีและไม่มีปัญหา ขัดกับความเชื่อที่นิยมนี้เป็นกรณีที่แทบจะไม่เคย

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

  3. จัดเตรียมชุดปฏิบัติการที่เกี่ยวข้องทั้งหมดไว้ให้เสมอ
    ผู้ประกอบการมีความสัมพันธ์ซึ่งกันและกันและการดำเนินงานอื่น ๆ หากประเภทของคุณรองรับa + bผู้ใช้จะคาดหวังว่าจะสามารถโทรa += bได้เช่นกัน หากรองรับการเพิ่มคำนำหน้า++aพวกเขาคาดว่าa++จะทำงานได้เช่นกัน หากพวกเขาสามารถตรวจสอบว่าพวกเขาแน่นอนที่สุดจะคาดหวังว่าจะยังจะสามารถที่จะตรวจสอบว่าa < b a > bหากพวกเขาสามารถคัดลอก - สร้างประเภทของคุณพวกเขาคาดว่าการมอบหมายให้ทำงานเช่นกัน


ดำเนินการต่อเพื่อการตัดสินใจระหว่างสมาชิกและไม่เป็นสมาชิก


16
สิ่งเดียวที่ฉันรู้ซึ่งละเมิดสิ่งเหล่านี้คือboost::spiritฮ่า ๆ
Billy ONeal

66
@Billy: ตามที่บางคน, การดูถูกเหยียดหยาม+สำหรับการเรียงสตริงเป็นการละเมิด แต่ตอนนี้มันกลายเป็นแพรคซิสที่ยอมรับกันดีแล้วเพื่อให้ดูเหมือนเป็นธรรมชาติ แม้ว่าฉันจะจำคลาสสตริงที่ใช้ในบ้านได้ฉันเห็นใน 90ies ที่ใช้ไบนารี&เพื่อจุดประสงค์นี้ (หมายถึงพื้นฐานสำหรับแพรคซิสที่จัดตั้งขึ้น) แต่ใช่แล้วใส่ลงใน std lib โดยทั่วไปตั้งค่านี้ในหิน เช่นเดียวกับการใช้ในทางที่ผิด<<และ>>สำหรับ IO, BTW ทำไมการเลื่อนซ้ายไปเป็นการดำเนินการที่เห็นได้ชัด? เพราะเราทุกคนเรียนรู้เกี่ยวกับมันเมื่อเราเห็น "สวัสดีโลก!" ครั้งแรกของเรา ใบสมัคร และไม่มีเหตุผลอื่น
sbi

5
@curtguy: ถ้าคุณต้องอธิบายมันก็ไม่ชัดเจนและไม่มีปัญหาแน่นอน ในทำนองเดียวกันหากคุณต้องการพูดคุยหรือป้องกันการโอเวอร์โหลด
sbi

5
@sbi: "การตรวจสอบโดยเพื่อน" เป็นความคิดที่ดีเสมอ สำหรับฉันแล้วโอเปอเรเตอร์ที่เลือกไม่ดีนั้นไม่แตกต่างจากชื่อฟังก์ชันที่เลือกไม่ดี (ฉันเห็นหลายรายการ) ผู้ประกอบการเป็นเพียงฟังก์ชั่น ไม่มากไม่น้อย. กฎก็เหมือนกัน และเพื่อให้เข้าใจว่าแนวคิดดีหรือไม่วิธีที่ดีที่สุดคือเข้าใจว่าต้องใช้เวลานานเท่าใดจึงจะเข้าใจ (ดังนั้นการตรวจทานโดยเพื่อนจึงเป็นสิ่งจำเป็น แต่เพื่อนต้องเลือกระหว่างผู้คนที่ปราศจากความประพฤติและอคติ)
Emilio Garavaglia

5
@sbi สำหรับฉันความจริงที่ชัดเจนและเถียงไม่ได้operator==คือว่ามันควรจะเป็นความสัมพันธ์ที่เท่าเทียมกัน (IOW คุณไม่ควรใช้ NaN ที่ไม่ใช่การส่งสัญญาณ) ภาชนะบรรจุมีความสัมพันธ์ที่มีประโยชน์มากมาย ความเสมอภาคหมายถึงอะไร " aเท่ากับb" หมายความว่าaและbมีค่าทางคณิตศาสตร์เหมือนกัน แนวคิดของค่าทางคณิตศาสตร์ของ (ไม่ใช่ NaN) floatมีความชัดเจน แต่ค่าทางคณิตศาสตร์ของคอนเทนเนอร์สามารถมีคำจำกัดความที่เป็นประโยชน์มากมาย (ชนิดเรียกซ้ำ) คำจำกัดความที่แข็งแกร่งที่สุดของความเท่าเทียมกันคือ "พวกมันเป็นวัตถุเดียวกัน" และมันไร้ประโยชน์
2012

265

ไวยากรณ์ทั่วไปของโอเปอร์เรเตอร์ที่บรรทุกเกินพิกัดใน C ++

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

ผู้ให้บริการบางรายสามารถโอเวอร์โหลดใน C ++ ไม่ได้ ในบรรดาโอเปอเรเตอร์ที่ไม่สามารถโอเวอร์โหลดคือ: . :: sizeof typeid .*และโอเปอเรเตอร์เดียวใน C ++?:

ในบรรดาตัวดำเนินการที่สามารถโอเวอร์โหลดใน C ++ คือ:

  • ตัวดำเนินการทางคณิตศาสตร์: + - * / %และ+= -= *= /= %=(ทุกไบนารีมัด); + -(คำนำหน้า unary); ++ --(คำนำหน้า unary และ postfix)
  • การจัดการบิต: & | ^ << >>และ&= |= ^= <<= >>=(ทุกไบนารีมัด); ~(คำนำหน้า unary)
  • พีชคณิตแบบบูล: == != < > <= >= || &&(ไบนารีมัดทั้งหมด); !(คำนำหน้า unary)
  • การจัดการหน่วยความจำ: new new[] delete delete[]
  • ตัวดำเนินการแปลงโดยนัย
  • เรื่องจิปาถะ: = [] -> ->* , (ทุกมัดฐาน); * &(คำนำหน้าทั้งหมด unary) ()(การเรียกใช้ฟังก์ชัน, n-ary infix)

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

ใน C ++, ผู้ประกอบการมีมากเกินไปในรูปแบบของฟังก์ชั่นที่มีชื่อพิเศษ เช่นเดียวกับฟังก์ชั่นอื่น ๆ ที่ผู้ประกอบการมากเกินไปโดยทั่วไปจะสามารถนำมาใช้ไม่ว่าจะเป็นฟังก์ชั่นสมาชิกของชนิดถูกดำเนินการทางด้านซ้ายของพวกเขาหรือเป็นฟังก์ชั่นที่ไม่ใช่สมาชิก ไม่ว่าคุณจะมีอิสระในการเลือกหรือผูกพันกับการใช้งานขึ้นอยู่กับเกณฑ์หลายอย่าง 2เอกผู้ประกอบการ@3นำไปใช้กับวัตถุ x, ถูกเรียกว่าจะเป็นหรือเป็นoperator@(x) x.operator@()ผู้ประกอบการมัดไบนารี@นำไปใช้กับวัตถุxและyจะเรียกว่าไม่ว่าจะเป็นหรือเป็นoperator@(x,y) 4x.operator@(y)

ตัวดำเนินการที่นำมาใช้เป็นฟังก์ชั่นที่ไม่ใช่สมาชิกบางครั้งเป็นเพื่อนของประเภทตัวถูกดำเนินการของพวกเขา

1 คำว่า“ ผู้ใช้กำหนด” อาจทำให้เข้าใจผิดเล็กน้อย C ++ สร้างความแตกต่างระหว่างชนิดในตัวและชนิดที่ผู้ใช้กำหนด สำหรับอดีตนั้นเป็นของอย่าง int, char, และ double; ในส่วนหลังจะเป็นชนิดของคลาสคลาสสหภาพและ enum ทั้งหมดรวมถึงชนิดจากไลบรารีมาตรฐานแม้ว่าจะไม่ได้กำหนดไว้โดยผู้ใช้

2 สิ่งนี้กล่าวถึงในส่วนท้ายของคำถามที่พบบ่อยนี้

3 ตัวดำเนินการ@ไม่ถูกต้องใน C ++ ซึ่งเป็นสาเหตุที่ฉันใช้มันเป็นตัวยึดตำแหน่ง

4 ตัวดำเนินการแบบไตรภาคใน C ++ ไม่สามารถโหลดมากเกินไปและตัวดำเนินการแบบ n-ary เท่านั้นจะต้องถูกนำมาใช้เป็นฟังก์ชันสมาชิกเสมอ


ดำเนินการต่อไปสามกฎพื้นฐานของการดำเนินงานมากใน C ++


~เป็นคำนำหน้า unary ไม่ใช่ไบนารีมัด
mrkj

1
.*หายไปจากรายการของตัวดำเนินการที่ไม่สามารถโอเวอร์โหลดได้
celticminstrel

1
@Mateen ฉันต้องการใช้ตัวยึดตำแหน่งแทนตัวดำเนินการจริงเพื่อให้ชัดเจนว่านี่ไม่เกี่ยวกับตัวดำเนินการพิเศษ แต่ใช้กับพวกเขาทั้งหมด และถ้าคุณต้องการที่จะเป็นโปรแกรมเมอร์ C ++ คุณควรเรียนรู้ที่จะใส่ใจแม้กระทั่งพิมพ์เล็ก ๆ :)
sbi

1
@HR: หากคุณอ่านคู่มือนี้คุณจะรู้ว่ามีอะไรผิดปกติ โดยทั่วไปฉันแนะนำว่าคุณควรอ่านสามคำตอบแรกที่เชื่อมโยงจากคำถาม นั่นไม่ควรเกินครึ่งชั่วโมงในชีวิตของคุณและให้ความรู้พื้นฐานแก่คุณ ไวยากรณ์เฉพาะของโอเปอเรเตอร์ที่คุณสามารถค้นหาได้ในภายหลัง ปัญหาเฉพาะของคุณแนะนำให้คุณลองโอเวอร์โหลดoperator+()ในฐานะสมาชิกฟังก์ชั่น ดูที่นี่
sbi

1
@sbi: ฉันได้อ่านสามโพสต์แรกแล้วและขอบคุณที่ทำให้พวกเขา :) ฉันจะลองแก้ไขปัญหามิฉะนั้นฉันคิดว่ามันจะดีกว่าถ้าถามคำถามแยกต่างหาก ขอบคุณอีกครั้งที่ทำให้ชีวิตง่ายขึ้นสำหรับเรา! : D
Hosein Rahnama

251

การตัดสินใจระหว่างสมาชิกและไม่ใช่สมาชิก

ตัวดำเนินการไบนารี=(การมอบหมาย), [](การสมัครสมาชิกอาเรย์), ->(การเข้าถึงสมาชิก) รวมถึงตัวดำเนินการ n-ary ()(การเรียกใช้ฟังก์ชัน) ต้องดำเนินการในฐานะสมาชิกฟังก์ชันเสมอเนื่องจากไวยากรณ์ของภาษาต้องการให้พวกเขา

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

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

  1. ถ้าเป็นตัวดำเนินการ unary ให้ใช้มันเป็นฟังก์ชันสมาชิก
  2. หากตัวดำเนินการไบนารีปฏิบัติต่อทั้งสองตัวถูกดำเนินการเท่ากัน (มันจะไม่เปลี่ยนแปลง) ให้ใช้ตัวดำเนินการนี้เป็นฟังก์ชันที่ไม่ใช่สมาชิก
  3. หากตัวดำเนินการไบนารีไม่ปฏิบัติกับตัวถูกดำเนินการทั้งสองเท่า ๆ กัน (โดยปกติจะเปลี่ยนตัวถูกดำเนินการด้านซ้าย) อาจเป็นประโยชน์ในการทำให้ฟังก์ชันสมาชิกเป็นประเภทของตัวถูกดำเนินการด้านซ้ายหากมีการเข้าถึงส่วนส่วนตัวของตัวถูกดำเนินการ

แน่นอนเช่นเดียวกับกฎของหัวแม่มือทั้งหมดมีข้อยกเว้น หากคุณมีประเภท

enum Month {Jan, Feb, ..., Nov, Dec}

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

(อย่างไรก็ตามถ้าคุณสร้างข้อยกเว้นอย่าลืมปัญหาของconst-ness สำหรับตัวถูกดำเนินการนั้นสำหรับฟังก์ชั่นสมาชิกจะกลายเป็นthisอาร์กิวเมนต์โดยปริยายหากผู้ประกอบการที่ไม่ใช่สมาชิกฟังก์ชั่นจะใช้อาร์กิวเมนต์ซ้ายสุดเป็นconstข้อมูลอ้างอิง ผู้ประกอบการเช่นเดียวกับฟังก์ชั่นสมาชิกจะต้องมีconstที่สิ้นสุดเพื่อให้อ้างอิง.)*thisconst


ดำเนินการต่อไปผู้ประกอบการทั่วไปในการโอเวอร์โหลด


9
รายการของ Herb Sutter ใน C ++ ที่มีประสิทธิภาพ (หรือเป็นมาตรฐานการเข้ารหัส C ++ หรือไม่) กล่าวว่าหนึ่งควรชอบฟังก์ชั่นที่ไม่ใช่สมาชิกที่ไม่ใช่สมาชิกฟังก์ชั่นสมาชิกเพื่อเพิ่มการห่อหุ้มของชั้นเรียน IMHO เหตุผลของการห่อหุ้มนั้นมีความสำคัญกว่ากฎของหัวแม่มือ แต่จะไม่ลดค่าคุณภาพของกฎของหัวแม่มือของคุณ
paercebal

8
@ paercebal: C ++ ที่มีประสิทธิภาพคือโดย Meyers, C ++ มาตรฐานการเข้ารหัสโดย Sutter คุณหมายถึงอันไหน อย่างไรก็ตามฉันไม่ชอบความคิดพูดoperator+=()ไม่ได้เป็นสมาชิก มันจะต้องเปลี่ยนตัวถูกดำเนินการมือซ้ายของมันดังนั้นโดยนิยามแล้วมันต้องขุดลึกเข้าไปในอวัยวะภายในของมัน คุณจะได้อะไรจากการไม่ได้เป็นสมาชิก
sbi

9
@sbi: รายการ 44 ในมาตรฐานการเข้ารหัส C ++ (ซัทเทอร์) แน่นอนว่าการเขียนฟังก์ชันที่ไม่ใช่สมาชิกที่ไม่เป็นสมาชิกนั้นจะใช้งานได้เฉพาะในกรณีที่คุณสามารถเขียนฟังก์ชันนี้โดยใช้เพียงส่วนต่อประสานสาธารณะของชั้นเรียนเท่านั้น หากคุณไม่สามารถ (หรือสามารถ แต่จะขัดขวางประสิทธิภาพการทำงานที่ไม่ดี) คุณต้องกำหนดให้เป็นสมาชิกหรือเพื่อน
Matthieu M.

3
@sbi: โอ๊ะ, มีประสิทธิภาพ, ยอดเยี่ยม ... ไม่น่าแปลกใจที่ฉันผสมชื่อเข้าด้วยกัน อย่างไรก็ตามการได้รับคือการ จำกัด จำนวนฟังก์ชั่นที่สามารถเข้าถึงข้อมูลส่วนตัว / การป้องกันของวัตถุได้มากที่สุด ด้วยวิธีนี้คุณเพิ่มการห่อหุ้มในชั้นเรียนของคุณทำให้การบำรุงรักษา / การทดสอบ / การวิวัฒนาการง่ายขึ้น
paercebal

12
@sbi: ตัวอย่างหนึ่ง สมมติว่าคุณกำลังเข้ารหัสระดับสตริงกับทั้งoperator +=และappendวิธีการ appendเป็นวิธีการที่สมบูรณ์มากขึ้นเพราะคุณสามารถผนวกย่อยของพารามิเตอร์จากดัชนี i เพื่อดัชนี n -1: append(string, start, end)มันดูเหมือนว่าตรรกะที่จะมีการ+=ผนวกกับการโทรและstart = 0 end = string.sizeในขณะนั้นการผนวกอาจเป็นวิธีการของสมาชิก แต่operator +=ไม่จำเป็นต้องเป็นสมาชิกและการทำให้เป็นผู้ที่ไม่ได้เป็นสมาชิกจะลดปริมาณการเล่นรหัสกับเครื่องใน String ดังนั้นจึงเป็นสิ่งที่ดี .... ^ _ ^ ...
paercebal

165

ผู้ให้บริการการแปลง (หรือที่เรียกว่าการแปลงที่ผู้ใช้กำหนด)

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

ตัวดำเนินการแปลงโดยนัย (C ++ 98 / C ++ 03 และ C ++ 11)

ตัวดำเนินการแปลงโดยนัยอนุญาตให้คอมไพเลอร์แปลงโดยปริยาย (เช่นการแปลงระหว่างintและlong) ค่าของประเภทที่ผู้ใช้กำหนดเป็นประเภทอื่น

ต่อไปนี้เป็นคลาสที่เรียบง่ายพร้อมตัวดำเนินการแปลงโดยนัย:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

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

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

ในตอนแรกดูเหมือนว่าจะมีประโยชน์มาก แต่ปัญหาของเรื่องนี้ก็คือการแปลงโดยนัยจะเริ่มขึ้นเมื่อไม่ได้คาดหวัง ในรหัสต่อไปนี้void f(const char*)จะถูกเรียกเพราะmy_string()ไม่ใช่lvalueดังนั้นรหัสแรกไม่ตรงกัน:

void f(my_string&);
void f(const char*);

f(my_string());

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

ตัวดำเนินการแปลงอย่างชัดเจน (C ++ 11)

ซึ่งแตกต่างจากผู้ให้บริการการแปลงโดยนัยผู้ให้บริการการแปลงที่ชัดเจนจะไม่เปิดเมื่อคุณไม่คาดหวัง ต่อไปนี้เป็นคลาสธรรมดาที่มีตัวดำเนินการแปลงอย่างชัดเจน:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

explicitแจ้งให้ทราบล่วงหน้า ตอนนี้เมื่อคุณพยายามเรียกใช้โค้ดที่ไม่คาดคิดจากโอเปอเรเตอร์การแปลงโดยนัยคุณจะได้รับข้อผิดพลาดของคอมไพเลอร์:

prog.cpp: ในฟังก์ชัน 'int main ()':
prog.cpp: 15: 18: ข้อผิดพลาด: ไม่มีฟังก์ชันที่ตรงกันสำหรับการเรียกไปที่ 'f (my_string)'
prog.cpp: 15: 18: หมายเหตุ: ผู้สมัครคือ:
prog.cpp: 11: 10: หมายเหตุ: void f (my_string &)
prog.cpp: 11: 10: หมายเหตุ: ไม่รู้จักการแปลงสำหรับอาร์กิวเมนต์ 1 จาก 'my_string' ถึง 'my_string &'
prog.cpp: 12: 10: หมายเหตุ: เป็นโมฆะ f (const char *)
prog.cpp: 12: 10: หมายเหตุ: ไม่รู้จักการแปลงสำหรับอาร์กิวเมนต์ 1 จาก 'my_string' เป็น 'const char *'

ในการเรียกใช้โอเปอเรเตอร์การส่งแบบชัดแจ้งคุณต้องใช้งานstatic_castการหล่อแบบ C หรือการสร้างแบบการสร้าง (เช่นT(value))

แต่มีหนึ่งข้อยกเว้นนี้: boolคอมไพเลอร์ได้รับอนุญาตให้โดยปริยายแปลง นอกจากนี้คอมไพเลอร์ไม่ได้รับอนุญาตให้ทำการแปลงโดยนัยอีกครั้งหลังจากแปลงเป็นbool(คอมไพเลอร์ได้รับอนุญาตให้ทำการแปลง 2 ครั้งต่อครั้ง แต่แปลงได้เพียง 1 ครั้งที่ผู้ใช้กำหนดสูงสุด)

เพราะคอมไพเลอร์จะไม่โยน "ที่ผ่านมา" boolผู้ประกอบการแปลงอย่างชัดเจนในขณะนี้ลบความจำเป็นในการที่สำนวนปลอดภัย Bool ตัวอย่างเช่นพอยน์เตอร์อัจฉริยะก่อน C ++ 11 ใช้สำนวน Safe Bool เพื่อป้องกันการแปลงเป็นประเภทอินทิกรัล ใน C ++ 11 สมาร์ทพอยน์เตอร์ใช้ตัวดำเนินการอย่างชัดเจนแทนเนื่องจากคอมไพเลอร์ไม่ได้รับอนุญาตให้แปลงเป็นชนิดอินทิกรัลโดยปริยายหลังจากแปลงชนิดเป็นบูลอย่างชัดเจน

ดำเนินการต่อไปมากไปnewdeleteและ


148

การบรรทุกเกินพิกัดnewและdelete

หมายเหตุ:นี่จะเกี่ยวข้องกับไวยากรณ์ของการโอเวอร์โหลดnewและdeleteไม่ใช่กับการใช้งานของตัวดำเนินการโอเวอร์โหลดดังกล่าว ฉันคิดว่าความหมายของการบรรทุกเกินพิกัดnewและdeleteสมควรได้รับคำถามที่พบบ่อยของตัวเองภายในหัวข้อของการบรรทุกเกินพิกัดที่ฉันไม่สามารถทำมันได้อย่างยุติธรรม

ข้อมูลพื้นฐานเกี่ยวกับ

ใน C ++ เมื่อคุณเขียนนิพจน์ใหม่เหมือนnew T(arg)สองสิ่งเกิดขึ้นเมื่อนิพจน์นี้ถูกประเมิน: อันดับแรกoperator newถูกเรียกใช้เพื่อรับหน่วยความจำดิบจากนั้นตัวสร้างที่เหมาะสมของTจะถูกเรียกใช้เพื่อเปลี่ยนหน่วยความจำดิบนี้เป็นวัตถุที่ถูกต้อง ในทำนองเดียวกันเมื่อคุณลบวัตถุแรก destructor operator deleteของมันจะถูกเรียกว่าหน่วยความจำและจากนั้นจะถูกส่งกลับไปยัง
C ++ ช่วยให้คุณปรับแต่งการดำเนินการทั้งสองนี้ได้: การจัดการหน่วยความจำและการสร้าง / ทำลายวัตถุที่หน่วยความจำที่จัดสรร หลังจะทำโดยการเขียนตัวสร้างและ destructors สำหรับชั้นเรียน จัดการหน่วยความจำปรับจูนจะทำโดยการเขียนของคุณเองและoperator newoperator delete

ครั้งแรกของกฎพื้นฐานของผู้ประกอบการมากไป - ไม่ได้ทำมัน - ใช้เฉพาะกับการบรรทุกเกินพิกัดและnew deleteเกือบจะเป็นเหตุผลเดียวที่ทำให้ผู้ประกอบการเหล่านี้มีปัญหาประสิทธิภาพการทำงานและข้อ จำกัด ของหน่วยความจำและในหลาย ๆ กรณีการกระทำอื่น ๆ เช่นการเปลี่ยนแปลงอัลกอริธึมที่ใช้จะทำให้อัตราส่วนค่าใช้จ่ายต่อกำไรสูงกว่าการพยายามปรับการจัดการหน่วยความจำ

ไลบรารีมาตรฐาน C ++ มาพร้อมกับชุดของตัวกำหนดล่วงหน้าnewและdeleteตัวดำเนินการ สิ่งที่สำคัญที่สุดคือ:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

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

การวาง new

C ++ อนุญาตให้ตัวดำเนินการใหม่และตัวลบสามารถรับอาร์กิวเมนต์เพิ่มเติมได้
ตำแหน่งใหม่ที่เรียกว่าช่วยให้คุณสร้างวัตถุที่อยู่ที่แน่นอนซึ่งถูกส่งผ่านไปยัง:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

ไลบรารีมาตรฐานมาพร้อมกับโอเวอร์โหลดที่เหมาะสมของตัวดำเนินการใหม่และตัวลบสำหรับสิ่งนี้:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

โปรดทราบว่าในโค้ดตัวอย่างสำหรับการวางตำแหน่งใหม่ที่ระบุไว้ด้านบนoperator deleteจะไม่ถูกเรียกเว้นแต่ว่านวกรรมิกของ X จะส่งข้อยกเว้น

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

คลาสเฉพาะใหม่และลบ

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

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

โอเวอร์โหลดดังนั้นใหม่และลบการทำงานเหมือนฟังก์ชั่นสมาชิกคงที่ สำหรับวัตถุของmy_classการโต้แย้งจะเป็นstd::size_t sizeof(my_class)อย่างไรก็ตามตัวดำเนินการเหล่านี้จะถูกเรียกใช้สำหรับออบเจ็กต์ที่จัดสรรแบบไดนามิกของคลาสที่ได้รับมาซึ่งในกรณีนี้มันอาจจะมากกว่านั้น

ใหม่ระดับโลกและลบ

หากต้องการโอเวอร์โหลดใหม่และลบทั่วโลกเพียงแค่แทนที่โอเปอเรเตอร์ที่กำหนดไว้ล่วงหน้าของไลบรารีมาตรฐานด้วยของเราเอง อย่างไรก็ตามสิ่งนี้ไม่จำเป็นต้องทำ


11
ฉันยังไม่เห็นด้วยว่าการแทนที่ผู้ให้บริการเครือข่ายทั่วโลกนั้นใหม่และการลบมักจะเป็นเรื่องของประสิทธิภาพการทำงาน: ในทางกลับกันมักจะเป็นการติดตามบั๊ก
Yttrill

1
คุณควรทราบด้วยว่าหากคุณใช้โอเปอเรเตอร์ใหม่ที่มีการโอเวอร์โหลดคุณจำเป็นต้องระบุโอเปอเรเตอร์การลบพร้อมอาร์กิวเมนต์ที่ตรงกัน คุณบอกว่าในส่วนเกี่ยวกับโลกใหม่ / ลบที่มันไม่น่าสนใจ
Yttrill

13
@Yttrill คุณกำลังสับสนสิ่งต่าง ๆ ความหมายได้รับมากเกินไป "โอเปอเรเตอร์เกินกำลัง" หมายถึงอะไรคือความหมายนั้นมีมากเกินไป ไม่ได้หมายความว่าฟังก์ชั่นแท้จริงมีการโหลดมากเกินไปและโดยเฉพาะอย่างยิ่งผู้ปฏิบัติงานใหม่จะไม่ทำงานเกินมาตรฐานเวอร์ชัน @sbi ไม่อ้างสิทธิ์ตรงกันข้าม เป็นเรื่องปกติที่จะเรียกมันว่า "การบรรทุกเกินพิกัดใหม่" มากพอ ๆ กับการพูดว่า
Johannes Schaub - litb

1
@sbi: เห็น (หรือดีกว่าการเชื่อมโยงไป) gotw.ca/publications/mill15.htm เป็นการฝึกฝนที่ดีสำหรับคนที่บางครั้งใช้nothrowใหม่
Alexandre C.

1
"หากคุณไม่ได้ลบโอเปอเรเตอร์ที่ตรงกันออกมาค่าปริยายจะเรียกว่า" -> จริง ๆ แล้วถ้าคุณเพิ่มอาร์กิวเมนต์ใด ๆ และไม่สร้างการลบที่ตรงกันจะไม่มีการลบตัวดำเนินการใด ๆ เลยและคุณมีหน่วยความจำรั่วไหล (15.2.2 การจัดเก็บที่ถูกครอบครองโดยวัตถุที่ถูก deallocated เฉพาะในกรณีที่เหมาะสม ... ผู้ประกอบการลบพบ)
dascandy

46

เหตุใดจึงไม่สามารถoperator<<ใช้ฟังก์ชั่นการสตรีมวัตถุไปยังstd::coutหรือไฟล์เป็นฟังก์ชันสมาชิกได้

สมมติว่าคุณมี:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

ระบุว่าคุณไม่สามารถใช้:

Foo f = {10, 20.0};
std::cout << f;

เนื่องจากoperator<<โอเวอร์โหลดเป็นฟังก์ชันสมาชิกของFooLHS ของโอเปอเรเตอร์จะต้องเป็นFooวัตถุ ซึ่งหมายความว่าคุณจะต้องใช้:

Foo f = {10, 20.0};
f << std::cout

ซึ่งไม่ใช้งานง่ายมาก

หากคุณกำหนดว่าเป็นฟังก์ชั่นที่ไม่ใช่สมาชิก

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

คุณจะสามารถใช้:

Foo f = {10, 20.0};
std::cout << f;

ซึ่งใช้งานง่ายมาก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.