Analisis kode proyek DeepSpeech atau mengapa tidak perlu ditulis di namespace std

DeepSpeech adalah sumber terbuka dan mesin pengenalan suara gratis yang dikembangkan oleh Mozilla. Mesin tersebut memiliki performa yang cukup tinggi dan ulasan pengguna yang baik, yang menjadikan kode proyek sebagai target yang menarik untuk pengujian. Artikel ini dikhususkan untuk analisis kesalahan yang ditemukan dalam kode C ++ proyek DeepSpeech.



gambar1.png


pengantar



Kami telah berulang kali mencari kesalahan dalam proyek menggunakan pembelajaran mesin, dan DeepSpeech tidak terkecuali bagi kami. Tidak mengherankan, karena proyek ini cukup populer: pada saat penulisan ini, sudah memiliki lebih dari 15k bintang di GitHub.



Seperti biasa, pencarian kesalahan yang akan saya kutip dalam artikel ini dilakukan dengan menggunakan penganalisis kode statis PVS-Studio.



Untuk pekerjaannya, DeepSpeech menggunakan pustaka TensorFlow. Saya mematikan analisis kode perpustakaan ini, karena kami telah menulis artikel terpisah tentangnya., bagaimanapun, saya tidak mematikan analisis dari perpustakaan lain yang digunakan. Apa alasannya ini? Kesalahan dalam perpustakaan apa pun yang Anda sertakan dalam proyek Anda juga menjadi kesalahan dalam proyek Anda. Oleh karena itu, sangat berguna untuk menganalisis tidak hanya Anda, tetapi juga kode pihak ketiga yang Anda gunakan. Anda dapat membaca opini rinci tentang ini di artikel terbaru kami .



Ini mengakhiri pengantar singkat - saatnya beralih ke analisis kesalahan. Ngomong-ngomong, jika Anda datang ke sini untuk mengetahui jawaban atas pertanyaan yang saya ajukan di judul artikel (mengapa Anda tidak boleh menulis di namespace std), Anda bisa langsung melihat di akhir artikel. Contoh yang sangat menarik menanti Anda di sana!



Gambaran dari 10 peringatan menarik yang dikeluarkan oleh penganalisis



Peringatan 1



V773 Fungsi itu keluar tanpa melepaskan penunjuk 'data'. Kebocoran memori mungkin terjadi. edit-fst.h 311



// EditFstData method implementations: just the Read method.
template <typename A, typename WrappedFstT, typename MutableFstT>
EditFstData<A, WrappedFstT, MutableFstT> *
EditFstData<A, WrappedFstT, MutableFstT>::Read(std::istream &strm,
                                               const FstReadOptions &opts)
{
  auto *data = new EditFstData<A, WrappedFstT, MutableFstT>();
  // next read in MutabelFstT machine that stores edits
  FstReadOptions edits_opts(opts);

  ....
  
  std::unique_ptr<MutableFstT> edits(MutableFstT::Read(strm, edits_opts));
  if (!edits) return nullptr; // <=

  ....
}


Potongan kode ini berisi contoh klasik kebocoran memori: fungsi Baca memanggil ' return nullptr ' tanpa membebaskan memori yang dialokasikan dengan ekspresi ' new EditFstData '. Dengan keluarnya fungsi seperti itu (tanpa memanggil delete data ), hanya penunjuk itu sendiri yang akan dihapus, dan destruktor dari objek yang dituju tidak akan dipanggil. Dengan demikian, objek tersebut akan terus disimpan dalam memori, dan tidak dapat lagi menghapus atau menggunakannya.



Selain kesalahan, kode ini juga berisi praktik lain yang tidak terlalu bagus: kode dari satu fungsi secara bersamaan menggunakan petunjuk cerdas dan biasa. Misalnya jika datajuga merupakan penunjuk cerdas, maka kesalahan seperti itu tidak akan terjadi: jika perlu, saat keluar dari ruang lingkup, penunjuk cerdas secara otomatis memanggil destruktor dari objek yang disimpan.



Peringatan 2



