Bagaimana cara menghapus duplikasi kode antara fungsi anggota const dan non-const yang serupa?

242

Katakanlah saya memiliki yang berikut di class Xmana saya ingin mengembalikan akses ke anggota internal:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

Dua fungsi anggota X::Z()dan X::Z() constmemiliki kode yang identik di dalam kawat gigi. Ini adalah kode duplikat dan dapat menyebabkan masalah pemeliharaan untuk fungsi yang panjang dengan logika yang kompleks .

Apakah ada cara untuk menghindari duplikasi kode ini?

Kevin
sumber
Dalam contoh ini saya akan mengembalikan nilai dalam kasus const sehingga Anda tidak dapat melakukan refactoring di bawah ini. int Z () const {return z; }
Matt Price
1
Untuk tipe fundamental, Anda benar sekali! Contoh pertama saya tidak begitu baik. Katakanlah sebaliknya kita mengembalikan beberapa instance kelas sebagai gantinya. (Saya memperbarui pertanyaan untuk mencerminkan ini.)
Kevin

Jawaban:

189

Untuk penjelasan terperinci, silakan lihat tajuk "Hindari Duplikasi dalam constdan constFungsi Non- Anggota," pada halaman. 23, dalam Butir 3 "Gunakan constjika memungkinkan," dalam C ++ Efektif , 3d ed oleh Scott Meyers, ISBN-13: 9780321334879.

teks alternatif

Inilah solusi Meyers (disederhanakan):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

Dua gips dan pemanggilan fungsi mungkin jelek tapi itu benar. Meyers memiliki penjelasan menyeluruh mengapa.

jwfearn
sumber
45
Tidak ada yang pernah dipecat karena mengikuti Scott Meyers :-)
Steve Jessop
11
witkamp benar bahwa secara umum buruk menggunakan const_cast. Ini adalah kasus khusus di mana tidak, seperti yang dijelaskan Meyers. @ Adam: ROM => const baik-baik saja. const == ROM jelas tidak masuk akal karena siapa pun dapat memberikan non-const ke const mau tak mau: itu setara dengan hanya memilih untuk tidak mengubah sesuatu.
Steve Jessop
44
Secara umum saya akan menyarankan menggunakan const_cast daripada static_cast untuk menambah const karena mencegah Anda mengubah jenis secara tidak sengaja.
Greg Rogers
6
@HelloGoodbye: Saya pikir Meyers mengasumsikan jumlah sedikit kecerdasan dari desainer antarmuka kelas. Jika get()constmengembalikan sesuatu yang didefinisikan sebagai objek const, maka seharusnya tidak ada versi non-const get()sama sekali. Sebenarnya pemikiran saya tentang hal ini telah berubah dari waktu ke waktu: solusi template adalah satu-satunya cara untuk menghindari duplikasi dan mendapatkan comp-check const-correctness, jadi secara pribadi saya tidak akan lagi menggunakan const_castuntuk menghindari duplikasi kode, saya akan memilih antara menempatkan kode yang dibohongi ke dalam templat fungsi atau membiarkannya ditiru.
Steve Jessop
7
Dua templat berikut sangat membantu keterbacaan solusi ini: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }dan template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Maka Anda dapat melakukan:return variable(constant(*this).get());
Casey Rodarmor
64

Ya, mungkin untuk menghindari duplikasi kode. Anda perlu menggunakan fungsi anggota konst untuk memiliki logika dan meminta fungsi anggota non-konst untuk memanggil fungsi anggota konst dan melemparkan kembali nilai kembali ke referensi non-const (atau pointer jika fungsi mengembalikan pointer):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.z(index) );
   }
 #endif
};

CATATAN: Adalah penting bahwa Anda TIDAK menempatkan logika dalam fungsi non-const dan meminta fungsi const memanggil fungsi non-const - ini dapat mengakibatkan perilaku yang tidak terdefinisi. Alasannya adalah bahwa instance kelas konstan akan digunakan sebagai instance yang tidak konstan. Fungsi non-const member dapat secara tidak sengaja memodifikasi kelas, yang menyatakan standar C ++ akan menghasilkan perilaku yang tidak terdefinisi.

