Saturday, February 18, 2017

√ Cara Menghindari Bug Memakai C ++ Modern

Tutorialankha.com | Salah satu problem utama dengan C ++ yaitu mempunyai sejumlah besar konstruksi yang perilakunya tidak terdefinisi, atau hanya tak terduga bagi programmer. Kami sering menemukan mereka ketika memakai penganalisis statis kami di banyak sekali proyek. Tapi, menyerupai kita semua tahu, yang terbaik yaitu mendeteksi kesalahan pada tahap kompilasi. Mari kita lihat teknik di C + + modern yang membantu menulis tidak hanya arahan yang sederhana dan jelas, namun membuatnya lebih kondusif dan lebih sanggup diandalkan.



Apa itu Modern C ++?

Istilah Modern C ++ menjadi sangat terkenal sehabis rilis C ++ 11. Apa artinya? Pertama-tama, Modern C ++ yaitu seperangkat pola dan idiom yang dirancang untuk menghilangkan kelemahan dari "C dengan kelas" usang yang baik, sehingga banyak programmer C ++ terbiasa, terutama bila mereka mulai memprogram di C. C ++ 11 Terlihat jauh lebih ringkas dan gampang dimengerti, yang sangat penting.

Apa yang orang biasanya pikirkan ketika mereka berbicara wacana 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 yaitu semua modernisasi yang sangat keren, dan kami berharap sanggup melihat mereka di set standar berikutnya. Namun, saya ingin menarik perhatian pada cara standar gres memungkinkan penulisan arahan yang lebih aman. Saat menyebarkan penganalisis statis, kita melihat sejumlah besar kesalahan yang bervariasi, dan terkadang kita tidak sanggup tidak berpikir: "Tapi di C ++ modern ini sanggup dihindari". Oleh lantaran itu, saya sarankan kita menyidik beberapa kesalahan yang ditemukan oleh PVS-Studio di banyak sekali proyek Open Source. Juga, kita akan melihat bagaimana mereka sanggup 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 gampang untuk mempersingkat jenis yang panjang, tanpa kehilangan pembacaan kode. Namun, kata kunci ini menjadi cukup ekspansif, bersama dengan template: tidak perlu memilih jenis nilai pengembalian dengan auto dan decltype.

Tapi mari kita kembali ke topik kita. Berikut yaitu teladan 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 sanggup ditunjukkan oleh variabel tipe unsigned. Bisa nampak bahwa ini yaitu perkara dimana auto sanggup menyelamatkan kita dari problem menyerupai ini: jenis dari n variabel tidak penting bagi kita, yang terpenting yaitu sanggup mengakomodasi semua kemungkinan nilai string :: find. Dan memang, bila kita menulis ulang teladan 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 sanggup menulis arahan menyerupai 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 hingga 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 bila ada kesalahan dalam arahan tersebut: mereka mengungkapkan diri mereka dalam perkara 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 jelek lagi.

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

Berbahaya jumlah (Count of)

Salah satu tipe "berbahaya" di C ++ yaitu array. Seringkali ketika 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 menyerupai itu sanggup timbul lantaran memilih ukuran sebuah array dalam argumen: nomor ini tidak berarti kompiler, dan hanya merupakan petunjuk bagi programmer.

Masalahnya yaitu arahan ini dikompilasi, dan programmer tidak menyadari ada yang tidak beres. Solusi yang terang yaitu memakai 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 sanggup memakai std :: size.

Di C ++ 11, fungsi std :: extent ditambahkan, tapi tidak sesuai hitungan, lantaran menghasilkan 0 untuk jenis yang tidak tepat.
std::extent<decltype(iNeighbors)>(); //=> 0 
Anda sanggup 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 mempunyai banyak masalah. Inilah sebabnya mengapa Anda harus memakai std :: array: di C ++ modern APInya menyerupai dengan std :: vector dan kontainer lainnya, dan lebih sulit untuk membuat kesalahan ketika menggunakannya.
void Foo(std::array<uint8, 16> array)
{
  array.size(); //=> 16
}
Bagaimana membuat kesalahan secara sederhana