V1062 Kelas 'DfsState' mendefinisikan operator 'baru' kustom. Operator 'hapus' juga harus ditentukan. dfs-visit.h 62



// An FST state's DFS stack state.
template <class FST>
struct DfsState {
public:
  ....
  void *operator new(size_t size, 
                     MemoryPool<DfsState<FST>> *pool) {
    return pool->Allocate();
  }
  ....
}


PVS-Studio tidak berhenti dan terus menambahkan diagnostik baru. Potongan kode ini adalah contoh yang bagus untuk menunjukkan pekerjaan dari diagnostik terbaru bernomor V1062 .



Aturan yang diberlakukan diagnostik ini sederhana: jika Anda menentukan operator 'baru' Anda sendiri, Anda juga harus menentukan operator 'hapus' Anda sendiri. Cara sebaliknya bekerja dengan cara yang sama: jika Anda mendefinisikan 'hapus' Anda sendiri, maka 'baru' milik Anda juga harus ditentukan.



Pada contoh di atas, aturan ini dilanggar: objek akan dibuat menggunakan 'baru' yang kami definisikan, dan dihapus menggunakan 'delete' standar. Mari kita lihat apa fungsi Allocate dari kelas MemoryPool ,yang disebut 'baru' kita sendiri:



void *Allocate() {
  if (free_list_ == nullptr) {
    auto *link = static_cast<Link *>(mem_arena_.Allocate(1));
    link->next = nullptr;
    return link;
  } else {
    auto *link = free_list_;
    free_list_ = link->next;
    return link;
  }
}


Fungsi ini membuat item dan menambahkannya ke daftar tertaut. Adalah logis bahwa alokasi seperti itu harus ditulis dengan 'baru' sendiri.



Tapi tunggu dulu! Beberapa baris di bawah ini berisi fungsi berikut:



void Free(void *ptr) {
  if (ptr) {
    auto *link = static_cast<Link *>(ptr);
    link->next = free_list_;
    free_list_ = link;
  }
}


Ini berarti kita sudah memiliki fungsi siap pakai untuk alokasi dan rilis. Kemungkinan besar, programmer harus menulis operator 'hapus' miliknya sendiri, menggunakan fungsi Free () untuk membebaskannya .



Penganalisis mendeteksi setidaknya tiga kesalahan lainnya:



  • V1062 Kelas 'VectorState' mendefinisikan operator 'baru' kustom. Operator 'hapus' juga harus ditentukan. vektor-fst.h 31
  • V1062 Kelas 'CacheState' mendefinisikan operator 'baru' kustom. Operator 'hapus' juga harus ditentukan. cache.h 65


Peringatan 3



V703 Aneh bahwa bidang 'first_path' di kelas turunan 'ShortestPathOptions' menimpa bidang di kelas dasar 'ShortestDistanceOptions'. Periksa jalur: jalur terpendek. Jam: 35, jarak terpendek. Jam: 34. jalur terpendek. h 35



// Base class
template <class Arc, class Queue, class ArcFilter>
struct ShortestDistanceOptions {
  Queue *state_queue;    // Queue discipline used; owned by caller.
  ArcFilter arc_filter;  // Arc filter (e.g., limit to only epsilon graph).
  StateId source;        // If kNoStateId, use the FST's initial state.
  float delta;           // Determines the degree of convergence required
  bool first_path;       // For a semiring with the path property (o.w.
                         // undefined), compute the shortest-distances along
                         // along the first path to a final state found
                         // by the algorithm. That path is the shortest-path
                         // only if the FST has a unique final state (or all
                         // the final states have the same final weight), the
                         // queue discipline is shortest-first and all the
                         // weights in the FST are between One() and Zero()
                         // according to NaturalLess.