Kevin
sumber
3
Wow ... itu mengerikan. Anda baru saja meningkatkan jumlah kode, mengurangi kejelasan, dan menambahkan dua konst_cast stinkin '>. Mungkin Anda memiliki contoh di mana ini benar-benar masuk akal?
Shog9
14
Hei, jangan lakukan ini! Ini mungkin jelek, tapi menurut Scott Meyers, itu (hampir) cara yang benar. Lihat Efektif C ++ , 3d ed, Butir 3 di bawah judul "Menghindari duplikasi dalam fungsi anggota konst dan non-biaya.
jwfearn
17
Sementara saya mengerti bahwa solusinya mungkin jelek, bayangkan bahwa kode yang menentukan apa yang harus dikembalikan adalah 50 baris. Maka duplikasi sangat tidak diinginkan - terutama ketika Anda harus faktor ulang kode. Saya telah mengalami ini berkali-kali dalam karir saya.
Kevin
8
Perbedaan antara ini dan Meyers adalah bahwa Meyers memiliki static_cast <const X &> (* this). const_cast adalah untuk menghapus const, bukan menambahkannya.
Steve Jessop
8
@ VioletGiraffe kita tahu bahwa objek tersebut pada awalnya tidak diciptakan const, karena ia adalah anggota non-const dari objek non const, yang kita tahu karena kita berada dalam metode non-const dari objek tersebut. Kompiler tidak membuat inferensi ini, ia mengikuti aturan konservatif. Menurut Anda mengapa const_cast ada, jika bukan karena situasi seperti ini?
Caleth
47

C ++ 17 telah memperbarui jawaban terbaik untuk pertanyaan ini:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Ini memiliki kelebihan yaitu:

  • Jelas apa yang sedang terjadi
  • Memiliki overhead kode minimal - pas dalam satu baris
  • Sulit untuk salah (hanya bisa dibuang volatilesecara tidak sengaja, tetapi volatilemerupakan kualifikasi langka)

Jika Anda ingin pergi rute deduksi penuh maka itu dapat dicapai dengan memiliki fungsi pembantu

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

Sekarang Anda bahkan tidak dapat mengacaukan volatile, dan penggunaannya terlihat seperti

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
sumber
Perhatikan bahwa "as_mutable" dengan konstanta nilai berlebih dihapus (yang umumnya lebih disukai) mencegah contoh terakhir dari bekerja jika f()kembali Tsebagai gantinya T&.
Max Truxa
1
@ MaxTruxa: Ya, dan ini adalah hal yang baik. Jika hanya dikompilasi, kami akan memiliki referensi yang menggantung. Dalam kasus f()pengembalian T, kami tidak ingin memiliki dua kelebihan, constversi saja sudah cukup.
David Stone
Sangat benar, saya minta maaf atas kentut otak saya yang penuh kemarin, tidak tahu apa yang saya pikirkan ketika saya menulis komentar itu. Saya sedang melihat sepasang getter const / bisa berubah mengembalikan a shared_ptr. Jadi apa yang saya benar-benar diperlukan adalah sesuatu seperti as_mutable_ptryang terlihat hampir identik dengan as_mutabledi atas, kecuali bahwa dibutuhkan dan kembali sebuah shared_ptrdan penggunaan std::const_pointer_castbukan const_cast.
Max Truxa
1
Jika suatu metode kembali T const*maka ini akan mengikat T const* const&&daripada mengikat T const* const&(setidaknya dalam pengujian saya itu lakukan). Saya harus menambahkan overload T const*sebagai tipe argumen untuk metode yang mengembalikan pointer.
monkey0506
2
@ monkey0506: Saya telah memperbarui jawaban saya untuk mendukung petunjuk dan juga referensi
David Stone
34

