Breaking

Cara menghindari bug menggunakan C ++ modern

Tutorialankha.com | Salah satu masalah utama dengan C ++ adalah memiliki sejumlah besar konstruksi yang perilakunya tidak terdefinisi, atau hanya tak terduga bagi programmer. Kami sering menemukan mereka saat menggunakan penganalisis statis kami di berbagai proyek. Tapi, seperti kita semua tahu, yang terbaik adalah mendeteksi kesalahan pada tahap kompilasi. Mari kita lihat teknik di C + + modern yang membantu menulis tidak hanya kode yang sederhana dan jelas, namun membuatnya lebih aman dan lebih dapat diandalkan.


Apa itu Modern C ++?

Istilah Modern C ++ menjadi sangat populer setelah rilis C ++ 11. Apa artinya? Pertama-tama, Modern C ++ adalah seperangkat pola dan idiom yang dirancang untuk menghilangkan kelemahan dari "C dengan kelas" lama yang baik, sehingga banyak programmer C ++ terbiasa, terutama jika mereka mulai memprogram di C. C ++ 11 Terlihat jauh lebih ringkas dan mudah dimengerti, yang sangat penting.

Apa yang orang biasanya pikirkan ketika mereka berbicara tentang Modern C ++? Paralelisme, perhitungan waktu kompilasi, RAII, lambdas, rentang, konsep, modul, dan komponen penting lainnya dari perpustakaan standar (misalnya, API untuk bekerja dengan sistem berkas). Ini adalah semua modernisasi yang sangat keren, dan kami berharap dapat melihat mereka di set standar berikutnya. Namun, saya ingin menarik perhatian pada cara standar baru memungkinkan penulisan kode yang lebih aman. Saat mengembangkan penganalisis statis, kita melihat sejumlah besar kesalahan yang bervariasi, dan terkadang kita tidak dapat tidak berpikir: "Tapi di C ++ modern ini bisa dihindari". Oleh karena itu, saya sarankan kita memeriksa beberapa kesalahan yang ditemukan oleh PVS-Studio di berbagai proyek Open Source. Juga, kita akan melihat bagaimana mereka bisa diperbaiki.

Inferensi tipe otomatis

Di C ++, kata kunci otomatis dan deklarasi ditambahkan. Tentu saja, Anda sudah tahu bagaimana mereka bekerja.
std::map<int, int> m;
auto it = m.find(42);
//C++98: std::map<int, int>::iterator it = m.find(42);
Ini sangat mudah untuk mempersingkat jenis yang panjang, tanpa kehilangan pembacaan kode. Namun, kata kunci ini menjadi cukup ekspansif, bersama dengan template: tidak perlu menentukan jenis nilai pengembalian dengan auto dan decltype.

Tapi mari kita kembali ke topik kita. Berikut adalah contoh kesalahan 64-bit:
string str = .....;
unsigned n = str.find("ABC");
if (n != string::npos)
Dalam aplikasi 64-bit, nilai string :: pos lebih besar dari nilai maksimum UINT_MAX, yang dapat ditunjukkan oleh variabel tipe unsigned. Bisa nampak bahwa ini adalah kasus dimana auto dapat menyelamatkan kita dari masalah seperti ini: jenis dari n variabel tidak penting bagi kita, yang terpenting adalah dapat mengakomodasi semua kemungkinan nilai string :: find. Dan memang, jika kita menulis ulang contoh ini dengan auto, kesalahannya hilang:
string str = .....;
auto n = str.find("ABC");
if (n != string::npos)
Tapi tidak semuanya sesederhana itu. Menggunakan auto bukanlah obat mujarab, dan ada banyak jebakan yang terkait dengan penggunaannya. Misalnya, Anda bisa menulis kode seperti ini:
auto n = 1024 * 1024 * 1024 * 5;
char* buf = new char[n]; 
 
Auto tidak akan menyelamatkan kita dari integer overflow dan akan ada sedikit memori yang dialokasikan untuk buffer daripada 5GiB.

Auto juga tidak banyak membantu ketika sampai pada kesalahan yang sangat umum: sebuah loop yang ditulis dengan tidak benar. Mari kita lihat sebuah contoh:
std::vector<int> bigVector;
for (unsigned i = 0; i < bigVector.size(); ++i)
{ ... }
Untuk array ukuran besar, loop ini menjadi loop tak terhingga. Tidak mengherankan jika ada kesalahan dalam kode tersebut: mereka mengungkapkan diri mereka dalam kasus yang sangat jarang, yang mana tidak ada tes.