Satu lagi sumber kesalahan yaitu sederhana untuk loop. Anda mungkin berpikir, "Di mana Anda sanggup membuat kesalahan di sana? Apakah ada sesuatu yang berafiliasi 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 menyidik kesalahan tersebut secara rinci di cuilan sebelumnya: ukuran array tidak dievaluasi dengan benar lagi. Kita sanggup dengan gampang memperbaikinya dengan memakai 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 yaitu kesalahan khas ketika 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 menyerupai 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 jawaban refactoring. Bayangkan situasi ini: project bermigrasi dari platform 32-bit menjadi 64-bit. Sebelumnya, int / unsigned dipakai untuk pengindeksan dan keputusan dibentuk untuk menggantikannya dengan size_t / ptrdiff_t. Tapi dalam satu fragmen mereka secara tidak sengaja memakai tipe unsigned, bukan yang ditandatangani.

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


Apakah ada cara yang lebih kondusif 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 dekat bagi Anda.

char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it = rbegin(buf);
     it != rend(buf);
     ++it) {
  std::cout << *it;
}
Hebatnya, kini kita tidak perlu mengingat perbedaan antara siklus eksklusif dan sebaliknya. Ada juga tidak perlu memikirkan apakah kita memakai array sederhana atau array - loop akan bekerja dalam hal apapun. Menggunakan iterator yaitu cara yang elok untuk menghindari sakit kepala, tapi itu pun tidak selalu cukup baik. Cara terbaik yaitu memakai 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 administrasi loop yang fleksibel, dan bila ada pekerjaan yang lebih kompleks dengan indeks yang dibutuhkan, maka tidak akan banyak membantu kita. Tapi situasi menyerupai itu harus diperiksa secara terpisah. Kita mempunyai situasi yang sederhana: kita harus bergerak sepanjang item dalam urutan terbalik. Namun, pada tahap ini, sudah ada banyak kesulitan. Tidak ada kelas komplemen 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 sanggup menyederhanakan arahan dengan menghapus decltype. Anda sanggup melihat bagaimana auto membantu Anda menulis fungsi template - reversed_wrapper akan bekerja baik dengan array, dan std :: vector.

Sekarang kita sanggup menulis ulang fragmennya sebagai berikut:
char buf[4] = { 'a', 'b', 'c', 'd' };
for (auto it : reversed(buf)) {
  std::cout << it;
}
Apa kelebihan dari arahan ini?  
Pertama, sangat gampang 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 sanggup memakai boost :: adapter :: reverse (arr) dalam boost.

Tapi mari kita kembali ke teladan aslinya. Di sana, array dilewatkan oleh sepasang pointer-size. Jelas bahwa wangsit kita dengan terbalik tidak akan bekerja untuk itu. Apa yang harus kita lakukan? Gunakan kelas menyerupai span / array_view. Di C ++ 17 kita mempunyai 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 mempunyai string, sebenarnya, ini yaitu pembungkus di sekitar const char * dan panjangnya. Itu sebabnya dalam teladan kode, string dilewatkan nilai, bukan oleh referensi. Fitur utama dari string_view yaitu kompatibilitas dengan string dalam banyak sekali 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 lantaran itu kita sanggup menulis menyerupai ini:
Foo(pChars);
Tidak/bukan menyerupai ini:
Foo(wstring_view(pChars, nNumChars)); 
String yang ditunjukkan oleh string_view, tidak perlu tidak boleh null, nama string_view :: data memberi kami petunjuk wacana ini, dan perlu diingat hal itu ketika menggunakannya. Ketika melewati nilainya ke fungsi dari cstdlib, yang menunggu string C, Anda sanggup mendapatkan sikap yang tidak terdefinisi. Anda sanggup dengan gampang melewatkannya, bila dalam kebanyakan perkara yang Anda uji, ada string std :: string atau null-endedinated yang digunakan.