Saya pikir solusi Scott Meyers dapat ditingkatkan di C ++ 11 dengan menggunakan fungsi pembantu tempate. Ini membuat niat menjadi lebih jelas dan dapat digunakan kembali untuk banyak pengambil lainnya.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Fungsi pembantu ini dapat digunakan dengan cara berikut.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

Argumen pertama selalu merupakan penunjuk ini. Yang kedua adalah pointer ke fungsi anggota untuk memanggil. Setelah itu sejumlah argumen tambahan yang sewenang-wenang dapat diajukan sehingga mereka dapat diteruskan ke fungsi. Ini membutuhkan C ++ 11 karena templat variadic.

Pait
sumber
3
Sayang sekali kita tidak harus std::remove_bottom_constikut std::remove_const.
TBBle
Saya tidak suka solusi ini karena masih menyematkan a const_cast. Anda bisa membuat getElementtemplate sendiri, dan menggunakan sifat tipe di dalam untuk mpl::conditionaltipe yang Anda butuhkan, seperti iteratoratau constiteratorjika diperlukan. Masalah sebenarnya adalah bagaimana membuat versi const dari suatu metode ketika bagian tanda tangan ini tidak dapat di-templatize?
v.oddou
2
@ v.oddou: std::remove_const<int const&>is int const &(hapus constkualifikasi tingkat atas ), maka dari itulah senam NonConst<T>dalam jawaban ini. Putatif std::remove_bottom_constdapat menghapus constkualifikasi tingkat bawah , dan melakukan apa yang NonConst<T>dilakukan di sini: std::remove_bottom_const<int const&>::type=> int&.
TBBle
4
Solusi ini tidak berfungsi dengan baik, jika getElementkelebihan beban. Kemudian pointer fungsi tidak dapat diselesaikan tanpa memberikan parameter template secara eksplisit. Mengapa?
John
1
Anda perlu memperbaiki jawaban Anda untuk menggunakan C ++ 11 penerusan sempurna: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }Lengkap: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF
22

Sedikit lebih bertele-tele daripada Meyers, tetapi saya mungkin melakukan ini:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

Metode pribadi memiliki properti yang tidak diinginkan yang mengembalikan non-const Z & untuk turunan const, yang mengapa itu pribadi. Metode privat dapat memecah invarian dari antarmuka eksternal (dalam hal ini invarian yang diinginkan adalah "objek const tidak dapat dimodifikasi melalui referensi yang diperoleh melaluinya ke objek yang memiliki-a").

Perhatikan bahwa komentar adalah bagian dari pola - antarmuka _getZ menentukan bahwa tidak pernah valid untuk menyebutnya (selain dari pengakses, tentu saja): bagaimanapun juga tidak ada manfaat yang mungkin untuk melakukannya, karena 1 karakter lagi untuk diketik dan tidak akan menghasilkan kode yang lebih kecil atau lebih cepat. Memanggil metode ini sama dengan memanggil salah satu accessors dengan const_cast, dan Anda juga tidak ingin melakukannya. Jika Anda khawatir akan membuat kesalahan menjadi jelas (dan itu adalah tujuan yang adil), sebut saja itu const_cast_getZ alih-alih _getZ.

Omong-omong, saya menghargai solusi Meyers. Saya tidak punya keberatan filosofis untuk itu. Namun, secara pribadi, saya lebih suka sedikit pengulangan terkontrol, dan metode pribadi yang hanya boleh disebut dalam keadaan tertentu yang dikontrol ketat, daripada metode yang terlihat seperti derau garis. Pilih racun Anda dan tetap menggunakannya.