Bisakah kita menulis ulang fragmen ini dengan auto?
std::vector<int> bigVector;
for (auto i = 0; i < bigVector.size(); ++i)
{ ... }
 Tidak. ini bukan hanya kesalahannya ada disini. Hal ini telah menjadi lebih buruk lagi.

Dengan tipe sederhana otomatis berperilaku sangat buruk. Ya, dalam kasus yang paling sederhana (otomatis x = y) ia bekerja, tapi begitu ada konstruksi tambahan, perilaku itu bisa menjadi lebih tidak dapat diprediksi. Yang lebih parah lagi, kesalahan akan lebih sulit diperhatikan, karena jenis variabelnya tidak sekilas. Untungnya itu bukan masalah bagi penganalisis statis: mereka tidak bosan, dan tidak kehilangan perhatian. Tapi bagi kita, sebagai manusia sederhana lebih baik menentukan jenisnya secara eksplisit. Kita juga bisa menyingkirkan casting penyempitan dengan menggunakan metode lain, tapi kita akan membicarakannya nanti.

Berbahaya jumlah (Count of)

Salah satu tipe "berbahaya" di C ++ adalah array. Seringkali saat mengirimkannya ke fungsi, pemrogram lupa bahwa itu dilewatkan sebagai pointer, dan coba hitung jumlah elemen dengan ukuran.
#define RTL_NUMBER_OF_V1(A) (sizeof(A)/sizeof((A)[0]))
#define _ARRAYSIZE(A) RTL_NUMBER_OF_V1(A)

int GetAllNeighbors( const CCoreDispInfo *pDisp,
                     int iNeighbors[512] ) {
  ....
  if ( nNeighbors < _ARRAYSIZE( iNeighbors ) )
    iNeighbors[nNeighbors++] = pCorner->m_Neighbors[i];
  ....
}
Catatan: Kode ini diambil dari Source Engine SDK.

Peringatan PVS-Studio: V511 Ukuran operator () mengembalikan ukuran penunjuk, dan bukan dari array, dalam ekspresi 'ukuran (iNeighbors)'. Vrad_dll disp_vrad.cpp 60

Kebingungan seperti itu bisa timbul karena menentukan ukuran sebuah array dalam argumen: nomor ini tidak berarti kompiler, dan hanya merupakan petunjuk bagi programmer.

Masalahnya adalah kode ini dikompilasi, dan programmer tidak menyadari ada yang tidak beres. Solusi yang jelas adalah menggunakan metaprogramming:
template < class T, size_t N ><br>constexpr size_t countof( const T (&array)[N] ) {
  return N;
}
countof(iNeighbors); //compile-time error
Jika kita lolos ke fungsi ini, bukan array, kita mendapatkan error kompilasi. Di C ++ 17 Anda bisa menggunakan std :: size.

