Çok fazla iddia yazmak mümkün mü?
Eee, tabii ki oyle. [Burada iğrenç bir örnek düşünün.] Ancak, aşağıda ayrıntılı olarak verilen yönergeleri uygulayarak, bu limiti pratikte zorlamakta zorlanmayacaksınız. Ben de iddiaların büyük bir hayranıyım ve bunları bu ilkelere göre kullanıyorum. Bu tavsiyenin çoğu iddialara özel değildir, ancak yalnızca kendilerine uygulanan genel iyi mühendislik uygulamalarını içerir.
Çalışma süresi ve ikili ayak izini göz önünde bulundurun
İddialar harika, ancak programınızı kabul edilemez derecede yavaşlatırlarsa ya çok can sıkıcı olacak ya da er ya da geç kapatacaksınız.
İçerdiği işlevin maliyetine göre bir iddianın maliyetini ölçmeyi seviyorum. Aşağıdaki iki örneği inceleyin.
// 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();
}
Fonksiyonun kendisi bir O (1) işlemidir ancak iddialar ek yükü O ( n ) olarak hesaplar . Çok özel durumlarda olmadığı sürece böyle çeklerin aktif olmasını istediğinizi sanmıyorum.
İşte benzer iddiaları ile başka bir işlev.
// 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_)));
}
İşlev, bir O ( n ) işlemidir, bu nedenle iddia için ek bir O ( n ) ek yükü eklemek çok daha az acıtır . Bir işlevi küçük bir değere (bu durumda, muhtemelen 3'ten az) kısmak, genellikle bir hata ayıklama yapısında göze alabileceğimiz ancak serbest bırakma yapısında olmayan bir şeydir.
Şimdi bu örneği ele alalım.
// 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();
}
Birçok insan bu O (1) iddiasında önceki örnekteki iki O ( n ) iddiasına göre çok daha rahat olsa da , benim görüşüme göre ahlaki olarak eşdeğerdir. Her biri, fonksiyonun karmaşıklığının sırasına ek olarak gelir.
Son olarak, içinde bulundukları işlevin karmaşıklığının egemen olduğu “gerçekten ucuz” iddialar var.
// 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;
}
Burada, bir O ( n ) fonksiyonunda iki O (1) iddiamız var . Muhtemelen bu ek yükü serbest bırakma yapılarında bile tutmak sorun olmayacak.
Bununla birlikte, asimptotik karmaşıklıkların her zaman yeterli bir tahmin vermediğini unutmayın, çünkü pratikte, her zaman sınırlı son sabit tarafından sınırlanan girdi boyutları ile uğraşıyoruz ve “Big- O ” tarafından gizlenen sabit faktörler çok ihmal edilemeyebilir.
Öyleyse şimdi farklı senaryolar belirledik, onlar hakkında ne yapabiliriz? (Muhtemelen de) kolay bir yaklaşım, “İçlerinde bulundukları işleve hakim olan iddiaları kullanma” gibi bir kuralı takip etmek olacaktır. Bazı projeler için işe yarayabilirken, diğerleri daha farklı bir yaklaşıma ihtiyaç duyabilir. Bu, farklı durumlar için farklı değerlendirme makroları kullanılarak yapılabilir.
#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;
}
Artık üç makro kullanabilirsiniz MY_ASSERT_LOW
, MY_ASSERT_MEDIUM
ve MY_ASSERT_HIGH
standart kütüphanenin “tek beden herkese uyar” yerine assert
hakim, ne hakim ve sırasıyla bunların içeren fonksiyonunun karmaşıklığı hakim ne de hakimdir beyanlarına ilişkin makro. Yazılımı oluştururken, MY_ASSERT_COST_LIMIT
ne tür iddiaların çalıştırılabilir hale getirilmesi gerektiğini seçmek için ön işlemci sembolünü önceden tanımlayabilirsiniz . Sabitler MY_ASSERT_COST_NONE
ve MY_ASSERT_COST_ALL
herhangi bir assert makrosuna karşılık gelmezler ve MY_ASSERT_COST_LIMIT
tüm iddiaları sırayla kapatmak veya açmak için değerler olarak kullanılırlar .
Burada iyi bir derleyicinin kod oluşturmayacağı varsayımına güveniyoruz
if (false_constant_expression && run_time_expression) { /* ... */ }
ve dönüşüm
if (true_constant_expression && run_time_expression) { /* ... */ }
içine
if (run_time_expression) { /* ... */ }
Bugünlerde güvenli bir varsayım olduğuna inanıyorum.
Yukarıdaki kodu değiştirmek üzereyseniz, geçen iddiaların ek yükünü azaltmak için derleyiciye özel açıklamaları __attribute__ ((cold))
açık my::assertion_failed
veya __builtin_expect(…, false)
açık gibi düşünün !(CONDITION)
. Sürüm oluşturma işlemlerinde, işlev çağrısı yerine, bir tanılama mesajını kaybetme durumunda ayak izini azaltmak my::assertion_failed
gibi bir şeyle değiştirmeyi de düşünebilirsiniz __builtin_trap
.
Bu tür optimizasyonlar, yalnızca tüm mesaj dizelerini birleştirerek biriktirilen ikilinin ek boyutunu göz önünde bulundurmadan, çok kompakt bir fonksiyonda (zaten argümanlar olarak verilen iki tamsayının karşılaştırılması gibi) oldukça ucuz iddialarda geçerlidir.
Bu kodun nasıl olduğunu karşılaştırın
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;
}
aşağıdaki derleme içine derlenmiştir
_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:
aşağıdaki koddayken
int
positive_difference_2nd(const int a, const int b) noexcept
{
if (__builtin_expect(!(a > b), false))
__builtin_trap();
return a - b;
}
bu derleme verir
_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:
ki kendimi çok daha rahat hissediyorum. (Örnekler kullanılarak GCC 5.3.0 ile test edildi -std=c++14
, -O3
ve -march=native
4.3.3-2-ARCH x86_64 GNU / Linux bayrakları. Yukarıdaki snippet'lerde gösterilmemiştir bildirgeleridir test::positive_difference_1st
ve test::positive_difference_2nd
hangi bir ilave __attribute__ ((hot))
için. my::assertion_failed
İlan edildi __attribute__ ((cold))
.)
Onlara bağlı fonksiyonda ön koşulları belirtin
Belirtilen sözleşmede aşağıdaki işleve sahip olduğunuzu varsayalım.
/**
* @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;
Yazmak yerine
assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);
Her arama sitesinde, bu mantığı bir kez tanımına ekleyin. 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;
}
ve daha fazla uzatmadan arayın.
const auto frequency = count_letters(text, letter);
Bunun aşağıdaki avantajları vardır.
- Onay kodunu yalnızca bir kez yazmanız gerekir. Fonksiyonların amacı, bunların çağrılmasıdır - çoğu zaman bir kereden fazla - bu
assert
, kodunuzdaki toplam ifade sayısını azaltmalıdır .
- Önkoşulları, onlara bağlı olan mantığa yakın kontrol eden mantığı tutar. Bunun en önemli yön olduğunu düşünüyorum. Müşterileriniz arayüzünüzü kötüye kullanırsa, iddiaları doğru uyguladıkları varsayılmaz, bu nedenle işlev onlara söylerse daha iyidir.
Açık dezavantajı, çağrı sitesinin kaynak konumunu teşhis mesajına sokmamanızdır. Bunun küçük bir sorun olduğuna inanıyorum. İyi bir hata ayıklayıcı, sözleşme ihlalinin kökenini rahatça izlemenize izin vermelidir.
Aynı düşünce, aşırı yüklenmiş operatörler gibi “özel” fonksiyonlar için de geçerlidir. Yineleyiciler yazarken, genellikle - yineleyicinin yapısı izin veriyorsa - onlara bir üye işlevi verin
bool
good() const noexcept;
Bu, yineleyiciyi kaldırmanın güvenli olup olmadığını sormaya izin verir. (Elbette, pratikte, yineleyiciyi serbest bırakmanın güvenli olmayacağını garanti etmek neredeyse her zaman mümkündür . assert(iter.good())
ifadeleri ile yineleyici kullanır, yineleyici uygulamasında assert(this->good())
ilk satır olarak tek bir koymak istiyorum operator*
.
Standart kitaplığı kullanıyorsanız, kaynak kodunuzdaki ön koşullarını elle belirtmek yerine, hata ayıklama yapılarında denetimlerini açın. Bir yineleyicinin başvurduğu kabın hala var olup olmadığını test etmek gibi daha karmaşık kontroller yapabilirler. (Daha fazla bilgi için libstdc ++ ve libc ++ (devam eden çalışma) belgelerine bakın .)
Faktör ortak koşulları
Doğrusal bir cebir paketi yazdığınızı varsayalım. Pek çok işlev karmaşık ön koşullara sahip olacak ve bunları ihlal etmek çoğu zaman böyle bir şekilde hemen tanınamayan yanlış sonuçlara neden olacaktır. Bu işlevlerin ön koşullarını ortaya koyması çok iyi olurdu. Bir yapı hakkında size bazı özellikler anlatan bir dizi tahmin belirlerseniz, bu iddialar çok daha okunaklı hale gelir.
template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
assert(is_square(m) && is_symmetric(m));
// TODO: Somehow decompose that thing...
}
Aynı zamanda daha kullanışlı hata mesajları verecektir.
cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)
çok daha fazla yardımcı olur
detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)
ilk önce nereye bakmanız gerektiğini, gerçekte neyin test edildiğini bulmak için bağlamdaki kaynak koduna bakın.
Eğer bir varsa class
önemsiz olmayan değişmezler ile, muhtemelen dahili devlet ile haberci ve karşılığında ilgili geçerli bir durumda nesneyi terk ediyoruz sağlamak istiyoruz gelmiş zaman zaman onlara assert için iyi bir fikirdir.
Bu amaçla, private
geleneksel olarak çağırdığım bir üye fonksiyonu tanımlamayı faydalı buldum class_invaraiants_hold_
. Yeniden uyguladığınızı varsayalım std::vector
(Çünkü hepimiz yeterince iyi olmadığını biliyoruz.), Bunun gibi bir işlevi olabilir.
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;
}
Bununla ilgili birkaç şey dikkat edin.
- Öngörü işlevinin kendisidir
const
ve noexcept
kılavuza göre iddiaların yan etkileri olmayacaktır. Mantıklıysa, ilan edin constexpr
.
- Tahmini hiçbir şey kendisi göstermiyor. Bunun gibi iddiaların içinde çağrılmak istenmektedir
assert(this->class_invariants_hold_())
. Bu şekilde, iddialar derlenirse, çalışma zamanı ek yükü bulunmadığından emin olabiliriz.
- İşlev içindeki kontrol akışı, büyük bir ifadeden ziyade
if
, erken return
s ile çoklu ifadelere bölünür . Bu, fonksiyonun bir hata ayıklayıcıda adım adım ilerlemesini ve iddia ateşlenirse değişmezin hangi kısmının kırıldığını bulmayı kolaylaştırır.
Aptalca şeylere güvenme
Bazı şeyler üzerinde durmak mantıklı değil.
auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2); // silly
assert(!numbers.empty()); // silly and redundant
Bu iddialar, kodu küçük bir parçadan daha okunaklı veya akla gelmesi kolay hale getirmez. Her C ++ programcısı std::vector
, yukarıdaki kodun sadece ona bakarak doğru olduğundan emin olmak için nasıl çalıştığından emin olmalıdır . Asla bir konteynerin büyüklüğünü iddia etmemen gerektiğini söylemiyorum. Bazı önemsiz olmayan kontrol akışını kullanarak öğeler eklediyseniz veya kaldırdıysanız, böyle bir iddia yararlı olabilir. Ancak, sadece yukarıda belirtilen iddiasızlık kodunda yazılmış olanı tekrarlarsa, elde edilen hiçbir değer yoktur.
Ayrıca kütüphane işlevlerinin doğru çalıştığını iddia etmeyin.
auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled()); // probably silly
Kütüphaneye bu kadar az güveniyorsanız, bunun yerine başka bir kütüphane kullanmayı düşünün.
Öte yandan, kütüphanenin dokümantasyonu% 100 net değilse ve kaynak kodunu okuyarak sözleşmeleri hakkında güven kazanıyorsanız, bu “çıkarılan sözleşmeyi” iddia etmek çok mantıklı olacaktır. Kitaplığın gelecekteki bir sürümünde kırılırsa, hemen farkedeceksiniz.
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());
Bu, varsayımlarınızın doğru olup olmadığını size söylemeyecek olan aşağıdaki çözümden daha iyidir.
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();
}
Program mantığını uygulamak için iddiaları kötüye kullanmayın
İddialar yalnızca başvurunuzu derhal öldürmeye değecek böcekleri ortaya çıkarmak için kullanılmalıdır. Bu duruma uygun tepki de hemen bırakmak olsa bile, başka bir durumu doğrulamak için kullanılmamalıdırlar.
Bu nedenle, şunu yaz…
if (!server_reachable())
{
log_message("server not reachable");
shutdown();
}
…bunun yerine.
assert(server_reachable());
Ayrıca, güvensiz girişi doğrulamak ya da sizin std::malloc
yapmadığınızı kontrol etmek için asla iddiaları kullanmayın . İddiaları hiçbir zaman kapatmayacağınızı bilseniz bile, sürüm oluşturmada bile bir iddia, programın hatasız olduğu ve görünürde yan etkileri olmadığı göz önüne alındığında, okuyucuya her zaman doğru olan bir şeyi kontrol ettiğini bildirir. Bu iletişim kurmak istediğiniz mesaj türü değilse, istisna gibi alternatif bir hata işleme mekanizması kullanın . İddiaya girmeyen çekleriniz için bir makro sarmalayıcı bulundurmanın uygun olduğunu düşünüyorsanız, bir tane yazmaya devam edin. Sadece "assert", "varsayalım", "zorunlu", "emin" veya benzeri bir şey demeyin. İç mantığı , elbette asla derlenmemesi dışında olduğu gibi olabilir .return
nullptr
throw
assert
Daha fazla bilgi
John Lakos' konuşma bulundu Savunma Programlama Done Right CppCon'14 (verilen, 1 st kısmı , 2 nd parçası ) çok aydınlatarak. Hangi iddiaların etkinleştirilebileceğini ve başarısız olan istisnalara nasıl tepki verebileceğimi bu cevaba verdiğimden daha fazla uyarlama fikrini benimsemiştir.