  ShortestDistanceOptions(Queue *state_queue, ArcFilter arc_filter,
                          StateId source = kNoStateId,
                          float delta = kShortestDelta)
      : state_queue(state_queue),
        arc_filter(arc_filter),
        source(source),
        delta(delta),
        first_path(false) {}
};
// Derived class
template <class Arc, class Queue, class ArcFilter>
struct ShortestPathOptions
    : public ShortestDistanceOptions<Arc, Queue, ArcFilter> {
  using StateId = typename Arc::StateId;
  using Weight = typename Arc::Weight;

  int32 nshortest;    // Returns n-shortest paths.
  bool unique;        // Only returns paths with distinct input strings.
  bool has_distance;  // Distance vector already contains the
                      // shortest distance from the initial state.
  bool first_path;    // Single shortest path stops after finding the first
                      // path to a final state; that path is the shortest path
                      // only when:
                      // (1) using the ShortestFirstQueue with all the weights
                      // in the FST being between One() and Zero() according to
                      // NaturalLess or when
                      // (2) using the NaturalAStarQueue with an admissible
                      // and consistent estimate.
  Weight weight_threshold;  // Pruning weight threshold.
  StateId state_threshold;  // Pruning state threshold.

  ShortestPathOptions(Queue *queue, ArcFilter filter, int32 nshortest = 1,
                      bool unique = false, bool has_distance = false,
                      float delta = kShortestDelta, bool first_path = false,
                      Weight weight_threshold = Weight::Zero(),
                      StateId state_threshold = kNoStateId)
      : ShortestDistanceOptions<Arc, Queue, ArcFilter>(queue, filter,
                                                       kNoStateId, delta),
        nshortest(nshortest),
        unique(unique),
        has_distance(has_distance),
        first_path(first_path),
        weight_threshold(std::move(weight_threshold)),
        state_threshold(state_threshold) {}
};


Setuju, tidak mudah menemukan potensi kesalahan, bukan?



Masalahnya adalah bahwa basis dan kelas turunan berisi bidang dengan nama yang sama: first_path . Ini akan menyebabkan kelas turunan memiliki bidangnya sendiri yang berbeda, yang menggantikan bidang dari kelas dasar dengan namanya. Kesalahan seperti itu dapat menyebabkan kebingungan yang serius.



Untuk lebih memahami apa yang saya maksud, saya mengusulkan untuk mempertimbangkan contoh sintetis singkat dari dokumentasi kami. Katakanlah kita memiliki kode berikut:



class U {
public:
  int x;
};

class V : public U {
public:
  int x;  // <= V703 here
  int z;
};


Di sini nama x diganti di dalam kelas turunan. Sekarang pertanyaannya adalah: nilai apa yang akan dicetak kode berikut?



int main() {
  V vClass;
  vClass.x = 1;
  U *uClassPtr = &vClass;
  std::cout << uClassPtr->x << std::endl;
  ....
}


Jika Anda berpikir nilai yang tidak ditentukan akan menjadi keluaran, maka Anda benar. Dalam contoh ini, satuan akan dituliskan ke bidang kelas turunan, tetapi pembacaan akan dilakukan dari bidang kelas dasar, yang pada saat keluaran masih belum ditentukan.



Nama yang tumpang tindih dalam hierarki kelas adalah kesalahan potensial yang harus dihindari :)



Peringatan 4



V1004 Penunjuk 'aiter' digunakan dengan tidak aman setelah diverifikasi terhadap nullptr. Periksa jalur: 107, 119. kunjungi.h 119



template <....>
void Visit(....)
{
  ....
  // Deletes arc iterator if done.
  auto *aiter = arc_iterator[state];
  if ((aiter && aiter->Done()) || !visit) {
    Destroy(aiter, &aiter_pool);
    arc_iterator[state] = nullptr;
    state_status[state] |= kArcIterDone;
  }
  // Dequeues state and marks black if done.
  if (state_status[state] & kArcIterDone) {
    queue->Dequeue();
    visitor->FinishState(state);
    state_status[state] = kBlackState;
    continue;
  }
  const auto &arc = aiter->Value();       // <=
  ....
}


Pointer aiter digunakan setelah itu telah diperiksa untuk nullptr . Penganalisis membuat asumsi: jika pointer diperiksa untuk nullptr , maka selama pemeriksaan itu dapat memiliki nilai seperti itu.