Di C ++ 11, fungsi std :: extent ditambahkan, tapi tidak sesuai hitungan, karena menghasilkan 0 untuk jenis yang tidak tepat.
std::extent<decltype(iNeighbors)>(); //=> 0 
Anda bisa membuat kesalahan tidak hanya dengan hitungan saja, tapi juga dengan ukuran yang pas.
VisitedLinkMaster::TableBuilder::TableBuilder(
    VisitedLinkMaster* master,
    const uint8 salt[LINK_SALT_LENGTH])
    : master_(master),
      success_(true) {
  fingerprints_.reserve(4096);
  memcpy(salt_, salt, sizeof(salt));
  1. V511 Operator sizeof () mengembalikan ukuran pointer, dan bukan dari array, dalam ekspresi 'sizeof (salt)'. Browser visitedlink_master.cc 968
  2. V512 Panggilan fungsi 'memcpy' akan menyebabkan underflow buffer 'salt_'. Browser visitedlink_master.cc 968 
Seperti yang Anda lihat, standar C ++ array memiliki banyak masalah. Inilah sebabnya mengapa Anda harus menggunakan std :: array: di C ++ modern APInya mirip dengan std :: vector dan kontainer lainnya, dan lebih sulit untuk membuat kesalahan saat menggunakannya.
void Foo(std::array<uint8, 16> array)
{
  array.size(); //=> 16
}
Bagaimana membuat kesalahan secara sederhana

Satu lagi sumber kesalahan adalah sederhana untuk loop. Anda mungkin berpikir, "Di mana Anda bisa membuat kesalahan di sana? Apakah ada sesuatu yang berhubungan dengan kondisi keluar yang rumit atau menghemat jalur kode?" Tidak, pemrogram membuat kesalahan dalam loop yang paling sederhana. Mari kita lihat fragmen dari project:
Const int SerialWindow::kBaudrates[] = { 50, 75, 110, .... };

SerialWindow::SerialWindow() : ....
{
  ....
  for(int i = sizeof(kBaudrates) / sizeof(char*); --i >= 0;)
  {
    message->AddInt32("baudrate", kBaudrateConstants[i]);
    ....
  }
}
Catatan: Kode ini diambil dari Haiku Operation System.

Peringatan PVS-Studio: V706 Divisi mencurigakan: sizeof (kBaudrates) / sizeof (char *). Ukuran setiap elemen dalam array 'kBaudrates' tidak sama dengan pembagi. SerialWindow.cpp 162

Kami telah memeriksa kesalahan tersebut secara rinci di bab sebelumnya: ukuran array tidak dievaluasi dengan benar lagi. Kita dapat dengan mudah memperbaikinya dengan menggunakan std :: size:
const int SerialWindow::kBaudrates[] = { 50, 75, 110, .... };

SerialWindow::SerialWindow() : ....
{
  ....
  for(int i = std::size(kBaudrates); --i >= 0;) {
    message->AddInt32("baudrate", kBaudrateConstants[i]);
    ....
  }
}
Tapi ada cara yang lebih baik. Mari kita lihat satu fragmen lagi.
inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(
  const TCHAR* pChars, size_t nNumChars)
{
  if (nNumChars > 0)
  {
    for (size_t nCharPos = nNumChars - 1;
         nCharPos >= 0;
         --nCharPos)
      UnsafePutCharBack(pChars[nCharPos]);
  }
}
Catatan: Kode ini diambil dari Shareaza.

Peringatan PVS-Studio: V547 Expression 'nCharPos> = 0' selalu benar. Nilai tipe yang tidak dicantumkan selalu> = 0. BugTrap xmlreader.h 946

Ini adalah kesalahan khas saat menulis sebuah loop terbalik: programmer lupa bahwa iterator tipe unsigned dan cek selalu kembali benar. Anda mungkin berpikir, "Kenapa hanya siswa dan siswa yang membuat kesalahan seperti itu? Kami, profesional, tidak." Sayangnya, ini tidak sepenuhnya benar. Tentu saja, semua orang mengerti itu (unsigned> = 0) - benar. Dari mana kesalahan itu berasal? Mereka sering terjadi akibat refactoring. Bayangkan situasi ini: project bermigrasi dari platform 32-bit menjadi 64-bit. Sebelumnya, int / unsigned digunakan untuk pengindeksan dan keputusan dibuat untuk menggantikannya dengan size_t / ptrdiff_t. Tapi dalam satu fragmen mereka secara tidak sengaja menggunakan tipe unsigned, bukan yang ditandatangani.

Apa yang harus kita lakukan untuk menghindari situasi ini dalam kode Anda? Beberapa orang menyarankan penggunaan tipe yang ditandatangani, seperti pada C # atau Qt. Mungkin, ini bisa menjadi jalan keluar, tapi jika kita ingin bekerja dengan data dalam jumlah besar, maka tidak ada cara untuk menghindari size_t.


Apakah ada cara yang lebih aman untuk iterate melalui array di C + +? Tentu saja ada. Mari kita mulai dengan yang paling sederhana: fungsi non-anggota. Ada fungsi standar untuk bekerja dengan koleksi, array dan initializer_list; Prinsip mereka harus akrab bagi Anda.

char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it = rbegin(buf);
     it != rend(buf);
     ++it) {
  std::cout << *it;
}
Hebatnya, sekarang kita tidak perlu mengingat perbedaan antara siklus langsung dan sebaliknya. Ada juga tidak perlu memikirkan apakah kita menggunakan array sederhana atau array - loop akan bekerja dalam hal apapun. Menggunakan iterator adalah cara yang bagus untuk menghindari sakit kepala, tapi itu pun tidak selalu cukup baik. Cara terbaik adalah menggunakan range-based for loop:
char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it : buf) {
  std::cout << it;
}
 