Enum

Mari kita tinggalkan C ++ sebentar dan pikirkan usang C. Bagaimana keamanan di sana? Lagi pula, tidak ada problem dengan panggilan dan operator konstruktor implisit, atau jenis konversi, dan tidak ada problem dengan banyak sekali jenis string. Dalam prakteknya, kesalahan sering terjadi pada konstruksi yang paling sederhana: yang paling rumit ditinjau ulang dan di-debugged secara menyeluruh, lantaran ini menyebabkan beberapa keraguan. Pada ketika yang sama programmer lupa untuk menyidik konstruksi sederhana. Berikut yaitu teladan struktur berbahaya, yang tiba 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 arahan dan nilai yang lebih mungkin dan kesalahannya tidak begitu jelas.

Alasan untuk itu yaitu mengetik sedikit enum - mereka mungkin secara implisit casting ke int, dan ini meninggalkan banyak ruang untuk kesalahan.Dalam C + + 11 Anda dapat, dan harus, memakai kelas enum: trik menyerupai itu tidak akan bekerja di sana, dan kesalahan akan muncul pada tahap kompilasi. Akibatnya, arahan 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 mempunyai tanda-tanda 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 jelek di C ++ (di C juga), namun biarpun pemrogram memakai enum, itu tidak akan membuat hidup lebih mudah. Perbandingan yang hilang tidak akan terungkap dalam perkara enum (dan terutama bila terjadi makro). Pada ketika yang sama kelas enum tidak mengizinkan hal ini, lantaran tidak akan ada casting implisit dari bool.

Inisialisasi dalam constructor

Tapi kembali ke problem 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: arahan 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 dibentuk 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 membuat satu kelas lagi, yang kemudian segera dihancurkan. Artinya, inisialisasi teladan orisinil tidak terjadi. Tentu saja ada 1001 cara untuk memperbaikinya. Sebagai contoh, kita sanggup 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 yaitu permainan yang berbahaya, dan kita perlu memahami apa yang sedang terjadi. Varian dengan Init () jauh lebih baik dan lebih jelas.

Tapi yang terbaik yaitu memakai delegasi konstruktor di sini. Makara kita sanggup secara eksplisit memanggil satu konstruktor dari yang lain dengan cara berikut:
Guess::Guess(const char * guess_str) : Guess()
{
  ....
}
Konstruktor semacam itu mempunyai beberapa keterbatasan. 
Pertama: konstruktor yang didelegasikan bertanggung jawab penuh atas inisialisasi suatu objek. Artinya, mustahil 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 membuat sebuah lingkaran, lantaran mustahil untuk keluar dari situ. Sayangnya, arahan 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 problem potensial: masalahnya yaitu sangat gampang membuat kesalahan pada tanda tangan kelas turunan dan alhasil tidak mengesampingkan sebuah fungsi, namun untuk menyatakan sebuah problem yang baru. Mari kita lihat situasi menyerupai ini dalam teladan berikut:

class Base {
  virtual void Foo(int x);
}
class Derived : public class Base {
  void Foo(int x, int a = 1);
}
Metode Derived :: Foo mustahil dipanggil oleh pointer / tumpuan ke Base. Tapi ini yaitu teladan sederhana, dan Anda mungkin menyampaikan bahwa tidak ada yang membuat kesalahan menyerupai 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 menyidik 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 menyerupai itu dengan argumen yang mempunyai nilai default.

Dalam fragmen berikutnya situasinya sedikit lebih rumit. Kode ini akan bekerja bila dikompilasi sebagai arahan 32-bit, namun tidak akan berfungsi dalam versi 64-bit. Awalnya, di kelas dasar, parameternya yaitu tipe DWORD, namun kemudian dikoreksi ke DWORD_PTR. Pada ketika 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 sanggup membuat kesalahan dalam tanda tangan dengan cara yang lebih boros. Anda sanggup melupakan const dari fungsi, atau sebuah argumen. Anda sanggup lupa bahwa fungsi di kelas dasar tidak virtual. Anda sanggup membingungkan tipe yang ditandatangani / tidak bertanda tangan.