Dalam hal ini, mari kita lihat apa yang terjadi pada aiter jika itu benar-benar sama dengan nol. Pertama, pointer ini akan diperiksa di pernyataan ' if ((aiter && aiter-> Done ()) ||! Visit) '. Kondisi ini akan sama dengan false , dan kita tidak akan masuk ke cabang lalu dari ini jika . Dan kemudian, menurut semua kanon kesalahan klasik, pointer nol akan didereferensi : ' aiter-> Value ();'. Dereferensi ini menghasilkan perilaku yang tidak terdefinisi.



Peringatan 5



Contoh berikut berisi dua kesalahan sekaligus:



  • V595 Penunjuk 'istrm' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 60, 61. mapped-file.cc 60
  • V595 Penunjuk 'istrm' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 39, 61. mapped-file.cc 39


MappedFile *MappedFile::Map(std::istream *istrm, bool memorymap,
                            const string &source, size_t size) {
  const auto spos = istrm->tellg();        // <=
  ....
  istrm->seekg(pos + size, std::ios::beg); // <=
  if (istrm) {                             // <=
    VLOG(1) << "mmap'ed region of " << size
            << " at offset " << pos
            << " from " << source
            << " to addr " << map;
  return mmf.release();
  }
  ....
}


Kesalahan yang ditemukan di sini lebih jelas daripada kesalahan dari contoh sebelumnya. The istrm pointer yang pertama dereferenced (dua kali), dan hanya setelah itu nol cek and error logging berikut. Ini dengan jelas menunjukkan: jika penunjuk null datang ke fungsi ini sebagai istrm , maka perilaku tidak terdefinisi (atau, lebih mungkin, program crash) akan terjadi tanpa logging apapun. Disorder ... bug seperti ini tidak boleh diabaikan.



gambar2.png


Peringatan 6



V730 Tidak semua anggota kelas diinisialisasi di dalam konstruktor. Pertimbangkan untuk memeriksa: stones_written_. ersatz_progress.cc 14



ErsatzProgress::ErsatzProgress()
  : current_(0)
  , next_(std::numeric_limits<uint64_t>::max())
  , complete_(next_)
  , out_(NULL)
{}


Penganalisis memperingatkan kita bahwa konstruktor tidak menginisialisasi semua bidang struktur ErzatzProgress . Mari bandingkan konstruktor ini dengan daftar bidang dalam struktur ini:



class ErsatzProgress {
  ....
private:
    void Milestone();

    uint64_t current_, next_, complete_;
    unsigned char stones_written_;
    std::ostream *out_;
};


Memang, Anda dapat melihat bahwa konstruktor menginisialisasi semua bidang kecuali stones_written_ .



Catatan : contoh ini mungkin bukan kesalahan. Kesalahan sebenarnya hanya akan terjadi jika nilai bidang yang tidak diinisialisasi digunakan .



Namun, diagnostik V730 membantu Anda men-debug kasus penggunaan ini sebelumnya. Bagaimanapun, sebuah pertanyaan alami muncul: jika programmer memutuskan untuk menginisialisasi semua bidang kelas secara khusus, lalu mengapa dia harus memiliki alasan untuk meninggalkan satu bidang tanpa nilai?



Dugaan saya bahwa bidang stones_written_ tidak diinisialisasi karena kesalahan dikonfirmasi ketika beberapa baris di bawah ini saya melihat konstruktor lain:



ErsatzProgress::ErsatzProgress(uint64_t complete,
                               std::ostream *to,
                               const std::string &message)
  : current_(0)
  , next_(complete / kWidth)
  , complete_(complete)
  , stones_written_(0)
  , out_(to)
{
  ....
}


Di sini semua bidang kelas diinisialisasi, yang menegaskan: pemrogram benar-benar berencana untuk menginisialisasi semua bidang, tetapi secara tidak sengaja lupa tentang satu hal.



Peringatan 7



V780 Objek '& params' dari jenis non-pasif (non-PDS) tidak dapat diinisialisasi menggunakan fungsi memset. binary_format.cc 261



