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_MEDIUM
dan MY_ASSERT_HIGH
bukannya makro "satu ukuran cocok untuk semua" perpustakaan standar assert
untuk 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_LIMIT
untuk memilih jenis pernyataan apa yang harus dibuat menjadi executable. Konstanta MY_ASSERT_COST_NONE
dan MY_ASSERT_COST_ALL
tidak sesuai dengan makro MY_ASSERT_COST_LIMIT
pernyataan 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_failed
atau __builtin_expect(…, false)
di !(CONDITION)
untuk mengurangi overhead dari pernyataan berlalu. Dalam rilis rilis, Anda juga dapat mempertimbangkan mengganti panggilan fungsi my::assertion_failed
dengan sesuatu seperti __builtin_trap
mengurangi 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
, -O3
dan -march=native
ditandai pada 4.3.3-2-ARCH x86_64 GNU / Linux. Tidak ditampilkan dalam cuplikan di atas adalah deklarasi test::positive_difference_1st
dan test::positive_difference_2nd
yang saya tambahkan __attribute__ ((hot))
ke. my::assertion_failed
Dideklarasikan 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
assert
pernyataan 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 class
invarian 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 private
fungsi 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
const
dan 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
if
pernyataan dengan awal return
s 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::vector
kerjanya 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::malloc
bukan return
Anda 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 throw
ing 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.