Di C ++ beberapa kata kunci ditambahkan yang sanggup 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 memberikan pointer nol mengarah ke sejumlah situasi tak terduga. Masalahnya yaitu bahwa NULL yaitu makro normal yang berkembang pada 0 yang mempunyai tipe int: Itu sebabnya tidak sulit untuk memahami mengapa fungsi kedua dipilih dalam teladan 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 mempunyai tipe nullptr_t sendiri. Inilah sebabnya mengapa kita tidak sanggup memakai NULL (dan lebih dari itu 0) di C ++ modern.

Contoh lain: NULL sanggup dipakai untuk membandingkan dengan tipe integer lainnya. Anggap saja ada beberapa fungsi WinAPI yang mengembalikan HRESULT. Tipe ini tidak berafiliasi dengan pointer dengan cara apapun, jadi perbandingannya dengan NULL tidak ada artinya. Dan nullptr menggarisbawahi hal ini dengan mengeluarkan kesalahan kompilasi, pada ketika bersamaan NULL bekerja:
if (WinApiFoo(a, b) != NULL)    // That's badif (WinApiFoo(a, b) != nullptr// Hooray,
                                // a compilation error
Va_arg
Ada perkara di mana perlu melewatkan sejumlah argumen yang tidak terdefinisi. Contoh tipikal - fungsi input / ouput yang diformat. Ya, sanggup ditulis sedemikian rupa sehingga sejumlah argumen tidak akan dibutuhkan, tapi saya tidak melihat alasan untuk meninggalkan sintaks ini lantaran jauh lebih gampang dan gampang dibaca. Apa yang ditawarkan standar C ++ tua? Mereka menyarankan memakai va_list. Masalah apa yang kita hadapi dengan itu? Tidak gampang untuk melewati argumen tipe yang salah dengan argumen semacam itu. Atau jangan hingga 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 dibutuhkan mendapatkan variabel tipe kelas sebagai argumen kasatmata ketiga. Delegate_execute.cc 96

Pemrogram ingin mencetak std :: wstring string, tapi lupa memanggil metode c_str (). Makara 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 yaitu 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 yaitu perkara yang jarang terjadi, oleh lantaran 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 menyidik argumen kasatmata keempat dari fungsi 'sprintf'. Argumen jenis SIGNED integer diharapkan. Igame.h 66

Tipe integer juga sangat gampang 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 bahwasanya dibutuhkan ketika 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 sanggup membuat perkiraan wacana apa yang ada di sana. Tentu, kita perlu memberikan 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 menyidik argumen bahwasanya 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 yaitu wangsit yang buruk. Apa yang harus kita gunakan sebagai gantinya? Alat analisa memberi kita petunjuk bahwa jawaban yang benar adalah% p. Ini elok bila pointer dicetak untuk debugging. Akan jauh lebih menarik bila nanti ada perjuangan untuk membacanya dari buffer dan menggunakannya.

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

Sangat elok bahwa ada alternatif yang lebih andal. Pertama, ada variadic template. Dengan santunan mereka, kami mendapatkan semua informasi wacana jenis yang dilalui selama kompilasi, dan sanggup menggunakannya sesuai keinginan. Sebagai teladan mari kita memakai 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 perkara template variadic, Anda hanya dibatasi oleh imajinasi Anda, bukan oleh fitur bahasa.

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

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

Apa hasilnya?


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

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


Demikianlah pemahasan mengenai Cara menghindari bug memakai C ++ modern ini semoga bermanfaat dan akan menamahkan wawasan kita dalam mencar ilmu salah satu bahasa pemrograman yaitu C++ ini. Wassalam..
Sumber http://www.tutorialankha.com