Tentu saja, ada beberapa kelemahan dalam rentang berbasis ini: tidak memungkinkan manajemen loop yang fleksibel, dan jika ada pekerjaan yang lebih kompleks dengan indeks yang dibutuhkan, maka tidak akan banyak membantu kita. Tapi situasi seperti itu harus diperiksa secara terpisah. Kita memiliki situasi yang sederhana: kita harus bergerak sepanjang item dalam urutan terbalik. Namun, pada tahap ini, sudah ada banyak kesulitan. Tidak ada kelas tambahan di perpustakaan standar untuk jangkauan berbasis. Mari kita lihat bagaimana penerapannya:
template <typename T>
struct reversed_wrapper {
  const T& _v;

  reversed_wrapper (const T& v) : _v(v) {}

  auto begin() -> decltype(rbegin(_v))
  {
    return rbegin(_v);
  }

  auto end() -> decltype(rend(_v))
  {
    return rend(_v);
  }
};

template <typename T>
reversed_wrapper<T> reversed(const T& v)
{
  return reversed_wrapper<T>(v);
}
Dalam C + + 14 Anda dapat menyederhanakan kode dengan menghapus decltype. Anda dapat melihat bagaimana auto membantu Anda menulis fungsi template - reversed_wrapper akan bekerja baik dengan array, dan std :: vector.

Sekarang kita bisa menulis ulang fragmennya sebagai berikut:
char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it : reversed(buf)) {
  std::cout << it;
}
Apa kelebihan dari kode ini?  
Pertama, sangat mudah dibaca. Kita segera melihat bahwa susunan elemen berada dalam urutan terbalik. Kedua, lebih sulit membuat kesalahan.  
Dan ketiga, ia bekerja dengan tipe apapun. Ini jauh lebih baik dari apa adanya.

Anda bisa menggunakan boost :: adapter :: reverse (arr) dalam boost.

Tapi mari kita kembali ke contoh aslinya. Di sana, array dilewatkan oleh sepasang pointer-size. Jelas bahwa ide kita dengan terbalik tidak akan bekerja untuk itu. Apa yang harus kita lakukan? Gunakan kelas seperti span / array_view. Di C ++ 17 kita memiliki string_view, dan saya sarankan untuk menggunakannya:


