Apakah mungkin untuk menulis terlalu banyak penegasan?

33

Saya penggemar menulis assertcek dalam kode C ++ sebagai cara untuk menangkap kasus selama pengembangan yang tidak mungkin terjadi tetapi bisa terjadi karena bug logika dalam program saya. Ini adalah praktik yang baik secara umum.

Namun, saya perhatikan bahwa beberapa fungsi yang saya tulis (yang merupakan bagian dari kelas yang kompleks) memiliki 5+ penegasan yang terasa seperti berpotensi menjadi praktik pemrograman yang buruk, dalam hal keterbacaan dan pemeliharaan. Saya pikir itu masih bagus, karena masing-masing mengharuskan saya untuk berpikir tentang pra-dan pasca-kondisi fungsi dan mereka benar-benar membantu menangkap bug. Namun, saya hanya ingin meletakkan ini di luar sana untuk bertanya apakah ada paradigma yang lebih baik untuk menangkap kesalahan logika dalam kasus-kasus ketika sejumlah besar pemeriksaan diperlukan.

Komentar Emacs : Karena Emacs adalah IDE pilihan saya, saya memilikinya sedikit mengeluarkan pernyataan tegas yang membantu mengurangi perasaan kekacauan yang mereka dapat berikan. Inilah yang saya tambahkan ke file .emacs saya:

; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))

; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
  (lambda () (font-lock-add-keywords nil
    '(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))
Alan Turing
sumber
3
Saya harus mengakui bahwa ini adalah pertanyaan yang terlintas di benak saya sekarang dan lagi. Tertarik mendengar pendapat orang lain tentang ini.
Kapten Sensible

Jawaban:

45

Saya telah melihat ratusan bug yang akan dipecahkan lebih cepat jika seseorang telah menulis lebih banyak penegasan, dan tidak satu pun yang akan dipecahkan lebih cepat dengan menulis lebih sedikit .

[C] ould [terlalu banyak menegaskan] berpotensi menjadi praktik pemrograman yang buruk, dalam hal keterbacaan dan pemeliharaan [?]

Keterbacaan bisa menjadi masalah, mungkin - meskipun pengalaman saya bahwa orang yang menulis pernyataan yang baik juga menulis kode yang dapat dibaca. Dan itu tidak pernah mengganggu saya untuk melihat awal dari suatu fungsi dimulai dengan blok menegaskan untuk memverifikasi bahwa argumen itu bukan sampah - cukup masukkan baris kosong setelahnya.

Juga dalam pengalaman saya, rawatan selalu ditingkatkan dengan menegaskan, seperti halnya dengan unit test. Asserts memberikan kewarasan memeriksa bahwa kode sedang digunakan seperti yang dimaksudkan untuk digunakan.

Bob Murphy
sumber
1
Jawaban yang bagus. Saya juga menambahkan deskripsi untuk pertanyaan tentang bagaimana saya meningkatkan keterbacaan dengan Emacs.
Alan Turing
2
"Sudah pengalaman saya bahwa orang yang menulis pernyataan yang baik juga menulis kode yang dapat dibaca" << poin yang sangat baik. Membuat kode dapat dibaca adalah tergantung pada masing-masing programmer karena itu adalah teknik yang dia dan tidak diizinkan untuk digunakan. Saya telah melihat teknik yang baik menjadi tidak dapat dibaca di tangan yang salah, dan bahkan apa yang kebanyakan orang anggap teknik yang buruk menjadi sangat jelas, bahkan elegan, dengan penggunaan abstraksi dan komentar yang tepat.
Greg Jackson
Saya mengalami beberapa gangguan aplikasi yang disebabkan oleh pernyataan yang salah. Jadi saya telah melihat bug yang tidak akan ada jika seseorang (saya) menulis lebih sedikit konfirmasi.
CodesInChaos
@CodesInChaos Bisa dibilang, salah ketik, ini menunjuk ke kesalahan dalam perumusan masalah - yaitu, bug itu dalam desain, maka ketidakcocokan antara pernyataan dan kode (lainnya).
Lawrence
12

Apakah mungkin untuk menulis terlalu banyak penegasan?

Ya tentu saja. [Bayangkan contoh yang menjengkelkan di sini.] Namun, menerapkan pedoman yang dirinci di bawah ini, Anda seharusnya tidak kesulitan mendorong batas itu dalam praktik. Saya penggemar berat pernyataan juga, dan saya menggunakannya sesuai dengan prinsip-prinsip ini. Banyak dari saran ini tidak khusus untuk pernyataan tetapi hanya praktik rekayasa umum yang baik yang diterapkan padanya.

Jaga agar run-time dan jejak biner tetap diingat

Penegasan itu bagus, tetapi jika mereka membuat program Anda terlalu lambat, itu akan sangat mengganggu atau Anda akan mematikannya cepat atau lambat.

Saya suka mengukur biaya pernyataan relatif terhadap biaya fungsi yang terkandung di dalamnya. Perhatikan dua contoh berikut.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

Fungsi itu sendiri adalah operasi O (1) tetapi pernyataan akun untuk O ( n ) overhead. Saya tidak berpikir Anda ingin cek seperti itu aktif kecuali dalam keadaan yang sangat khusus.

Berikut adalah fungsi lain dengan pernyataan serupa.

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

Fungsi itu sendiri adalah operasi O ( n ) sehingga sangat menyakitkan untuk menambahkan overhead O ( n ) tambahan untuk pernyataan tersebut. Memperlambat fungsi dengan faktor konstan kecil (dalam hal ini, mungkin kurang dari 3) adalah sesuatu yang biasanya dapat kita beli di build debug tetapi mungkin tidak dalam build rilis.

Sekarang perhatikan contoh ini.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

Sementara banyak orang mungkin akan jauh lebih nyaman dengan pernyataan O (1) ini daripada dengan dua pernyataan O ( n ) dalam contoh sebelumnya, mereka secara moral setara dalam pandangan saya. Masing-masing menambahkan overhead pada urutan kompleksitas fungsi itu sendiri.

Akhirnya, ada pernyataan "benar-benar murah" yang didominasi oleh kompleksitas fungsi yang dikandungnya.

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

Di sini, kami memiliki dua pernyataan O (1) dalam fungsi O ( n ). Mungkin tidak akan menjadi masalah untuk mempertahankan overhead ini meskipun dalam rilis build.

Namun perlu diingat bahwa kompleksitas asimptotik tidak selalu memberikan perkiraan yang memadai karena dalam praktiknya, kita selalu berhadapan dengan ukuran input yang dibatasi oleh beberapa faktor konstan dan konstan yang disembunyikan oleh “Big- O ” mungkin tidak akan diabaikan.

Jadi sekarang kami telah mengidentifikasi skenario yang berbeda, apa yang bisa kami lakukan? Pendekatan (mungkin terlalu) mudah adalah mengikuti aturan seperti "Jangan gunakan pernyataan yang mendominasi fungsi yang ada di dalamnya." Sementara itu mungkin bekerja untuk beberapa proyek, yang lain mungkin membutuhkan pendekatan yang lebih berbeda. Ini bisa dilakukan dengan menggunakan makro pernyataan yang berbeda untuk kasus yang berbeda.

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

Anda sekarang dapat menggunakan tiga makro MY_ASSERT_LOW, MY_ASSERT_MEDIUMdan MY_ASSERT_HIGHbukannya makro "satu ukuran cocok untuk semua" perpustakaan standar assertuntuk pernyataan yang didominasi oleh, tidak mendominasi atau mendominasi dan mendominasi kompleksitas fungsi masing-masing yang berisi. Saat Anda membangun perangkat lunak, Anda dapat menetapkan simbol pra-prosesor MY_ASSERT_COST_LIMITuntuk memilih jenis pernyataan apa yang harus dibuat menjadi executable. Konstanta MY_ASSERT_COST_NONEdan MY_ASSERT_COST_ALLtidak sesuai dengan makro MY_ASSERT_COST_LIMITpernyataan apa pun dan dimaksudkan untuk digunakan sebagai nilai untuk mematikan semua asersi atau masing-masing.

Kami mengandalkan asumsi di sini bahwa kompiler yang baik tidak akan menghasilkan kode apa pun untuk

if (false_constant_expression && run_time_expression) { /* ... */ }

dan mentransformasikannya

if (true_constant_expression && run_time_expression) { /* ... */ }

ke

if (run_time_expression) { /* ... */ }

yang saya percaya adalah asumsi yang aman saat ini.

Jika Anda hendak men-tweak kode di atas, pertimbangkan penjelasan compiler-spesifik seperti __attribute__ ((cold))pada my::assertion_failedatau __builtin_expect(…, false)di !(CONDITION)untuk mengurangi overhead dari pernyataan berlalu. Dalam rilis rilis, Anda juga dapat mempertimbangkan mengganti panggilan fungsi my::assertion_faileddengan sesuatu seperti __builtin_trapmengurangi jejak kaki karena ketidaknyamanan kehilangan pesan diagnostik.

Optimalisasi semacam ini benar-benar hanya relevan dalam pernyataan yang sangat murah (seperti membandingkan dua bilangan bulat yang sudah diberikan sebagai argumen) dalam fungsi yang itu sendiri sangat kompak, tidak mempertimbangkan ukuran tambahan biner yang diakumulasikan dengan memasukkan semua string pesan.

Bandingkan bagaimana kode ini

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

dikompilasi ke dalam rakitan berikut

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

sedangkan kode berikut

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

memberikan majelis ini

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

yang saya merasa jauh lebih nyaman dengan. (Contoh diuji dengan GCC 5.3.0 menggunakan -std=c++14, -O3dan -march=nativeditandai pada 4.3.3-2-ARCH x86_64 GNU / Linux. Tidak ditampilkan dalam cuplikan di atas adalah deklarasi test::positive_difference_1stdan test::positive_difference_2ndyang saya tambahkan __attribute__ ((hot))ke. my::assertion_failedDideklarasikan dengan __attribute__ ((cold)).)

Menegaskan prasyarat dalam fungsi yang bergantung padanya

Misalkan Anda memiliki fungsi berikut dengan kontrak yang ditentukan.

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

Alih-alih menulis

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

di setiap situs panggilan, masukkan logika itu sekali ke dalam definisi count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

dan menyebutnya tanpa basa-basi lagi.

const auto frequency = count_letters(text, letter);

Ini memiliki keuntungan sebagai berikut.

  • Anda hanya perlu menulis kode pernyataan sekali. Karena tujuan fungsi adalah bahwa mereka dipanggil - sering lebih dari sekali - ini harus mengurangi jumlah keseluruhan assertpernyataan dalam kode Anda.
  • Itu membuat logika yang memeriksa prasyarat dekat dengan logika yang tergantung pada mereka. Saya pikir ini adalah aspek yang paling penting. Jika klien Anda menyalahgunakan antarmuka Anda, mereka tidak dapat diasumsikan untuk menerapkan pernyataan dengan benar sehingga lebih baik fungsi memberi tahu mereka.

Kerugian yang jelas adalah bahwa Anda tidak akan mendapatkan lokasi sumber situs panggilan ke dalam pesan diagnostik. Saya percaya bahwa ini adalah masalah kecil. Seorang debugger yang baik harus bisa membiarkan Anda melacak asal pelanggaran kontrak dengan mudah.

Pemikiran yang sama berlaku untuk fungsi "khusus" seperti operator kelebihan beban. Ketika saya menulis iterator, saya biasanya - jika sifat iterator memungkinkannya - beri mereka fungsi anggota

bool
good() const noexcept;

yang memungkinkan untuk bertanya apakah aman untuk dereferensi iterator. (Tentu saja, dalam praktiknya, hampir selalu hanya mungkin untuk menjamin bahwa itu tidak akan aman untuk mengulangi iterator. Tapi saya percaya Anda masih dapat menangkap banyak bug dengan fungsi seperti itu.) Daripada membuang sampah sembarangan semua kode saya yang menggunakan iterator dengan assert(iter.good())pernyataan, saya lebih suka menempatkan satu assert(this->good())sebagai baris pertama operator*dalam implementasi iterator.

Jika Anda menggunakan pustaka standar, alih-alih menegaskan secara manual tentang prasyaratnya dalam kode sumber Anda, nyalakan pemeriksaan mereka dalam pembuatan debug. Mereka bahkan dapat melakukan pemeriksaan yang lebih canggih seperti menguji apakah wadah yang disebut iterator masih ada. (Lihat dokumentasi untuk libstdc ++ dan libc ++ (sedang berlangsung) untuk informasi lebih lanjut.)

Faktor kondisi umum keluar

Misalkan Anda sedang menulis paket aljabar linier. Banyak fungsi akan memiliki prasyarat yang rumit dan melanggarnya akan sering menyebabkan hasil yang salah yang tidak segera dikenali. Akan sangat baik jika fungsi-fungsi ini menegaskan prasyarat mereka. Jika Anda mendefinisikan sekelompok predikat yang memberi tahu Anda properti tertentu tentang struktur, pernyataan itu menjadi jauh lebih mudah dibaca.

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

Ini juga akan memberikan pesan kesalahan yang lebih bermanfaat.

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

membantu lebih banyak daripada, katakanlah

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

di mana Anda pertama kali harus melihat kode sumber dalam konteks untuk mencari tahu apa yang sebenarnya diuji.

Jika Anda memiliki classinvarian non-sepele, mungkin ide yang baik untuk menyatakannya dari waktu ke waktu ketika Anda telah mengacaukan keadaan internal dan ingin memastikan bahwa Anda meninggalkan objek dalam keadaan valid saat pengembalian.

Untuk tujuan ini, saya merasa berguna untuk mendefinisikan privatefungsi anggota yang saya panggil secara konvensional class_invaraiants_hold_. Misalkan Anda menerapkan kembali std::vector(Karena kita semua tahu itu tidak cukup baik.), Mungkin memiliki fungsi seperti ini.

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

Perhatikan beberapa hal tentang ini.

  • Fungsi predikat itu sendiri adalah constdan noexcept, sesuai dengan pedoman bahwa pernyataan tidak akan memiliki efek samping. Jika itu masuk akal, nyatakan juga constexpr.
  • Predikat itu tidak menegaskan apa pun itu sendiri. Ini dimaksudkan untuk disebut pernyataan dari dalam , seperti assert(this->class_invariants_hold_()). Dengan cara ini, jika pernyataan dikompilasi keluar, kita dapat yakin bahwa tidak ada overhead run-time yang dikeluarkan.
  • Aliran kontrol di dalam fungsi dipecah menjadi beberapa ifpernyataan dengan awal returns daripada ekspresi besar. Ini membuatnya mudah untuk melangkah melalui fungsi dalam debugger dan mencari tahu bagian invarian mana yang rusak jika pernyataan itu menyala.

Jangan menegaskan hal-hal konyol

Beberapa hal tidak masuk akal untuk ditegaskan.

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

Pernyataan ini tidak membuat kode sedikit lebih mudah dibaca atau lebih mudah dipikirkan. Setiap programmer C ++ harus cukup percaya diri bagaimana cara std::vectorkerjanya untuk memastikan bahwa kode di atas benar hanya dengan melihatnya. Saya tidak mengatakan bahwa Anda tidak boleh menegaskan ukuran wadahnya. Jika Anda telah menambahkan atau menghapus elemen menggunakan beberapa aliran kontrol non-sepele, pernyataan seperti itu bisa berguna. Tetapi jika itu hanya mengulangi apa yang ditulis dalam kode non-tegas di atas, tidak ada nilai yang didapat.

Juga jangan nyatakan bahwa fungsi perpustakaan bekerja dengan benar.

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

Jika Anda mempercayai perpustakaan itu sedikit, lebih baik pertimbangkan untuk menggunakan perpustakaan lain.

Di sisi lain, jika dokumentasi perpustakaan tidak 100% jelas dan Anda mendapatkan kepercayaan tentang kontraknya dengan membaca kode sumber, akan sangat masuk akal untuk menegaskan “kontrak yang disimpulkan” tersebut. Jika rusak di versi perpustakaan yang akan datang, Anda akan melihat dengan cepat.

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

Ini lebih baik daripada solusi berikut yang tidak akan memberi tahu Anda apakah asumsi Anda benar.

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

Jangan menyalahgunakan pernyataan untuk mengimplementasikan logika program

Penegasan hanya boleh digunakan untuk mengungkap bug yang layak segera membunuh aplikasi Anda. Mereka tidak boleh digunakan untuk memverifikasi kondisi lain apa pun meskipun reaksi yang sesuai dengan kondisi itu juga akan segera dihentikan.

Karena itu, tulis ini ...

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

... bukannya itu.

assert(server_reachable());

Juga tidak pernah menggunakan pernyataan untuk memvalidasi input yang tidak tepercaya atau memeriksa yang std::mallocbukan returnAnda nullptr. Bahkan jika Anda tahu bahwa Anda tidak akan pernah mematikan pernyataan, bahkan dalam rilis rilis, pernyataan berkomunikasi kepada pembaca bahwa itu memeriksa sesuatu yang selalu benar mengingat bahwa program tersebut bebas bug dan tidak memiliki efek samping yang terlihat. Jika ini bukan jenis pesan yang ingin Anda komunikasikan, gunakan mekanisme penanganan kesalahan alternatif seperti throwing pengecualian. Jika Anda merasa nyaman untuk memiliki pembungkus makro untuk cek non-penegasan Anda, silakan menulis satu. Hanya saja jangan menyebutnya "menegaskan", "menganggap", "mengharuskan", "memastikan" atau sesuatu seperti itu. Logikanya internal bisa sama dengan assert, kecuali bahwa itu tidak pernah dikompilasi, tentu saja.

Informasi lebih lanjut

Saya menemukan John Lakos' bicara Defensive Pemrograman Selesai Tepat , diberikan pada CppCon'14 ( 1 st bagian , 2 nd bagian ) sangat mencerahkan. Dia mengambil gagasan untuk menyesuaikan pernyataan apa yang diaktifkan dan bagaimana bereaksi terhadap pengecualian yang gagal bahkan lebih jauh daripada yang saya lakukan dalam jawaban ini.

5gon12eder
sumber
4
Assertions are great, but ... you will turn them off sooner or later.- Semoga lebih cepat, seperti sebelum kode dikirimkan. Hal-hal yang perlu membuat program mati dalam produksi harus menjadi bagian dari kode "nyata", bukan dalam pernyataan.
Blrfl
4

Saya menemukan bahwa seiring waktu saya menulis lebih sedikit menegaskan karena banyak dari mereka berjumlah "adalah kompiler bekerja" dan "adalah perpustakaan bekerja". Setelah Anda mulai berpikir tentang apa yang sebenarnya Anda uji, saya kira Anda akan menulis lebih sedikit konfirmasi.

Misalnya, metode yang (misalnya) menambahkan sesuatu ke koleksi seharusnya tidak perlu menyatakan bahwa koleksi itu ada - itu umumnya merupakan prasyarat dari kelas yang memiliki pesan atau itu adalah kesalahan fatal yang harus membuatnya kembali ke pengguna . Jadi periksa sekali, sangat awal, lalu anggap itu.

Pernyataan kepada saya adalah alat debugging, dan saya biasanya akan menggunakannya dalam dua cara: menemukan bug di meja saya (dan mereka tidak diperiksa. Yah, mungkin satu kunci yang mungkin); dan menemukan bug di meja pelanggan (dan mereka diperiksa). Kedua kali saya menggunakan pernyataan kebanyakan untuk menghasilkan jejak stack setelah memaksa pengecualian sedini mungkin. Ketahuilah bahwa pernyataan yang digunakan dengan cara ini dapat dengan mudah menyebabkan heisenbug - bug tersebut mungkin tidak pernah terjadi di debug build yang memiliki pernyataan diaktifkan.


sumber
4
Saya tidak mengerti maksud Anda ketika Anda mengatakan "itu umumnya merupakan prasyarat dari kelas yang memiliki pesan atau itu adalah kesalahan fatal yang seharusnya membuatnya kembali ke pengguna. Jadi periksalah satu kali, sangat awal, lalu anggap itu. ”Untuk apa Anda menggunakan pernyataan kalau bukan untuk memverifikasi asumsi Anda?
5gon12eder
4

Terlalu sedikit pernyataan: semoga berhasil mengubah kode yang penuh dengan asumsi tersembunyi.

Terlalu banyak asersi: dapat menyebabkan masalah keterbacaan dan berpotensi berbau kode - apakah kelas, fungsi, API dirancang dengan benar ketika memiliki begitu banyak asumsi yang ditempatkan dalam pernyataan yang tegas?

Mungkin ada juga pernyataan yang tidak benar-benar memeriksa apa pun atau memeriksa hal-hal seperti pengaturan kompiler di setiap fungsi: /

Bertujuan untuk sweet spot, tetapi tidak kurang (seperti orang lain sudah mengatakan, "lebih" dari pernyataan kurang berbahaya daripada memiliki terlalu sedikit atau tuhan membantu kita - tidak ada).

Merusak
sumber
3

Akan luar biasa jika Anda bisa menulis fungsi Assert yang hanya mengambil referensi ke metode boolean CONST, dengan cara ini Anda yakin bahwa pernyataan Anda tidak memiliki efek samping dengan memastikan bahwa metode const boolean digunakan untuk menguji pernyataan tersebut

itu akan menarik sedikit dari keterbacaan, khususnya karena saya tidak berpikir Anda tidak dapat membuat anotasi lambda (dalam c ++ 0x) untuk menjadi const untuk beberapa kelas, yang berarti Anda tidak dapat menggunakan lambdas untuk itu

berlebihan jika Anda bertanya kepada saya, tetapi jika saya akan mulai melihat tingkat polusi tertentu karena menegaskan saya akan waspada terhadap dua hal:

  • memastikan tidak ada efek samping yang terjadi di tempat tersebut (disediakan oleh sebuah konstruk seperti dijelaskan di atas)
  • kinerja selama pengujian pengembangan; ini dapat diatasi dengan menambahkan level (seperti logging) ke fasilitas tegas; sehingga Anda dapat menonaktifkan beberapa penegasan dari build pengembangan untuk meningkatkan kinerja
lurscher
sumber
2
Omong kosong Anda suka kata "tertentu" dan turunannya. Saya menghitung 8 penggunaan.
Casey Patton
ya, maaf saya cenderung klik pada kata-kata terlalu banyak - diperbaiki, terima kasih
lurscher
2

Saya telah menulis dalam bahasa C # lebih dari yang saya lakukan di C ++, tetapi kedua bahasa tersebut tidak terlalu berjauhan. Di .Net saya menggunakan Asserts untuk kondisi yang seharusnya tidak terjadi, tetapi saya juga sering melempar pengecualian ketika tidak ada cara untuk melanjutkan. VS2010 debugger menunjukkan kepada saya banyak info bagus tentang pengecualian, tidak peduli seberapa dioptimalkan Rilis build. Anda juga bisa menambahkan tes unit jika memungkinkan. Terkadang logging juga merupakan hal yang baik untuk dimiliki sebagai bantuan debugging.

Jadi, bisakah ada terlalu banyak penegasan? Iya nih. Memilih antara Batalkan / Abaikan / Lanjutkan 15 kali dalam satu menit terasa menyebalkan. Pengecualian hanya dibuang satu kali. Sulit untuk menghitung titik di mana ada terlalu banyak penegasan, tetapi jika pernyataan Anda memenuhi peran penegasan, pengecualian, pengujian unit dan pencatatan, maka ada sesuatu yang salah.

Saya akan memesan pernyataan untuk skenario yang seharusnya tidak terjadi. Anda mungkin terlalu menekankan pada awalnya, karena pernyataan lebih cepat untuk ditulis, tetapi faktor ulang kode nanti - ubah beberapa dari mereka menjadi pengecualian, beberapa menjadi tes, dll. Jika Anda memiliki disiplin yang cukup untuk membersihkan setiap komentar TODO, maka tinggalkan komentar di sebelah masing-masing yang Anda rencanakan untuk dikerjakan ulang, dan JANGAN LUPA untuk membahas TODO nanti.

Pekerjaan
sumber
Jika kode Anda gagal 15 pernyataan per menit, saya pikir ada masalah yang lebih besar. Pernyataan tidak boleh diaktifkan dalam kode bebas bug dan itu mereka lakukan, mereka harus mematikan aplikasi untuk mencegah kerusakan lebih lanjut atau menjatuhkan Anda ke debugger untuk melihat apa yang sedang terjadi.
5gon12eder
2

Saya ingin bekerja dengan Anda! Seseorang yang banyak menulisasserts sangat fantastis. Saya tidak tahu apakah ada yang namanya "terlalu banyak". Jauh lebih umum bagi saya adalah orang-orang yang menulis terlalu sedikit dan akhirnya berakhir dengan masalah UB yang sesekali mematikan yang hanya muncul di bulan purnama yang bisa dengan mudah direproduksi berulang kali dengan sederhana assert.

Pesan Gagal

Satu hal yang dapat saya pikirkan adalah memasukkan informasi kegagalan ke dalam assertjika Anda belum melakukannya, seperti:

assert(n >= 0 && n < num && "Index is out of bounds.");

Dengan cara ini Anda mungkin tidak lagi merasa memiliki terlalu banyak jika Anda belum melakukan ini, karena sekarang Anda membuat pernyataan Anda untuk memainkan peran yang lebih kuat dalam mendokumentasikan asumsi dan prasyarat.

Efek samping

Tentu saja assertsebenarnya dapat disalahgunakan dan memperkenalkan kesalahan, seperti:

assert(foo() && "Call to foo failed!");

... jika foo() memicu efek samping, jadi Anda harus sangat berhati-hati tentang hal itu, tetapi saya yakin Anda sudah sebagai orang yang menyatakan dengan sangat bebas (seorang "asserter berpengalaman"). Semoga prosedur pengujian Anda juga sebaik perhatian Anda untuk menegaskan asumsi.

Kecepatan Debugging

Sementara kecepatan debug umumnya harus di bagian bawah daftar prioritas kami, suatu kali saya akhirnya menyatakan begitu banyak dalam basis kode sebelum menjalankan debug membangun melalui debugger lebih dari 100 kali lebih lambat daripada rilis.

Itu terutama karena saya memiliki fungsi seperti ini:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

... di mana setiap panggilan tunggal operator[]akan melakukan pernyataan batas memeriksa. Saya akhirnya mengganti beberapa yang kritis-kinerja dengan setara yang tidak aman yang tidak menegaskan hanya untuk mempercepat pembangunan debugging secara drastis dengan biaya kecil untuk hanya keselamatan implementasi tingkat detail, dan hanya karena kecepatan mulai itu mulai untuk menurunkan produktivitas dengan sangat nyata (memanfaatkan debugging yang lebih cepat melebihi biaya kehilangan beberapa konfirmasi, tetapi hanya untuk fungsi-fungsi seperti fungsi lintas produk ini yang digunakan dalam jalur yang paling kritis dan terukur, bukan untukoperator[] secara umum).

Prinsip Tanggung Jawab Tunggal

Meskipun saya tidak berpikir Anda bisa benar-benar salah dengan lebih banyak penegasan (setidaknya itu jauh, jauh lebih baik untuk berbuat salah di sisi terlalu banyak daripada terlalu sedikit), menegaskan sendiri mungkin tidak menjadi masalah tetapi mungkin menunjukkan satu.

Jika Anda memiliki 5 pernyataan untuk satu panggilan fungsi, misalnya, itu mungkin terlalu banyak. Antarmukanya mungkin memiliki terlalu banyak prasyarat dan parameter input, mis. Saya menganggap hal itu tidak terkait dengan topik apa yang membentuk sejumlah pernyataan yang sehat (yang biasanya saya tanggapi, "semakin meriah!"), Tetapi itu mungkin saja kemungkinan bendera merah (atau sangat mungkin tidak).


sumber
1
Ya, mungkin ada "terlalu banyak" yang menegaskan secara teori, meskipun masalah itu menjadi sangat cepat: Jika pernyataan itu membutuhkan waktu yang jauh lebih lama daripada daging fungsi. Diakui, saya tidak ingat pernah menemukan bahwa di alam liar, masalah sebaliknya adalah lazim.
Deduplicator
@Dupuplikator Ah ya, saya menemukan kasus itu dalam rutinitas matematika vektor kritis itu. Meskipun tampaknya jauh lebih baik untuk berbuat salah di samping terlalu banyak daripada terlalu sedikit!
-1

Sangat masuk akal untuk menambahkan cek ke kode Anda. Untuk pernyataan biasa (yang dibangun ke kompiler C dan C ++) pola penggunaan saya adalah pernyataan gagal berarti ada bug dalam kode yang perlu diperbaiki. Saya menafsirkan ini agak murah hati; jika saya mengharapkan permintaan web untuk kembali status 200 dan menegaskan untuk itu tanpa penanganan kasus lain maka pernyataan gagal melakukan memang menunjukkan bug dalam kode saya, sehingga menegaskan dibenarkan.

Jadi ketika orang mengatakan pernyataan bahwa hanya memeriksa apa yang dilakukan kode itu berlebihan, itu tidak benar. Penegasan itu memeriksa apa yang menurut mereka dilakukan oleh kode, dan inti dari pernyataan tersebut adalah untuk memeriksa bahwa asumsi tidak ada bug dalam kode itu benar. Dan pernyataan itu bisa berfungsi sebagai dokumentasi juga. Jika saya berasumsi bahwa setelah menjalankan loop i == n dan tidak 100% jelas dari kode, maka "menegaskan (i == n)" akan sangat membantu.

Lebih baik memiliki lebih dari sekadar "menegaskan" dalam daftar Anda untuk menangani situasi yang berbeda. Misalnya situasi di mana saya memeriksa bahwa sesuatu tidak terjadi yang akan menunjukkan bug, tetapi masih terus mengatasi kondisi itu. (Misalnya, jika saya menggunakan beberapa cache maka saya mungkin akan memeriksa kesalahan, dan jika kesalahan terjadi secara tak terduga mungkin aman untuk memperbaiki kesalahan dengan membuang cache. Saya ingin sesuatu yang hampir menegaskan, yang memberitahu saya selama pengembangan , dan masih memungkinkan saya melanjutkan.

Contoh lain adalah situasi di mana saya tidak mengharapkan sesuatu terjadi, saya punya solusi generik, tetapi jika hal ini terjadi, saya ingin tahu tentang hal itu dan memeriksanya. Sekali lagi sesuatu yang hampir seperti pernyataan, yang seharusnya memberi tahu saya selama pengembangan. Tapi tidak cukup tegas.

Terlalu banyak pernyataan: Jika suatu pernyataan crash program Anda ketika berada di tangan pengguna, maka Anda tidak boleh memiliki pernyataan yang macet karena negatif palsu.

gnasher729
sumber
-3

Tergantung. Jika persyaratan kode didokumentasikan dengan jelas, maka pernyataan harus selalu sesuai dengan persyaratan. Dalam hal ini adalah hal yang baik. Namun, jika tidak ada persyaratan atau persyaratan yang ditulis dengan buruk, maka akan sulit bagi programmer baru untuk mengedit kode tanpa harus merujuk pada unit test setiap kali untuk mencari tahu apa persyaratannya.

Cucky Arabi
sumber
3
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 8 jawaban sebelumnya
nyamuk