กลยุทธ์ C + DRY แห้ง


14

สำหรับการหลีกเลี่ยงการทำซ้ำที่ไม่เกี่ยวข้องกับ C ++ const มีกรณีที่ const_cast จะทำงานได้ แต่ฟังก์ชั่น const ส่วนตัวที่ส่งกลับไม่ใช่ const จะไม่?

ในรายการC ++ ของ Scott Meyers ที่มีประสิทธิผล 3 เขาแนะนำว่า const_cast ที่รวมกับการส่งสัญญาณคงที่นั้นเป็นวิธีที่มีประสิทธิภาพและปลอดภัยในการหลีกเลี่ยงรหัสซ้ำเช่น

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

เมเยอร์สอธิบายต่อไปว่าการใช้ฟังก์ชัน const เรียกฟังก์ชันที่ไม่ใช่ const นั้นเป็นอันตราย

รหัสด้านล่างคือตัวอย่างที่แสดงให้เห็น:

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

กลยุทธ์ const_cast อย่างใดอย่างหนึ่งของการหลีกเลี่ยงการทำสำเนารหัสถือเป็นแนวปฏิบัติที่ดีหรือไม่? คุณต้องการกลยุทธ์กลยุทธ์ส่วนตัวแทนไหม มีกรณีที่ const_cast จะทำงาน แต่วิธีส่วนตัวจะไม่? มีตัวเลือกอื่น ๆ (นอกเหนือจากการทำซ้ำ)?

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

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};

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

@SebastianRedl ฉันยอมรับว่าการทำซ้ำจะดีกว่าหากเพิ่งคืนสมาชิก โปรดจินตนาการว่ามันมีความซับซ้อนมากกว่าเช่นแทนที่จะส่งคืน mConstLongLived เราสามารถเรียกใช้ฟังก์ชันบน mConstLongLived ที่ส่งคืนการอ้างอิง const ซึ่งจะใช้เรียกฟังก์ชันอื่นที่ส่งคืนการอ้างอิง const ที่เราไม่ได้เป็นเจ้าของและมีสิทธิ์เข้าถึงเท่านั้น รุ่น const ของ ฉันหวังว่าประเด็นนี้จะชัดเจนว่า const_cast สามารถลบ const ออกจากบางสิ่งที่เราไม่สามารถเข้าถึงแบบไม่ใช่ const ได้
JDiMatteo

4
ทั้งหมดนี้ดูเหมือนว่าไร้สาระกับตัวอย่างง่ายๆ แต่การทำซ้ำ const ที่เกี่ยวข้องเกิดขึ้นในรหัสจริงข้อผิดพลาดของคอมไพเลอร์ const มีประโยชน์ในทางปฏิบัติ (มักจะจับข้อผิดพลาดโง่) และฉันประหลาดใจว่าโซลูชัน C ++ ที่มีประสิทธิภาพนั้นแปลก และดูเหมือนว่าจะมีการปลดเปลื้องผิดพลาด สมาชิก const ส่วนตัวที่กลับมาไม่ใช่สมาชิกดูเหมือนว่าจะดีกว่านักแสดงสองคนอย่างชัดเจนและฉันต้องการทราบว่ามีบางสิ่งที่ฉันขาดหายไปหรือไม่
JDiMatteo

คำตอบ:


8

เมื่อใช้งานฟังก์ชั่นสมาชิก const และไม่ใช่สมาชิกที่แตกต่างกันโดยเฉพาะว่าการอ้างอิง PTR / ส่งคืนเป็น const กลยุทธ์ DRY ที่ดีที่สุดคือ:

  1. หากเขียน accessor ให้พิจารณาว่าคุณต้องการ accessor จริงๆหรือไม่ดูคำตอบของ cmasterและhttp://c2.com/cgi/wiki?AccessorsAreEvil
  2. เพียงแค่ทำซ้ำรหัสถ้ามันเล็กน้อย (เช่นเพียงแค่ส่งคืนสมาชิก)
  3. อย่าใช้ const_cast เพื่อหลีกเลี่ยงการทำซ้ำที่เกี่ยวข้องกับ const
  4. เพื่อหลีกเลี่ยงความซ้ำซ้อนที่ไม่น่ารำคาญให้ใช้ฟังก์ชั่น const ส่วนตัวที่ส่งกลับค่าไม่ใช่คอนซึ่งเรียกฟังก์ชันสาธารณะทั้งแบบ const และ non-const