void Foo(std::string_view s);
std::string str = "abc";
Foo(std::string_view("
abc", 3));
Foo("
abc");
Foo(str); 
String_view tidak memiliki string, sebenarnya, ini adalah pembungkus di sekitar const char * dan panjangnya. Itu sebabnya dalam contoh kode, string dilewatkan nilai, bukan oleh referensi. Fitur utama dari string_view adalah kompatibilitas dengan string dalam berbagai presentasi string: const char *, std :: string dan non-null diakhiri const char *.

Akibatnya, fungsinya mengambil bentuk sebagai berikut:

inline void CXmlReader::CXmlInputStream::UnsafePutCharsBack(
  std::wstring_view chars)
{
  for (wchar_t ch : reversed(chars))
    UnsafePutCharBack(ch);
}
 
Melewati fungsinya, penting untuk diingat bahwa constructor string_view (const char *) tersirat, oleh karena itu kita bisa menulis seperti ini:
Foo(pChars);
Tidak/bukan seperti ini:
Foo(wstring_view(pChars, nNumChars)); 
String yang ditunjukkan oleh string_view, tidak perlu dihentikan null, nama string_view :: data memberi kami petunjuk tentang ini, dan perlu diingat hal itu saat menggunakannya. Ketika melewati nilainya ke fungsi dari cstdlib, yang menunggu string C, Anda bisa mendapatkan perilaku yang tidak terdefinisi. Anda dapat dengan mudah melewatkannya, jika dalam kebanyakan kasus yang Anda uji, ada string std :: string atau null-endedinated yang digunakan.

Enum

Mari kita tinggalkan C ++ sebentar dan pikirkan lama C. Bagaimana keamanan di sana? Lagi pula, tidak ada masalah dengan panggilan dan operator konstruktor implisit, atau jenis konversi, dan tidak ada masalah dengan berbagai jenis string. Dalam prakteknya, kesalahan sering terjadi pada konstruksi yang paling sederhana: yang paling rumit ditinjau ulang dan di-debugged secara menyeluruh, karena ini menyebabkan beberapa keraguan. Pada saat yang sama programmer lupa untuk memeriksa konstruksi sederhana. Berikut adalah contoh struktur berbahaya, yang datang kepada kita dari C:
enum iscsi_param {
  ....
  ISCSI_PARAM_CONN_PORT,
  ISCSI_PARAM_CONN_ADDRESS,
  ....
};
 
enum iscsi_host_param {
  ....
  ISCSI_HOST_PARAM_IPADDRESS,
  ....
};
int iscsi_conn_get_addr_param(....,
  
enum iscsi_param param, ....)
{
  ....
  switch (param) {
  case ISCSI_PARAM_CONN_ADDRESS:
  case ISCSI_HOST_PARAM_IPADDRESS:
  ....
  }

  return len;
}
Contoh dari kernel Linux. Peringatan PVS-Studio: V556 Nilai tipe enum yang berbeda dibandingkan: switch (ENUM_TYPE_A) {case ENUM_TYPE_B: ...}. Libiscsi.c 3501

Perhatikan nilai pada kotak switch: salah satu konstanta yang dinamai diambil dari enumerasi yang berbeda. Tentu saja, ada banyak kode dan nilai yang lebih mungkin dan kesalahannya tidak begitu jelas.

Alasan untuk itu adalah mengetik sedikit enum - mereka mungkin secara implisit casting ke int, dan ini meninggalkan banyak ruang untuk kesalahan.Dalam C + + 11 Anda dapat, dan harus, menggunakan kelas enum: trik seperti itu tidak akan bekerja di sana, dan kesalahan akan muncul pada tahap kompilasi. Akibatnya, kode berikut tidak dikompilasi, itulah yang kita butuhkan:
enum class ISCSI_PARAM {
  ....
  CONN_PORT,
  CONN_ADDRESS,
  ....
};

enum class ISCSI_HOST {
  ....
  PARAM_IPADDRESS,
  ....
};
int iscsi_conn_get_addr_param(....,
 ISCSI_PARAM param, ....)
{
  ....
  switch (param) {
  case ISCSI_PARAM::CONN_ADDRESS:
  
case ISCSI_HOST::PARAM_IPADDRESS:
  ....
  }

  return len;
}
Fragmen berikut tidak cukup terhubung dengan enum, namun memiliki gejala yang serupa:
void adns__querysend_tcp(....) {
  ...
  if (!(errno == EAGAIN || EWOULDBLOCK ||
        errno == EINTR || errno == ENOSPC ||
        errno == ENOBUFS || errno == ENOMEM)) {
  ...
}
Catatan: Kode ini diambil dari ReactOS.

Ya, nilai errno dinyatakan sebagai makro, yang merupakan praktik buruk di C ++ (di C juga), namun biarpun pemrogram menggunakan enum, itu tidak akan membuat hidup lebih mudah. Perbandingan yang hilang tidak akan terungkap dalam kasus enum (dan terutama jika terjadi makro). Pada saat yang sama kelas enum tidak mengizinkan hal ini, karena tidak akan ada casting implisit dari bool.

Inisialisasi dalam constructor

Tapi kembali ke masalah C ++ asli. Salah satunya mengungkapkan bila ada kebutuhan untuk menginisialisasi objek dengan cara yang sama pada beberapa konstruktor. Situasi sederhana: ada kelas, dua konstruktor, satu di antaranya memanggil yang lain. Semuanya terlihat cukup logis: kode umum dimasukkan ke metode terpisah - tidak ada yang suka menduplikat kode. Apa perangkapnya?
Guess::Guess() {
  language_str = DEFAULT_LANGUAGE;
  country_str = DEFAULT_COUNTRY;
  encoding_str = DEFAULT_ENCODING;
}
Guess::Guess(const char * guess_str) {
  Guess();
  ....
}
Catatan: Kode ini diambil dari LibreOffice.

Peringatan PVS-Studio: V603 Objek dibuat tapi tidak digunakan. Jika Anda ingin menghubungi konstruktor, 'this-> Guess :: Guess (....)' harus digunakan. Tebak.cxx 56

Perangkap itu ada dalam sintaks panggilan konstruktor. Cukup sering dilupakan, dan pemrogram menciptakan satu kelas lagi, yang kemudian segera dihancurkan. Artinya, inisialisasi contoh asli tidak terjadi. Tentu saja ada 1001 cara untuk memperbaikinya. Sebagai contoh, kita dapat secara eksplisit memanggil konstruktor melalui ini, atau memasukkan semuanya ke fungsi yang terpisah:
Guess::Guess(const char * guess_str)
{
  this->Guess();
  ....
}

Guess::Guess(const char * guess_str)
{
  Init();
  ....
}
Ngomong-ngomong, panggilan konstruktor yang berulang secara eksplisit, misalnya, melalui permainan ini adalah permainan yang berbahaya, dan kita perlu memahami apa yang sedang terjadi. Varian dengan Init () jauh lebih baik dan lebih jelas.

Tapi yang terbaik adalah menggunakan delegasi konstruktor di sini. Jadi kita bisa secara eksplisit memanggil satu konstruktor dari yang lain dengan cara berikut:
Guess::Guess(const char * guess_str) : Guess()
{
  ....
}
Konstruktor semacam itu memiliki beberapa keterbatasan. 
Pertama: konstruktor yang didelegasikan bertanggung jawab penuh atas inisialisasi suatu objek. Artinya, tidak mungkin menginisialisasi bidang kelas lain dengannya dalam daftar inisialisasi:
Guess::Guess(const char * guess_str)
  : Guess(),         
    m_member(42)
{
  ....
}
 
Dan tentu saja, kita harus memastikan bahwa delegasi tidak menciptakan sebuah lingkaran, karena tidak mungkin untuk keluar dari situ. Sayangnya, kode ini dikompilasi:
Guess::Guess(const char * guess_str)
  : Guess(std::string(guess_str))
{
  ....
}

Guess::Guess(std::string guess_str)
  : Guess(guess_str.c_str())
{
  ....
}
 
Tentang fungsi virtual

Fungsi virtual menghambat masalah potensial: masalahnya adalah sangat mudah membuat kesalahan pada tanda tangan kelas turunan dan akibatnya tidak mengesampingkan sebuah fungsi, namun untuk menyatakan sebuah masalah yang baru. Mari kita lihat situasi seperti ini dalam contoh berikut:

class Base {
  virtual void Foo(int x);
}
class Derived : public class Base {
  void Foo(int x, int a = 1);
}
Metode Derived :: Foo tidak mungkin dipanggil oleh pointer / referensi ke Base. Tapi ini adalah contoh sederhana, dan Anda mungkin mengatakan bahwa tidak ada yang membuat kesalahan seperti itu. Biasanya orang membuat kesalahan dengan cara berikut:

Catatan: Kode ini diambil dari MongoDB.
class DBClientBase : .... {
public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    int nToReturn = 0
    int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    int queryOptions = 0,
    
int batchSize = 0 );
};
class DBDirectClient : public DBClientBase {
public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    
int nToReturn = 0,
    
int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    
int queryOptions = 0);
};
Peringatan PVS-Studio: V762 Pertimbangkan untuk memeriksa argumen fungsi virtual. Lihat argumen ketujuh fungsi 'query' di kelas turunan 'DBDirectClient', dan kelas dasar 'DBClientBase'. Dbdirectclient.cpp 61

Ada banyak argumen dan tidak ada argumen terakhir dalam fungsi kelas pewaris. Ini berbeda, fungsinya tidak terhubung. Sering terjadi kesalahan seperti itu dengan argumen yang memiliki nilai default.

Dalam fragmen berikutnya situasinya sedikit lebih rumit. Kode ini akan bekerja jika dikompilasi sebagai kode 32-bit, namun tidak akan berfungsi dalam versi 64-bit. Awalnya, di kelas dasar, parameternya adalah tipe DWORD, namun kemudian dikoreksi ke DWORD_PTR. Pada saat yang sama itu tidak berubah di kelas warisan. Hidupkan malam tanpa tidur, debugging, dan kopi!
class CWnd : public CCmdTarget {
  ....
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd = HELP_CONTEXT);
  ....
};
class CFrameWnd : public CWnd { .... };
class CFrameWndEx : public CFrameWnd {
  ....
  virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
  ....
};
 Anda bisa membuat kesalahan dalam tanda tangan dengan cara yang lebih boros. Anda bisa melupakan const dari fungsi, atau sebuah argumen. Anda bisa lupa bahwa fungsi di kelas dasar tidak virtual. Anda dapat membingungkan tipe yang ditandatangani / tidak bertanda tangan.

Di C ++ beberapa kata kunci ditambahkan yang bisa mengatur override fungsi virtual. Override akan sangat membantu. Kode ini tidak akan dikompilasi.
class DBDirectClient : public DBClientBase {public:
  virtual auto_ptr<DBClientCursor> query(
    const string &ns,
    Query query,
    int nToReturn = 0,
    int nToSkip = 0,
    const BSONObj *fieldsToReturn = 0,
    int queryOptions = 0) override;
};
NULL vs nullptr
Menggunakan NULL untuk menunjukkan pointer nol mengarah ke sejumlah situasi tak terduga. Masalahnya adalah bahwa NULL adalah makro normal yang berkembang pada 0 yang memiliki tipe int: Itu sebabnya tidak sulit untuk memahami mengapa fungsi kedua dipilih dalam contoh ini:
void Foo(int x, int y, const char *name);
void Foo(int x, 
int y, int ResourceID);
Foo(1, 2, NULL);
Meski alasannya jelas, sangat tidak masuk akal. Inilah sebabnya mengapa ada kebutuhan dalam nullptr yang memiliki tipe nullptr_t sendiri. Inilah sebabnya mengapa kita tidak dapat menggunakan NULL (dan lebih dari itu 0) di C ++ modern.

Contoh lain: NULL dapat digunakan untuk membandingkan dengan tipe integer lainnya. Anggap saja ada beberapa fungsi WinAPI yang mengembalikan HRESULT. Tipe ini tidak berhubungan dengan pointer dengan cara apapun, jadi perbandingannya dengan NULL tidak ada artinya. Dan nullptr menggarisbawahi hal ini dengan mengeluarkan kesalahan kompilasi, pada saat bersamaan NULL bekerja:
if (WinApiFoo(a, b) != NULL)    // That's badif (WinApiFoo(a, b) != nullptr// Hooray,
                                // a compilation error
Va_arg
Ada kasus di mana perlu melewatkan sejumlah argumen yang tidak terdefinisi. Contoh tipikal - fungsi input / ouput yang diformat. Ya, bisa ditulis sedemikian rupa sehingga sejumlah argumen tidak akan dibutuhkan, tapi saya tidak melihat alasan untuk meninggalkan sintaks ini karena jauh lebih mudah dan mudah dibaca. Apa yang ditawarkan standar C ++ tua? Mereka menyarankan menggunakan va_list. Masalah apa yang kita hadapi dengan itu? Tidak mudah untuk melewati argumen tipe yang salah dengan argumen semacam itu. Atau jangan sampai melewatkan argumen apapun. Mari kita lihat lebih dekat fragmennya.
typedef std::wstring string16;
const base::string16& relaunch_flags() 
const;

int RelaunchChrome(
const DelegateExecuteOperation& operation)
{
  AtlTrace("Relaunching [%ls] with flags [%s]\n",
           operation.mutex().c_str(),
           operation.relaunch_flags());
  ....
}
Catatan: Kode ini diambil dari Chromium.

Peringatan PVS-Studio: V510 Fungsi 'AtlTrace' tidak diharapkan menerima variabel tipe kelas sebagai argumen aktual ketiga. Delegate_execute.cc 96

Pemrogram ingin mencetak std :: wstring string, tapi lupa memanggil metode c_str (). Jadi tipe wstring akan diinterpretasikan dalam fungsi sebagai const wchar_t *. Tentu saja, ini tidak akan ada gunanya.
cairo_status_t
_cairo_win32_print_gdi_error (const char *context)
{
  ....
  fwprintf (stderr, L"%s: %S", context,
            (wchar_t *)lpMsgBuf);
  ....
}

Dalam fragmen ini, programmer membingungkan penspesifikasi format string. Masalahnya adalah bahwa dalam Visual C ++ wchar_t *, dan% S - char *, menunggu wprintf% s. Ini menarik, bahwa kesalahan ini ada dalam string yang dimaksudkan untuk keluaran kesalahan atau informasi debug - tentunya ini adalah kasus yang jarang terjadi, oleh karena itu mereka dilewati.
static void GetNameForFile(
  const char* baseFileName,
  const uint32 fileIdx,
  char outputName[512] )
{
  assert(baseFileName != NULL);
  sprintf( outputName, "%s_%d", baseFileName, fileIdx );
Catatan: Kode ini diambil dari SDK CryEngine 3.

Peringatan PVS-Studio: V576 Format salah. Pertimbangkan untuk memeriksa argumen aktual keempat dari fungsi 'sprintf'. Argumen jenis SIGNED integer diharapkan. Igame.h 66

Tipe integer juga sangat mudah membingungkan. Apalagi bila ukuran mereka tergantung pada platform. Namun, ini jauh lebih sederhana: tipe yang ditandatangani dan unsigned bingung. Angka besar akan dicetak sebagai yang negatif.

ReadAndDumpLargeSttb(cb,err)
  int     cb;
  
int     err;
{
  ....
  printf("\n - %d strings were read, "
         "%d were expected (decimal numbers) -\n");
  ....
}
 
Catatan: Kode ini diambil dari Word for Windows 1.1a.

Peringatan PVS-Studio: V576 Format salah. Sejumlah argumen sebenarnya diharapkan saat memanggil fungsi 'printf'. Diharapkan: 3. Hadir: 1. dini.c 498

Contoh ditemukan di bawah salah satu penelitian arkeologi. String ini mengandaikan tiga argumen, tapi tidak ditulis. Mungkin pemrogram bermaksud mencetak data di tumpukan, tapi kita tidak bisa membuat asumsi tentang apa yang ada di sana. Tentu, kita perlu menyampaikan argumen ini secara eksplisit.

BOOL CALLBACK EnumPickIconResourceProc(
  HMODULE hModule, LPCWSTR lpszType,
  LPWSTR lpszName, LONG_PTR lParam)
{
  ....
  swprintf(szName, L"%u", lpszName);
  ....
 
Catatan: Kode ini diambil dari ReactOS.

Peringatan PVS-Studio: V576 Format salah. Pertimbangkan untuk memeriksa argumen sebenarnya dari fungsi 'swprintf'. Untuk mencetak nilai pointer '% p' ​​harus digunakan. Dialogs.cpp 66

Contoh kesalahan 64-bit. Ukuran pointer bergantung pada arsitektur, dan menggunakan% u untuk itu adalah ide yang buruk. Apa yang harus kita gunakan sebagai gantinya? Alat analisa memberi kita petunjuk bahwa jawaban yang benar adalah% p. Ini bagus jika pointer dicetak untuk debugging. Akan jauh lebih menarik jika nanti ada usaha untuk membacanya dari buffer dan menggunakannya.

Apa yang salah dengan fungsi dengan sejumlah argumen? Hampir semuanya! Anda tidak dapat memeriksa jenis argumen, atau jumlah argumen. Langkah ke kiri, melangkahlah ke atas-perilaku yang tidak terdefinisi.

Sangat bagus bahwa ada alternatif yang lebih andal. Pertama, ada variadic template. Dengan bantuan mereka, kami mendapatkan semua informasi tentang jenis yang dilalui selama kompilasi, dan dapat menggunakannya sesuai keinginan. Sebagai contoh mari kita menggunakan printf yang sangat, tapi yang lebih aman:
void printf(const char* s) {
  std::cout << s;
}
template<typename T, 
typename... Args>void printf(const char* s, T value, Args... args) {
  while (s && *s) {
    if (*s=='%' && *++s!='%') {
      std::cout << value;
      return printf(++s, args...);
    }
    std::cout << *s++;
  }
}
Tentu ini hanya sebuah contoh: dalam praktek penggunaannya tidak ada gunanya. Tapi dalam kasus template variadic, Anda hanya dibatasi oleh imajinasi Anda, bukan oleh fitur bahasa.

Satu lagi konstruksi yang bisa dijadikan pilihan untuk melewati sejumlah variabel argumen - std :: initializer_list. Ini tidak memungkinkan Anda untuk melewatkan argumen dari berbagai jenis. Tapi jika ini sudah cukup, Anda bisa menggunakannya:

void Foo(std::initializer_list<int> a);
Foo({1, 2, 3, 4, 5});
 
Ini juga sangat mudah untuk dilalui, seperti yang bisa kita gunakan mulai, akhir, dan jangkauannya.

Apa hasilnya?


Modern C ++ menyediakan banyak alat yang membantu Anda menulis kode dengan lebih aman. Banyak konstruksi untuk evaluasi dan pengecekan kompilasi telah muncul. Anda dapat beralih ke model pengelolaan memori dan sumber daya yang lebih nyaman.

Tapi tidak ada teknik atau paradigma pemrograman yang bisa melindungi Anda sepenuhnya dari kesalahan. Bersama dengan fungsionalitasnya, C ++ juga mendapatkan bug baru, yang hanya aneh baginya. Inilah sebabnya mengapa kita tidak dapat hanya bergantung pada satu metode: kita harus selalu menggunakan kombinasi kode-review, kode kualitas, dan alat yang layak; Yang dapat membantu menghemat waktu dan minuman energi Anda, yang keduanya bisa digunakan dengan cara yang lebih baik.


Demikianlah pemahasan mengenai Cara menghindari bug menggunakan C ++ modern ini semoga bermanfaat dan akan menamahkan wawasan kita dalam belajar salah satu bahasa pemrograman yaitu C++ ini. Wassalam..

No comments:

Post a Comment