[Sunting: Kevin dengan tepat menunjukkan bahwa _getZ mungkin ingin memanggil metode lebih lanjut (katakanlah generateZ) yang merupakan spesialisasi khusus dengan cara getZ yang sama. Dalam kasus ini, _getZ akan melihat const Z & dan harus const_cast sebelum kembali. Itu masih aman, karena accessor boiler mengatur segalanya, tetapi tidak terlalu jelas bahwa itu aman. Lebih jauh, jika Anda melakukan itu dan kemudian mengubah generateZ untuk selalu mengembalikan const, maka Anda juga perlu mengubah getZ untuk selalu mengembalikan const, tetapi kompiler tidak akan memberi tahu Anda bahwa Anda melakukannya.

Poin terakhir tentang kompiler juga berlaku untuk pola yang disarankan Meyers, tetapi poin pertama tentang const_cast yang tidak jelas tidak. Jadi pada keseimbangan saya berpikir bahwa jika _getZ ternyata membutuhkan const_cast untuk nilai pengembaliannya, maka pola ini kehilangan banyak nilainya dibandingkan dengan Meyers. Karena itu juga mengalami kerugian dibandingkan dengan Meyers, saya pikir saya akan beralih ke miliknya dalam situasi itu. Refactoring dari satu ke yang lain itu mudah - tidak memengaruhi kode lain yang valid di kelas, karena hanya kode yang tidak valid dan panggilan boilerplate _getZ.]

Steve Jessop
sumber
3
Ini masih memiliki masalah bahwa hal yang Anda kembalikan mungkin konstan untuk instance X yang konstan. Dalam hal ini, Anda masih memerlukan const_cast di _getZ (...). Jika disalahgunakan oleh pengembang selanjutnya, itu masih bisa mengarah ke UB. Jika hal yang dikembalikan adalah 'bisa berubah', maka ini adalah solusi yang bagus.
Kevin
1
Setiap fungsi pribadi (heck, yang publik juga) dapat salah digunakan oleh pengembang kemudian, jika mereka memilih untuk mengabaikan instruksi BLOCK CAPITAL pada penggunaan yang valid, dalam file header dan juga dalam Doxygen dll. Saya tidak bisa menghentikan itu, dan saya tidak menganggap itu masalah saya karena instruksinya mudah dimengerti.
Steve Jessop
13
-1: Ini tidak berfungsi dalam banyak situasi. Bagaimana jika somethingdalam _getZ()fungsi adalah variabel instan? Kompiler (atau paling tidak beberapa kompiler) akan mengeluh bahwa karena _getZ()const, variabel instan apa pun yang dirujuk di dalamnya adalah const juga. Maka somethingakan menjadi const (itu akan menjadi tipe const Z&) dan tidak dapat dikonversi ke Z&. Dalam pengalaman saya (memang agak terbatas), sebagian besar waktu somethingadalah variabel instan dalam kasus seperti ini.
Gravity
2
@GravityBringer: lalu "sesuatu" perlu melibatkan a const_cast. Itu dimaksudkan untuk menjadi place-holder untuk kode yang diperlukan untuk mendapatkan pengembalian non-const dari objek const, bukan sebagai place-holder untuk apa yang seharusnya ada pada pengambil duplikat. Jadi "sesuatu" bukan hanya variabel instan.
Steve Jessop
2
Saya melihat. Itu benar-benar mengurangi kegunaan teknik. Saya akan menghapus downvote, tetapi SO tidak akan membiarkan saya.
Gravity
22

Pertanyaan yang bagus dan jawaban yang bagus. Saya punya solusi lain, yang tidak menggunakan gips:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

Namun, itu memiliki keburukan yang membutuhkan anggota statis dan kebutuhan menggunakan instancevariabel di dalamnya.

Saya tidak mempertimbangkan semua kemungkinan implikasi (negatif) dari solusi ini. Tolong beri tahu saya jika ada.

