Strategi C ++ KERING Const

14

Untuk menghindari duplikasi yang terkait dengan konstanta C ++ non-sepele, apakah ada kasus di mana const_cast akan bekerja tetapi fungsi const pribadi yang mengembalikan non-const tidak?

Dalam Scott Meyers ' C ++ 3 item Efektif , ia menyarankan bahwa const_cast dikombinasikan dengan pemain statis bisa menjadi cara yang efektif dan aman untuk menghindari kode duplikat, misalnya

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

Meyers melanjutkan untuk menjelaskan bahwa memiliki fungsi panggilan const fungsi non-const berbahaya.

Kode di bawah ini adalah contoh tandingan yang menunjukkan:

  • bertentangan dengan saran Meyers, kadang-kadang const_cast yang dikombinasikan dengan pemain statis berbahaya
  • kadang-kadang memiliki fungsi panggilan const non-const kurang berbahaya
  • terkadang kedua cara menggunakan const_cast menyembunyikan kesalahan kompilator yang berguna
  • menghindari const_cast dan memiliki anggota pribadi konst tambahan yang mengembalikan non-const adalah pilihan lain

Apakah salah satu dari strategi const_cast menghindari duplikasi kode dianggap praktik yang baik? Apakah Anda lebih suka strategi metode pribadi saja? Apakah ada kasus di mana const_cast akan berfungsi tetapi metode pribadi tidak? Apakah ada opsi lain (selain duplikasi)?

Kekhawatiran saya dengan strategi const_cast adalah bahwa bahkan jika kode itu benar ketika ditulis, nanti selama pemeliharaan kode bisa menjadi salah dan const_cast akan menyembunyikan kesalahan kompiler yang berguna. Sepertinya fungsi pribadi umum umumnya lebih aman.

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 {};
JDiMatteo
sumber
Seorang pengambil yang hanya mengembalikan anggota lebih pendek dari yang melempar dan memanggil versi lain dari dirinya sendiri. Trik ini dimaksudkan untuk fungsi yang lebih rumit di mana keuntungan deduplikasi melebihi risiko casting.
Sebastian Redl
@SebastianRedl Saya setuju bahwa duplikasi akan lebih baik jika hanya mengembalikan anggota. Harap bayangkan bahwa ini lebih rumit, misal alih-alih mengembalikan mConstLongLived, kita dapat memanggil fungsi pada mConstLongLived yang mengembalikan referensi const yang kemudian digunakan untuk memanggil fungsi lain yang mengembalikan referensi const yang tidak kita miliki dan hanya memiliki akses ke versi const dari. Saya harap intinya jelas bahwa const_cast dapat menghapus const dari sesuatu yang kita tidak akan memiliki akses non-const.
JDiMatteo
4
Ini semua tampaknya agak konyol dengan contoh-contoh sederhana, tetapi duplikasi const terkait muncul dalam kode nyata, kesalahan comp compiler berguna dalam prakteknya (sering untuk menangkap kesalahan bodoh), dan saya terkejut bahwa solusi "efektif C ++" yang diusulkan adalah aneh dan sepasang gips yang cenderung error. Seorang anggota const pribadi yang mengembalikan non-const tampaknya jelas lebih unggul daripada pemain ganda, dan saya ingin tahu apakah ada sesuatu yang saya lewatkan.
JDiMatteo

Jawaban:

8

Ketika menerapkan fungsi anggota const dan non-const yang hanya berbeda dengan apakah ptr / referensi yang dikembalikan adalah const, strategi KERING terbaik adalah untuk:

  1. jika menulis accessor, pertimbangkan apakah Anda benar-benar membutuhkan accessor, lihat jawaban cmaster dan http://c2.com/cgi/wiki?AccessorsAreEvil
  2. cukup duplikat kode jika sepele (mis. hanya mengembalikan anggota)
  3. tidak pernah menggunakan const_cast untuk menghindari duplikasi terkait const
  4. untuk menghindari duplikasi non-sepele, gunakan fungsi const pribadi yang mengembalikan non-const yang memanggil fungsi publik const dan non-const

misalnya

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

Mari kita sebut ini fungsi const pribadi yang mengembalikan pola non-const .