/* Not the best numbering system,
   but it grew this way for historical reasons
 * and I want to preserve existing binary files. */
typedef enum
{
  PROBING=0,
  REST_PROBING=1,
  TRIE=2,
  QUANT_TRIE=3,
  ARRAY_TRIE=4,
  QUANT_ARRAY_TRIE=5
}
ModelType;

....

struct FixedWidthParameters {
  unsigned char order;
  float probing_multiplier;
  // What type of model is this?
  ModelType model_type;
  // Does the end of the file 
  // have the actual strings in the vocabulary?
  bool has_vocabulary;
  unsigned int search_version;
};

....

// Parameters stored in the header of a binary file.
struct Parameters {
  FixedWidthParameters fixed;
  std::vector<uint64_t> counts;
};

....

void BinaryFormat::FinishFile(....)
{
  ....
  // header and vocab share the same mmap.
  Parameters params = Parameters();
  memset(&params, 0, sizeof(Parameters)); // <=
  ....
}


Untuk memahami peringatan ini, saya sarankan untuk memahami terlebih dahulu apa itu tipe PDS. PDS adalah singkatan dari Passive Data Structure, yaitu struktur data sederhana. Kadang-kadang alih-alih "PDS" mereka mengatakan "POD" - "Data Lama Biasa". Secara sederhana (saya mengutip dari Wikipedia bahasa Rusia ), tipe PDS adalah tipe data yang memiliki pengaturan bidang yang didefinisikan secara kaku dalam memori, yang tidak memerlukan pembatasan akses dan kontrol otomatis. Sederhananya, ini adalah tipe data yang hanya berisi tipe bawaan.



Ciri khas dari tipe POD adalah variabel dari tipe ini dapat diubah dan diproses menggunakan fungsi manajemen memori primitif (memset, memcpy, dan sebagainya). Namun, ini tidak dapat dikatakan tentang jenis "non-PDS": penanganan nilai tingkat rendah seperti itu dapat menyebabkan kesalahan yang serius. Misalnya, kebocoran memori, pembilasan ganda dari resource yang sama, atau perilaku tidak ditentukan.



PVS-Studio mengeluarkan peringatan ke kode yang diberikan di atas: Anda tidak dapat menangani struktur tipe Parameter dengan cara ini. Jika Anda melihat definisi dari struktur ini, Anda dapat melihat bahwa anggota keduanya adalah tipe std :: vector... Jenis ini secara aktif menggunakan manajemen memori otomatis dan, selain data konten, menyimpan tambahan, variabel layanan. Mengosongkan bidang seperti itu menggunakan memset dapat mematahkan logika kelas dan merupakan kesalahan serius.



Peringatan 8



V575 Pointer null potensial diteruskan ke fungsi 'memcpy'. Periksa argumen pertama. Periksa baris: 73, 68.modelstate.cc 73



Metadata*
ModelState::decode_metadata(const DecoderState& state, 
                            size_t num_results)
{
  ....
  Metadata* ret = (Metadata*)malloc(sizeof(Metadata));
  ....
  memcpy(ret, &metadata, sizeof(Metadata));
  return ret;
}


Peringatan berikutnya memberi tahu kita bahwa pointer nol sedang diteruskan ke fungsi memcpy . Ya, memang, jika fungsi malloc gagal mengalokasikan memori, itu akan mengembalikan NULL . Dalam kasus ini, penunjuk ini akan diteruskan ke fungsi memset , di mana ia akan direferensikan - dan, karenanya, program crash yang mempesona.



Namun, beberapa pembaca kami mungkin marah: jika memori terlalu banyak / terfragmentasi sehingga malloc tidak dapat mengalokasikan memori, apakah penting apa yang terjadi selanjutnya? Program akan tetap macet, karena karena kekurangan memori, program tidak akan dapat berfungsi secara normal.