gd1
sumber
4
Nah, mari kita pergi dengan fakta sederhana bahwa Anda menambahkan lebih banyak boilerplate. Jika ada, ini harus digunakan sebagai contoh mengapa bahasa membutuhkan cara untuk memodifikasi fungsi kualifikasi bersama dengan tipe kembali auto get(std::size_t i) -> auto(const), auto(&&). Kenapa '&&'? Ahh, jadi saya bisa bilang:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: persis apa yang ada dalam pikiran saya. @ kfsone dan apa yang saya simpulkan juga.
v.oddou
1
@ kfsone, sintaksisnya harus memasukkan thiskata kunci. Saya menyarankan template< typename T > auto myfunction(T this, t args) -> decltype(ident)kata kunci ini akan dikenali sebagai argumen instance objek implisit dan biarkan kompiler mengenali bahwa fungsi saya adalah anggota atau T. Takan disimpulkan secara otomatis di situs panggilan, yang akan selalu menjadi tipe kelas, tetapi dengan kualifikasi cv gratis.
v.oddou
2
Solusi itu juga memiliki keuntungan (versus yang const_cast) untuk memungkinkan untuk kembali iteratordan const_iterator.
Jarod42
1
Jika implementasi dipindahkan dalam file cpp (dan karena metode untuk tidak terduplikasi tidak boleh sepele, itu mungkin terjadi), staticdapat dilakukan pada ruang lingkup file alih-alih ruang lingkup kelas. :-)
Jarod42
8

Anda juga bisa menyelesaikannya dengan templat. Solusi ini sedikit jelek (tetapi keburukan disembunyikan dalam file .cpp) tetapi memang memberikan pemeriksaan kompiler dari constness, dan tidak ada duplikasi kode.

file .h:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

file .cpp:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

Kerugian utama yang dapat saya lihat adalah bahwa karena semua implementasi kompleks dari metode ini ada dalam fungsi global, Anda juga perlu menghubungi anggota X menggunakan metode publik seperti GetVector () di atas (yang selalu perlu menjadi versi const dan non-const) atau Anda dapat menjadikan fungsi ini sebagai teman. Tapi saya tidak suka teman.

[Sunting: menghapus termasuk yang tidak dibutuhkan dari cstdio yang ditambahkan selama pengujian.]

Andy Balaam
sumber
3
Anda selalu dapat membuat fungsi implementasi yang kompleks menjadi anggota statis untuk mendapatkan akses ke anggota pribadi. Fungsi hanya perlu dideklarasikan di file header kelas, definisi dapat berada di file implementasi kelas. Bagaimanapun, ini adalah bagian dari implementasi kelas.
CB Bailey
Aah ya ide bagus! Saya tidak suka hal-hal template muncul di header, tetapi jika karena di sini berpotensi membuat implemtation jauh lebih sederhana, mungkin layak.
Andy Balaam
+1 untuk solusi ini yang tidak menduplikasi kode apa pun, juga tidak menggunakan jelek const_cast(yang secara tidak sengaja dapat digunakan untuk canst sesuatu yang sebenarnya seharusnya menjadi const untuk sesuatu yang tidak).
HelloGoodbye
Saat ini ini dapat disederhanakan dengan tipe pengembalian yang disimpulkan untuk templat (terutama berguna karena mengurangi apa yang harus diduplikasi di kelas dalam kasus anggota).
Davis Herring
3

Bagaimana dengan memindahkan logika ke metode pribadi, dan hanya melakukan hal-hal "dapatkan referensi dan kembalikan" di dalam getter? Sebenarnya, saya akan cukup bingung tentang static dan const cast di dalam fungsi pengambil sederhana, dan saya akan menganggap itu jelek kecuali untuk keadaan yang sangat langka!

MP24
sumber
Untuk menghindari perilaku tidak terdefinisi, Anda masih memerlukan const_cast. Lihat jawabannya oleh Martin York dan komentar saya di sana.
Kevin
1
Kevin, apa jawaban oleh Martin York
Peter Nimmo
2

Apakah curang menggunakan preprocessor?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Ini tidak semewah templat atau gips, tetapi itu membuat niat Anda ("dua fungsi ini harus identik") cukup eksplisit.

pengguna1476176
sumber
1
Tapi kemudian Anda harus berhati-hati dengan garis miring terbalik (seperti biasa untuk makro multiline) dan selain itu Anda kehilangan sintaks yang disorot di sebagian besar (jika tidak semua) editor.
Ruslan
2

Mengejutkan bagi saya bahwa ada begitu banyak jawaban yang berbeda, namun hampir semuanya bergantung pada keajaiban templat berat. Template itu kuat, tetapi terkadang makro mengalahkannya dalam keringkasan. Fleksibilitas maksimum sering dicapai dengan menggabungkan keduanya.