Ini adalah strategi terbaik untuk menghindari duplikasi dengan cara yang lurus ke depan sambil tetap memungkinkan kompiler untuk melakukan pemeriksaan yang berpotensi bermanfaat dan melaporkan pesan kesalahan terkait const.

JDiMatteo
sumber
argumen Anda agak meyakinkan, tetapi saya agak bingung bagaimana Anda bisa mendapatkan referensi non-const untuk sesuatu dari sebuah constinstance (kecuali referensi tersebut untuk sesuatu yang dinyatakan mutable, atau kecuali Anda menggunakan const_casttetapi dalam kedua kasus tidak ada masalah untuk memulai dengan ). Juga saya tidak bisa menemukan apa pun di "fungsi const pribadi yang mengembalikan pola non-const" (jika itu dimaksudkan untuk menjadi lelucon untuk menyebutnya pola .... itu tidak lucu;)
idclev 463035818
1
Berikut ini adalah contoh kompilasi berdasarkan kode dalam pertanyaan: ideone.com/wBE1GB . Maaf, saya tidak bermaksud menganggapnya sebagai lelucon, tetapi saya memang bermaksud memberikannya nama di sini (dalam hal yang tidak mungkin itu pantas disebut), dan saya memperbarui kata-kata dalam jawaban untuk mencoba membuatnya lebih jelas. Sudah beberapa tahun sejak saya menulis ini, dan saya tidak ingat mengapa saya pikir contoh memberikan referensi dalam konstruktor itu relevan.
JDiMatteo
Terima kasih untuk contohnya, saya tidak punya waktu sekarang, tetapi saya pasti akan kembali ke sana. Fwiw di sini adalah jawaban yang menyajikan pendekatan yang sama dan dalam komentar masalah serupa telah ditunjukkan: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Ya, Anda benar: Banyak program C ++ yang mencoba koreksi benar melanggar prinsip KERING, dan bahkan anggota pribadi yang mengembalikan non-const sedikit terlalu rumit untuk kenyamanan.

Namun, Anda melewatkan satu pengamatan: duplikasi kode karena kebenaran konst hanya pernah menjadi masalah jika Anda memberikan akses kode lain ke anggota data Anda. Ini sendiri merupakan pelanggaran enkapsulasi. Umumnya, duplikasi kode semacam ini kebanyakan terjadi pada pengakses yang sederhana (setelah semua, Anda memberikan akses ke anggota yang sudah ada, nilai pengembalian umumnya bukan hasil perhitungan).

Pengalaman saya adalah bahwa abstraksi yang baik cenderung tidak menyertakan pengakses. Akibatnya, saya sebagian besar menghindari masalah ini dengan mendefinisikan fungsi anggota yang benar-benar melakukan sesuatu, daripada hanya menyediakan akses ke anggota data; Saya mencoba memodelkan perilaku alih-alih data. Tujuan utama saya dalam hal ini adalah untuk benar-benar mendapatkan abstraksi dari kedua kelas saya dan fungsi masing-masing anggota, alih-alih hanya menggunakan objek saya sebagai wadah data. Tetapi gaya ini juga cukup berhasil dalam menghindari berton-ton aksesor satu baris konstan const / non-const yang begitu umum dalam kebanyakan kode.

cmaster - mengembalikan monica
sumber
Sepertinya diperdebatkan apakah accessors baik atau tidak, mis. Lihat diskusi di c2.com/cgi/wiki?AccessorsAreEvil . Dalam praktiknya, terlepas dari apa yang Anda pikirkan tentang accessors, basis kode besar sering menggunakannya, dan jika mereka menggunakannya, akan lebih baik untuk mematuhi prinsip KERING. Jadi saya pikir pertanyaannya lebih pantas dijawab daripada Anda tidak seharusnya menanyakannya.
JDiMatteo
1
Ini pasti pertanyaan yang patut ditanyakan :-) Dan saya bahkan tidak akan menyangkal bahwa Anda memerlukan aksesor dari waktu ke waktu. Saya hanya mengatakan bahwa gaya pemrograman yang tidak didasarkan pada aksesor sangat mengurangi masalah. Itu tidak menyelesaikan masalah sama sekali, tetapi setidaknya cukup baik untuk saya.
cmaster - mengembalikan monica