Kami telah berulang kali menemukan pendapat ini dan percaya bahwa itu tidak benar. Saya akan memberi tahu Anda secara detail mengapa ini benar-benar terjadi, tetapi topik ini layak mendapat artikel terpisah. Sangat pantas kami menulisnya beberapa tahun yang lalu :) Jika Anda bertanya-tanya mengapa Anda harus selalu memeriksa penunjuk yang dikembalikan oleh fungsi malloc , maka saya mengundang Anda untuk membaca: Mengapa penting untuk memeriksa apa yang dikembalikan malloc .



Peringatan 9 Peringatan



berikut ini disebabkan oleh alasan yang sama seperti yang sebelumnya. Benar, ini menunjukkan kesalahan yang sedikit berbeda.



V769Penunjuk 'middle_begin_' di ekspresi 'middle_begin_ + (counts.size () - 2)' bisa berupa nullptr. Dalam kasus seperti itu, nilai yang dihasilkan akan menjadi tidak masuk akal dan tidak boleh digunakan. Periksa baris: 553, 552. search_trie.cc 553



template <class Quant, class Bhiksha> class TrieSearch {
....
private:
  ....
  Middle *middle_begin_, *middle_end_;
  ....
};

template <class Quant, class Bhiksha>
uint8_t *TrieSearch<Quant, Bhiksha>::SetupMemory(....)
{
  ....
  middle_begin_
    = static_cast<Middle*>(malloc(sizeof(Middle) * (counts.size() - 2)));
  middle_end_ = middle_begin_ + (counts.size() - 2);
  ....
}


Seperti pada contoh sebelumnya, memori dialokasikan di sini menggunakan fungsi malloc . Pointer yang dikembalikan digunakan dalam ekspresi aritmatika tanpa memeriksa nullptr . Sayangnya, hasil dari ekspresi seperti itu tidak masuk akal, dan nilai yang sama sekali tidak berguna akan disimpan di bidang middle_end_ .



Peringatan 10



Dan akhirnya, contoh paling menarik menurut saya ditemukan di perpustakaan kenlm yang termasuk dalam DeepSpeech:



V1061 Memperluas namespace 'std' dapat mengakibatkan perilaku yang tidak ditentukan. ukuran_iterator.hh 210



// Dirty hack because g++ 4.6 at least wants
// to do a bunch of copy operations.
namespace std {
inline void iter_swap(util::SizedIterator first,
                      util::SizedIterator second)
{
  util::swap(*first, *second);
}
} // namespace std


Trik yang disebut "trik kotor" di komentar itu benar-benar kotor. Intinya adalah bahwa perluasan namespace std dapat menyebabkan perilaku tidak terdefinisi.



Mengapa? Karena isi namespace std ditentukan secara eksklusif oleh panitia standar. Itulah mengapa standar internasional bahasa C ++ secara eksplisit melarang perluasan std dengan cara ini.



Standar terakhir yang didukung dalam g ++ 4.6 adalah C ++ 03. Berikut adalah kutipan terjemahan dari draft kerja akhir C ++ 03(lihat item 17.6.4.2.1): "Perilaku program C ++ tidak terdefinisi jika program menambahkan deklarasi atau definisi ke namespace std atau namespace bertingkat std, kecuali ditentukan lain." Kutipan ini berlaku untuk semua standar berikutnya (C ++ 11, C ++ 14, C ++ 17, dan C ++ 20).



Saya mengusulkan untuk mempertimbangkan bagaimana Anda dapat memperbaiki kode yang bermasalah dari contoh kami. Pertanyaan logis pertama: apakah "kasus-kasus yang menunjukkan kebalikannya"? Ada beberapa situasi di mana ekspansi std tidak mengarah pada perilaku yang tidak ditentukan. Anda dapat membaca lebih lanjut tentang semua situasi ini di halaman dokumentasi untuk diagnostik V1061 , tetapi sekarang penting bagi kami bahwa salah satu kasus ini adalah penambahan spesialisasi template fungsi.



Karenastd sudah memiliki fungsi yang disebut iter_swap (catatan: fungsi template), adalah logis untuk mengasumsikan bahwa pemrogram ingin memperluas kemampuannya sehingga dapat bekerja dengan tipe util :: SizedIterator . Tapi inilah nasib buruknya: alih-alih menambahkan spesialisasi ke template fungsi , programmer hanya menulis kelebihan beban biasa . Seharusnya ditulis seperti ini:



namespace std {
template <>
inline void iter_swap(util::SizedIterator first,
                      util::SizedIterator second)
{
  util::swap(*first, *second);
}
} // namespace std


Namun, kode ini juga tidak sesederhana itu. Intinya adalah kode ini hanya akan valid hingga standar C ++ 20. Ya, itu juga mencatat spesialisasi template fungsi yang mengarah ke perilaku tak terdefinisi (lihat draf kerja akhir C ++ 20 , bagian 16.5.4.2.1). Dan karena kode ini milik perpustakaan, kemungkinan besar cepat atau lambat akan dibuat dengan tanda -std = C ++ 20 . Omong-omong, PVS-Studio membedakan versi standar mana yang digunakan dalam kode dan, bergantung pada ini, mengeluarkan atau tidak mengeluarkan peringatan. Lihat sendiri: contoh untuk C ++ 17 , contoh untuk C ++ 20 .



Nyatanya, Anda bisa melakukan lebih mudah. Untuk memperbaiki kesalahan ini, Anda hanya perlu mentransfer definisi iter_swap Anda sendirike dalam namespace yang sama yang mendefinisikan kelas SizedIterator . Dalam kasus ini, di tempat iter_swap dipanggil , Anda perlu menambahkan "using std :: iter_swap;". Ternyata seperti ini (definisi kelas SizedIterator dan fungsi util :: swap () telah diubah untuk kesederhanaan):



namespace util
{
  class SizedIterator
  {
  public:
    SizedIterator(int i) : m_data(i) {}

    int& operator*()
    {
      return m_data;
    }
  private:
    int m_data;
  };

  ....

  inline void iter_swap(SizedIterator first,
                        SizedIterator second)
  {
    std::cout << "we are inside util::iter_swap" << std::endl;
    swap(*first, *second);
  }
}


int main()
{
  double d1 = 1.1, d2 = 2.2;
  double *pd1 = &d1, *pd2 = &d2;
  util::SizedIterator si1(42), si2(43);

  using std::iter_swap;

  iter_swap(pd1, pd2);
  iter_swap(si1, si2); // "we are inside util::iter_swap"

  return 0;
}


Sekarang kompilator akan secara independen memilih kelebihan yang diperlukan dari fungsi iter_swap berdasarkan Pencarian Argumen (ADL). Untuk kelas SizedIterator , versi dari namespace util akan dipanggil , dan untuk jenis lainnya, versi dari namespace std akan dipanggil . Buktinya ada di tautan . Selain itu, tidak perlu menambahkan "using" di dalam fungsi library: karena kodenya sudah ada di dalam std , compiler akan tetap memilih overload yang benar.



Dan kemudian - voila - fungsi iter_swap khusus akan berfungsi sebagaimana mestinya tanpa "trik kotor" dan sihir lainnya :)



image3.png


Kesimpulan



Ini menyimpulkan artikel saya. Saya harap kesalahan yang saya temukan menarik bagi Anda dan Anda belajar sesuatu yang baru dan berguna bagi diri Anda sendiri. Jika Anda telah membaca sampai saat ini, maka saya dengan tulus berharap Anda kode yang bersih dan rapi tanpa kesalahan. Biarkan bug melewati proyek Anda!



PS Kami pikir itu adalah praktik yang buruk untuk menulis kode Anda sendiri di namespace std. Bagaimana menurut anda? Saya menantikan tanggapan Anda di komentar.



Jika Anda mengembangkan di C, C ++, C # atau Java dan, seperti saya, Anda tertarik pada topik analisis statis, maka saya sarankan untuk mencoba sendiri PVS-Studio. Anda dapat mengunduhnya di tautan .









Jika Anda ingin berbagi artikel ini dengan audiens berbahasa Inggris, silakan gunakan tautan terjemahan: George Gribkov. Memeriksa Kode DeepSpeech, atau Mengapa Anda Tidak Harus Menulis di namespace std .



All Articles