เช่น

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Let 's นี้เรียกฟังก์ชั่น const ส่วนตัวกลับมาในรูปแบบที่ไม่

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


ข้อโต้แย้งของคุณค่อนข้างน่าเชื่อถือ แต่ฉันค่อนข้างงงว่าคุณจะได้รับการอ้างอิงที่ไม่ใช่ค่าคงที่จากconstอินสแตนซ์ได้อย่างไร (ยกเว้นกรณีที่มีการอ้างอิงถึงสิ่งที่ประกาศmutableหรือเว้นแต่คุณจะจ้างconst_castแต่ในทั้งสองกรณีไม่มี probkem เริ่มต้นด้วย ) นอกจากนี้ฉันไม่สามารถหาอะไรใน "ฟังก์ชั่น const ส่วนตัวกลับมาไม่ใช่รูปแบบ const" (ถ้ามันตั้งใจจะเป็นเรื่องตลกที่จะเรียกมันว่ารูปแบบ .... มันไม่ตลก;)
idclev 463035818

1
นี่คือตัวอย่างการรวบรวมตามออกรหัสในคำถาม: ideone.com/wBE1GB ขออภัยฉันไม่ได้คิดว่ามันเป็นเรื่องตลก แต่ฉันตั้งใจจะให้ชื่อที่นี่ (ในกรณีที่ไม่น่าจะสมควรได้รับชื่อ) และฉันได้อัปเดตข้อความในคำตอบเพื่อพยายามทำให้ชัดเจนยิ่งขึ้น ไม่กี่ปีที่ผ่านมาตั้งแต่ฉันเขียนเรื่องนี้และฉันจำไม่ได้ว่าทำไมฉันถึงคิดว่าตัวอย่างที่ส่งผ่านการอ้างอิงในตัวสร้างมีความเกี่ยวข้อง
JDiMatteo

ขอบคุณสำหรับตัวอย่างฉันไม่มีเวลา แต่ฉันจะกลับมาอีกแน่นอน Fwiw นี่คือคำตอบที่นำเสนอวิธีการเดียวกันและในความคิดเห็นปัญหาที่คล้ายกันได้รับการชี้ให้เห็น: stackoverflow.com/a/124209/4117728
idclev 463035818

1

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

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

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


ดูเหมือนขึ้นสำหรับการอภิปรายหรือไม่ accessors เป็นสิ่งที่ดีเช่นดูการอภิปรายที่c2.com/cgi/wiki?AccessorsAreEvil ในทางปฏิบัติโดยไม่คำนึงถึงสิ่งที่คุณคิดว่า accessors ฐานรหัสขนาดใหญ่มักจะใช้พวกเขาและถ้าพวกเขาใช้พวกเขามันจะดีกว่าที่จะยึดมั่นในหลักการ DRY ดังนั้นฉันคิดว่าคำถามสมควรได้รับคำตอบมากกว่าที่คุณไม่ควรถาม
JDiMatteo

1
เป็นคำถามที่ควรค่าแก่การถาม :-) และฉันจะไม่ปฏิเสธด้วยซ้ำว่าคุณต้องการอุปกรณ์เสริมเป็นครั้งคราว ฉันแค่บอกว่าสไตล์การเขียนโปรแกรมที่ไม่ได้ขึ้นอยู่กับ accessors ช่วยลดปัญหาอย่างมาก มันไม่ได้แก้ปัญหาทั้งหมด แต่อย่างน้อยก็ดีพอสำหรับฉัน
cmaster - คืนสถานะโมนิก้า
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.