Saya menulis makro FROM_CONST_OVERLOAD()yang dapat ditempatkan di fungsi non-const untuk memanggil fungsi const.

Contoh penggunaan:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Implementasi yang sederhana dan dapat digunakan kembali:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Penjelasan:

Seperti yang diposting dalam banyak jawaban, pola umum untuk menghindari duplikasi kode dalam fungsi non-const member adalah ini:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Banyak pelat baja ini dapat dihindari menggunakan inferensi tipe. Pertama, const_castdapat diringkas dalam WithoutConst(), yang menyimpulkan jenis argumennya dan menghapus const-kualifikasi. Kedua, pendekatan yang sama dapat digunakan WithConst()untuk mengkualifikasi thispointer, yang memungkinkan memanggil metode const-overloaded.

Sisanya adalah makro sederhana yang mengawali panggilan dengan kualifikasi yang benar this->dan menghilangkan const dari hasilnya. Karena ekspresi yang digunakan dalam makro hampir selalu merupakan panggilan fungsi sederhana dengan argumen 1: 1 yang diteruskan, kelemahan makro seperti beberapa evaluasi tidak muncul. Ellipsis dan__VA_ARGS__ juga bisa digunakan, tetapi seharusnya tidak diperlukan karena koma (seperti pemisah argumen) terjadi di dalam tanda kurung.

Pendekatan ini memiliki beberapa manfaat:

  • Sintaks minimal dan alami - cukup bungkus panggilan FROM_CONST_OVERLOAD( )
  • Tidak diperlukan fungsi anggota tambahan
  • Kompatibel dengan C ++ 98
  • Implementasi sederhana, tidak ada pemrograman template, dan tidak ada ketergantungan
  • Extensible: hubungan const lainnya dapat ditambahkan (seperti const_iterator, std::shared_ptr<const T>, dll). Untuk ini, cukup kelebihan WithoutConst()untuk jenis yang sesuai.

Keterbatasan: solusi ini dioptimalkan untuk skenario di mana kelebihan non-const melakukan persis sama dengan overload const, sehingga argumen dapat diteruskan 1: 1. Jika logika Anda berbeda dan Anda tidak memanggil versi const melalui this->Method(args), Anda dapat mempertimbangkan pendekatan lain.

TheOperator
sumber
2

Bagi mereka (seperti saya) yang

  • gunakan c ++ 17
  • ingin menambahkan jumlah paling sedikit boilerplate / pengulangan dan
  • tidak keberatan menggunakan makro (sambil menunggu meta-class ...),

di sini adalah mengambil lain:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

Ini pada dasarnya adalah campuran dari jawaban dari @Pait, @DavidStone dan @ sh1 ( EDIT : dan peningkatan dari @cdhowie). Apa yang ditambahkan ke tabel adalah Anda hanya mendapatkan satu baris kode tambahan yang hanya memberi nama fungsi (tetapi tidak ada argumen atau duplikasi tipe pengembalian):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Catatan: gcc gagal mengkompilasi ini sebelum 8.1, clang-5 dan ke atas serta MSVC-19 senang (menurut kompiler explorer ).

axxel
sumber
Ini hanya berhasil bagi saya. Ini jawaban yang bagus, terima kasih!
Pendek
Tidakkah seharusnya decltype()s juga digunakan std::forwardpada argumen untuk memastikan bahwa kami menggunakan jenis pengembalian yang benar dalam kasus di mana kami memiliki kelebihan get()yang menggunakan berbagai jenis referensi?
cdhowie
@cdhowie Bisakah Anda memberikan contoh?
axxel
@ Maxxel Ini dibuat-buat, tapi ini dia . The NON_CONSTmenyimpulkan makro jenis kembali tidak benar dan const_casts dengan jenis yang salah karena kurangnya forwarding di decltype(func(a...))jenis. Menggantinya dengan decltype(func(std::forward<T>(a)...)) memecahkan ini . (Hanya ada kesalahan linker karena saya tidak pernah mendefinisikan X::getkelebihan yang dinyatakan .)
cdhowie
1
Terima kasih @cdhowie, saya menjadi mucikari contoh Anda untuk benar-benar menggunakan kelebihan non-const: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel
1

Berikut adalah versi C ++ 17 dari fungsi pembantu statis templat, dengan dan uji SFINAE opsional.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Versi lengkap: https://godbolt.org/z/mMK4r3

atablash
sumber
1

Saya datang dengan makro yang menghasilkan pasangan fungsi const / non-const secara otomatis.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

Lihat akhir jawaban untuk implementasi.

Argumen MAYBE_CONSTdiduplikasi. Dalam salinan pertama, CVdiganti dengan apa-apa; dan di salinan kedua diganti denganconst .

Tidak ada batasan berapa kali CV dapat muncul dalam argumen makro.

Namun ada sedikit ketidaknyamanan. Jika CVmuncul di dalam tanda kurung, sepasang tanda kurung ini harus diawali dengan CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Penerapan:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Implementasi Pra-C ++ 20 yang tidak mendukung CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end
HolyBlackCat
sumber
0

Biasanya, fungsi anggota yang Anda perlukan versi const dan non-const adalah getter dan setter. Sebagian besar waktu mereka satu-baris sehingga duplikasi kode tidak menjadi masalah.

Dima
sumber
2
Itu mungkin benar sebagian besar waktu. Namun ada beberapa pengecualian.
Kevin
1
getters tetap, setter const tidak masuk akal;)
jwfearn
Maksud saya pengambil non-const efektif setter. :)
Dima
0

Saya melakukan ini untuk seorang teman yang berhak membenarkan penggunaan const_cast... tidak mengetahuinya saya mungkin akan melakukan sesuatu seperti ini (tidak benar-benar elegan):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}
matovitch
sumber
0

Saya akan menyarankan templat fungsi statis pembantu pribadi, seperti ini:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};
dats
sumber
-1

Artikel DDJ ini menunjukkan cara menggunakan spesialisasi templat yang tidak mengharuskan Anda untuk menggunakan const_cast. Untuk fungsi sederhana seperti itu sebenarnya tidak diperlukan.

boost :: any_cast (pada satu titik, tidak lagi) menggunakan const_cast dari versi const yang memanggil versi non-const untuk menghindari duplikasi. Anda tidak bisa memaksakan const semantik pada versi non-const, jadi Anda harus sangat berhati-hati dengan itu.

Pada akhirnya beberapa kode duplikasi adalah baik-baik saja selama dua cuplikan secara langsung di atas satu sama lain.

Greg Rogers
sumber
Artikel DDJ tampaknya merujuk pada iterator - yang tidak relevan dengan pertanyaan. Konstanta bukan data konstan - mereka adalah iterator yang menunjuk ke data konstan.
Kevin
-1

Untuk menambah solusi jwfearn dan kevin yang disediakan, inilah solusi yang sesuai ketika fungsi mengembalikan shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};
Christer Swahn
sumber
-1

Tidak menemukan apa yang saya cari, jadi saya menggulung beberapa sendiri ...

Yang ini sedikit bertele-tele, tetapi memiliki keuntungan menangani banyak metode kelebihan nama yang sama (dan tipe pengembalian) sekaligus:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Jika Anda hanya memiliki satu constmetode per nama, tetapi masih banyak metode untuk digandakan, maka Anda mungkin lebih suka ini:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

Sayangnya ini rusak segera setelah Anda mulai kelebihan nama (daftar argumen argumen fungsi pointer tampaknya belum terselesaikan pada titik itu, sehingga tidak dapat menemukan kecocokan untuk argumen fungsi). Meskipun Anda dapat membuat templat jalan keluar dari itu,

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

Tetapi argumen referensi ke constmetode gagal untuk mencocokkan dengan argumen yang tampaknya bernilai dengan templat dan rusak. Tidak yakin kenapa.Inilah sebabnya .

sh1
sumber