क्या बहुत अधिक मुखर लिखना संभव है?
ठीक है, अवश्य है। [यहाँ अप्रिय उदाहरण की कल्पना करें।] हालांकि, निम्नलिखित में विस्तृत दिशा-निर्देशों को लागू करने से, आपको उस सीमा को व्यवहार में धकेलने में परेशानी नहीं होनी चाहिए। मैं मुखरता का बहुत बड़ा प्रशंसक हूं, और मैं इन सिद्धांतों के अनुसार उनका उपयोग करता हूं। इस सलाह में से अधिकांश जोर देने के लिए विशेष नहीं है, लेकिन केवल सामान्य अच्छे इंजीनियरिंग अभ्यास उन पर लागू होते हैं।
रन-टाइम और बाइनरी फ़ुटप्रिंट ओवरहेड को ध्यान में रखें
दावे महान हैं, लेकिन यदि वे आपके कार्यक्रम को अस्वीकार्य रूप से धीमा कर देते हैं, तो यह बहुत कष्टप्रद होगा या आप उन्हें जल्द या बाद में बंद कर देंगे।
मुझे उस फ़ंक्शन की लागत के सापेक्ष अभिकथन की लागत का अनुमान लगाना पसंद है जो इसमें निहित है। निम्नलिखित दो उदाहरणों पर विचार करें।
// 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();
}
समारोह अपने आप में एक है हे (1) आपरेशन लेकिन दावे के लिए खाते हे ( एन ) भूमि के ऊपर। मुझे नहीं लगता कि आप ऐसे चेक को सक्रिय करना चाहेंगे जब तक कि बहुत विशेष परिस्थितियों में नहीं।
यहां इसी तरह के दावे के साथ एक और समारोह है।
// 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_)));
}
फ़ंक्शन स्वयं एक O ( n ) ऑपरेशन है, इसलिए यह जोर के लिए एक अतिरिक्त O ( n ) ओवरहेड जोड़ने के लिए बहुत कम दर्द होता है । एक फ़ंक्शन को एक छोटे से धीमा करना (इस मामले में, शायद 3 से कम) निरंतर कारक एक ऐसी चीज है जिसे हम आमतौर पर डिबग बिल्ड में रख सकते हैं लेकिन शायद रिलीज़ बिल्ड में नहीं।
अब इस उदाहरण पर विचार करें।
// 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();
}
जबकि कई लोग संभवतः इस O (1) जोर के साथ पिछले उदाहरण में दो O ( n ) दावे के साथ अधिक सहज होंगे , वे मेरे विचार से नैतिक रूप से समकक्ष हैं। प्रत्येक फ़ंक्शन की जटिलता के आदेश पर ओवरहेड जोड़ता है।
अंत में, "वास्तव में सस्ते" दावे हैं जो उस फ़ंक्शन की जटिलता से प्रभावित होते हैं जिसमें वे निहित हैं।
// 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;
}
यहाँ, हमारे पास O ( n ) फ़ंक्शन में दो O (1) दावे हैं । यह शायद रिलीज ओवरडल्स में भी इस ओवरहेड को रखने के लिए कोई समस्या नहीं होगी।
हालांकि, ध्यान रखें, कि विषमतापूर्ण जटिलताएं हमेशा पर्याप्त अनुमान नहीं दे रही हैं क्योंकि व्यवहार में, हम हमेशा "बिग- ओ " द्वारा छिपे हुए कुछ परिमित स्थिर और स्थिर कारकों से बंधे इनपुट आकारों के साथ काम कर रहे हैं, बहुत अच्छी तरह से नगण्य नहीं हो सकता है।
इसलिए अब हमने विभिन्न परिदृश्यों की पहचान की है, हम उनके बारे में क्या कर सकते हैं? एक (संभवत: भी) आसान तरीका एक नियम का पालन करना होगा जैसे कि "उन फ़ंक्शन पर हावी होने का उपयोग न करें जो वे इसमें निहित हैं।" जबकि यह कुछ परियोजनाओं के लिए काम कर सकता है, दूसरों को अधिक विभेदित दृष्टिकोण की आवश्यकता हो सकती है। यह विभिन्न मामलों के लिए विभिन्न अभिकथन मैक्रो का उपयोग करके किया जा सकता है।
#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;
}
अब आप तीन मैक्रोज़ का उपयोग कर सकते हैं MY_ASSERT_LOW
, MY_ASSERT_MEDIUM
और MY_ASSERT_HIGH
मानक पुस्तकालय के बजाय "एक आकार सभी को फिट बैठता है" assert
स्थूल के लिए मैक्रो का उपयोग किया जाता है, जो न तो हावी होते हैं और न ही हावी होते हैं और क्रमशः उनके युक्त फ़ंक्शन की जटिलता पर हावी होते हैं। जब आप सॉफ़्टवेयर का निर्माण करते हैं, तो आप प्री-प्रोसेसर प्रतीक को यह निर्धारित करने के लिए पूर्व-परिभाषित कर सकते MY_ASSERT_COST_LIMIT
हैं कि किस प्रकार के दावे निष्पादन योग्य बनाने चाहिए। स्थिरांक MY_ASSERT_COST_NONE
और MY_ASSERT_COST_ALL
किसी भी मुखर मैक्रो के अनुरूप नहीं होते हैं और इसका उपयोग मूल्यों के रूप MY_ASSERT_COST_LIMIT
में सभी अभिक्रियाओं को बंद करने या क्रमशः करने के लिए किया जाता है।
हम यहां इस धारणा पर भरोसा कर रहे हैं कि एक अच्छा संकलक किसी भी कोड को उत्पन्न नहीं करेगा
if (false_constant_expression && run_time_expression) { /* ... */ }
और परिवर्तन
if (true_constant_expression && run_time_expression) { /* ... */ }
में
if (run_time_expression) { /* ... */ }
मेरा मानना है कि आजकल एक सुरक्षित धारणा है।
आप ऊपर कोड में और सुधार करने के लिए के बारे में कर रहे हैं, की तरह संकलक विशेष एनोटेशन पर विचार __attribute__ ((cold))
पर my::assertion_failed
या __builtin_expect(…, false)
पर !(CONDITION)
पारित कर दिया दावे की भूमि के ऊपर कम करने के लिए। रिलीज़ बिल्ड में, आप किसी नैदानिक संदेश को खोने की असुविधा पर फ़ुट-प्रिंट को कम करने के लिए फ़ंक्शन कॉल को my::assertion_failed
कुछ के साथ बदलने पर भी विचार कर सकते हैं __builtin_trap
।
इस प्रकार के अनुकूलन वास्तव में केवल एक सस्ते फ़ंक्शन में प्रासंगिक हैं (जैसे दो पूर्णांक जो पहले से ही तर्क के रूप में दिए गए हैं) की तुलना एक फ़ंक्शन में है जो स्वयं बहुत कॉम्पैक्ट है, सभी संदेश स्ट्रिंग को शामिल करके संचित बाइनरी के अतिरिक्त आकार पर विचार नहीं करता है।
इस कोड की तुलना करें
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;
}
निम्नलिखित विधानसभा में संकलित है
_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:
जबकि निम्नलिखित कोड
int
positive_difference_2nd(const int a, const int b) noexcept
{
if (__builtin_expect(!(a > b), false))
__builtin_trap();
return a - b;
}
यह विधानसभा देता है
_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:
जिसके साथ मैं ज्यादा सहज महसूस करता हूं। (उदाहरण का उपयोग करते हुए जीसीसी 5.3.0 के साथ परीक्षण किया गया -std=c++14
, -O3
और -march=native
4.3.3-2-आर्क x86_64 जीएनयू / लिनक्स पर झंडे। ऊपर के टुकड़े में नहीं दिखाया गया की घोषणाओं हैं test::positive_difference_1st
और test::positive_difference_2nd
जो मैं जोड़ा __attribute__ ((hot))
है। my::assertion_failed
साथ घोषित किया गया था __attribute__ ((cold))
।)
फ़ंक्शन में उन पर निर्भरता को बढ़ाएँ
मान लें कि आपके पास निर्दिष्ट अनुबंध के साथ निम्नलिखित फ़ंक्शन है।
/**
* @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;
लिखने के बजाय
assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);
प्रत्येक कॉल-साइट पर, उस तर्क को एक बार परिभाषा में रखें 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;
}
और आगे की हलचल के बिना इसे कॉल करें।
const auto frequency = count_letters(text, letter);
इसके निम्नलिखित फायदे हैं।
- आपको केवल एक बार अभिकथन कोड लिखना होगा। चूंकि कार्यों का बहुत उद्देश्य यह है कि उन्हें कहा जाता है - अक्सर एक से अधिक बार - यह
assert
आपके कोड में बयानों की समग्र संख्या को कम करना चाहिए ।
- यह तर्क को रखता है जो उन पूर्व शर्त के करीब जाँचता है जो उन पर निर्भर करता है। मुझे लगता है कि यह सबसे महत्वपूर्ण पहलू है। यदि आपके ग्राहक आपके इंटरफ़ेस का दुरुपयोग करते हैं, तो उन्हें सही तरीके से दावे को लागू करने के लिए नहीं माना जा सकता है, इसलिए यह बेहतर है कि फ़ंक्शन उन्हें बताता है।
स्पष्ट नुकसान यह है कि आपको निदान संदेश में कॉल-साइट का स्रोत स्थान नहीं मिलेगा। मेरा मानना है कि यह एक मामूली बात है। एक अच्छा डिबगर आपको अनुबंध के उल्लंघन की उत्पत्ति का आसानी से पता लगाने में सक्षम होने में सक्षम होना चाहिए।
एक ही सोच "विशेष" कार्यों के लिए लागू होती है जैसे कि अतिभारित ऑपरेटर। जब मैं पुनरावृत्तियों को लिख रहा हूं, तो मैं आमतौर पर - अगर पुनरावृत्त की प्रकृति इसे अनुमति देती है - उन्हें एक सदस्य फ़ंक्शन दें
bool
good() const noexcept;
यह पूछने की अनुमति देता है कि क्या यह पुनरावृत्ति करने वाले के लिए सुरक्षित है। (बेशक, व्यवहार में, यह लगभग हमेशा केवल यह गारंटी देना संभव है कि यह itter को निष्क्रिय करने के लिए सुरक्षित नहीं होगा । लेकिन मेरा मानना है कि आप अभी भी इस तरह के फ़ंक्शन के साथ बहुत सारे कीड़े पकड़ सकते हैं।) इसके बजाय मेरे सभी कोड को लिट करने की। assert(iter.good())
बयानों के साथ पुनरावृत्ति का उपयोग करता है , मैं इसे पुनरावृत्ति के कार्यान्वयन में assert(this->good())
पहली पंक्ति के रूप में एक ही operator*
डालूँगा।
यदि आप मानक पुस्तकालय का उपयोग कर रहे हैं, तो अपने स्रोत कोड में इसके पूर्व शर्त पर मैन्युअल रूप से दावा करने के बजाय, डिबग बिल्ड में उनके चेक चालू करें। वे परीक्षण की तरह और भी अधिक परिष्कृत जांच कर सकते हैं कि क्या कंटेनर का पुनरावृत्त अभी भी मौजूद है। ( अधिक जानकारी के लिए libstdc ++ और libc ++ (प्रगति में कार्य) के लिए दस्तावेज़ देखें ।)
फैक्टर सामान्य स्थिति
मान लीजिए आप एक रेखीय बीजगणित पैकेज लिख रहे हैं। कई कार्यों में जटिल प्राथमिकताएं होंगी और उनका उल्लंघन करना अक्सर गलत परिणाम देगा जो तुरंत पहचानने योग्य नहीं हैं। यह बहुत अच्छा होगा यदि ये कार्य उनके पूर्व शर्तो पर जोर देते हैं। यदि आप विधेय का एक गुच्छा परिभाषित करते हैं जो आपको एक संरचना के बारे में कुछ गुण बताते हैं, तो वे दावे बहुत अधिक पठनीय बन जाते हैं।
template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
assert(is_square(m) && is_symmetric(m));
// TODO: Somehow decompose that thing...
}
यह अधिक उपयोगी त्रुटि संदेश भी देगा।
cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)
से बहुत अधिक मदद करता है, कहते हैं
detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)
वास्तव में परीक्षण किया गया था, यह पता लगाने के लिए आपको संदर्भ में स्रोत कोड को देखना होगा।
यदि आपके पास class
गैर-तुच्छ आक्रमणकारियों के साथ है, तो संभवतः समय-समय पर उन पर जोर देना एक अच्छा विचार है जब आपने आंतरिक स्थिति के साथ खिलवाड़ किया है और यह सुनिश्चित करना चाहते हैं कि आप वस्तु को वापसी पर वैध स्थिति में छोड़ रहे हैं।
इस उद्देश्य के लिए, private
मुझे पारंपरिक रूप से कॉल करने वाले सदस्य फ़ंक्शन को परिभाषित करने के लिए उपयोगी लगा class_invaraiants_hold_
। मान लीजिए कि आप फिर से लागू कर रहे थे std::vector
(क्योंकि हम सभी जानते हैं कि यह पर्याप्त रूप से अच्छा नहीं है।), इसमें इस तरह का कार्य हो सकता है।
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;
}
इस बारे में कुछ बातें नोटिस करें।
- प्रेडिकेट कार्य स्वयं है
const
और noexcept
, दिशानिर्देशों के अनुसार कि अभिक्रियाओं के दुष्प्रभाव नहीं होंगे। अगर यह समझ में आता है, तो इसे भी घोषित करें constexpr
।
- विधेय खुद कुछ भी मुखर नहीं करता है। इसे मुखर रूप में कहा जाता है , जैसे कि
assert(this->class_invariants_hold_())
। इस तरह, यदि अभिकथनों को संकलित किया जाता है, तो हम सुनिश्चित कर सकते हैं कि कोई रन-टाइम ओवरहेड न हो।
- फ़ंक्शन के अंदर नियंत्रण प्रवाह एक बड़ी अभिव्यक्ति के बजाय
if
शुरुआती return
एस के साथ कई बयानों में टूट गया है । इससे डिबगर में फ़ंक्शन के माध्यम से कदम रखना आसान हो जाता है और पता चलता है कि मुखरता से आग लगने पर हमलावर का क्या हिस्सा टूट गया था।
मूर्खतापूर्ण बातों पर जोर न दें
कुछ चीजें सिर्फ समझ में नहीं आती हैं।
auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2); // silly
assert(!numbers.empty()); // silly and redundant
इन दावों के बारे में तर्क करने के लिए कोड को एक छोटे से अधिक पठनीय या आसान भी नहीं बनाते हैं। हर C ++ प्रोग्रामर को पर्याप्त आत्मविश्वास होना चाहिए कि कैसे std::vector
काम किया जाए कि उपरोक्त कोड को देखकर ही सही हो। मैं यह नहीं कह रहा हूं कि आपको कंटेनर के आकार पर जोर नहीं देना चाहिए। यदि आपने कुछ गैर-तुच्छ नियंत्रण प्रवाह का उपयोग करके तत्वों को जोड़ा या हटाया है, तो इस तरह का एक जोर उपयोगी हो सकता है। लेकिन अगर यह केवल वही दोहराता है जो ऊपर दिए गए गैर-अभिकथन कोड में लिखा गया था, तो कोई मूल्य प्राप्त नहीं हुआ है।
इसके अलावा पुस्तकालय कार्यों सही ढंग से काम करते हैं कि जोर नहीं है।
auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled()); // probably silly
यदि आप लाइब्रेरी पर भरोसा करते हैं, तो इसके बजाय दूसरे लाइब्रेरी का उपयोग करने पर विचार करें।
दूसरी ओर, यदि लाइब्रेरी का प्रलेखन 100% स्पष्ट नहीं है और आप स्रोत कोड को पढ़कर इसके अनुबंधों के बारे में विश्वास हासिल करते हैं, तो यह उस "अनुमानित अनुबंध" पर जोर देने के लिए बहुत मायने रखता है। यदि यह लाइब्रेरी के भविष्य के संस्करण में टूट गया है, तो आप जल्दी से नोटिस करेंगे।
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());
यह निम्नलिखित समाधान से बेहतर है जो आपको यह नहीं बताएगा कि आपकी धारणा सही थी या नहीं।
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();
}
कार्यक्रम के तर्क को लागू करने के लिए दावे का दुरुपयोग न करें
कभी-कभी उन बगों को उजागर करने के लिए उपयोग किया जाना चाहिए जो आपके आवेदन को तुरंत मारने के योग्य हैं। उन्हें किसी अन्य शर्त को सत्यापित करने के लिए उपयोग नहीं किया जाना चाहिए, भले ही उस स्थिति के लिए उपयुक्त प्रतिक्रिया भी तुरंत छोड़ दी जाए।
इसलिए, यह लिखें ...
if (!server_reachable())
{
log_message("server not reachable");
shutdown();
}
…उसके बदले में।
assert(server_reachable());
इसके अलावा कभी भी अविश्वासित इनपुट को सत्यापित करने के लिए या आप std::malloc
नहीं थे की जाँच return
करें nullptr
। यहां तक कि अगर आपको पता है कि आप कभी भी दावे को बंद नहीं करेंगे, यहां तक कि रिलीज़ बिल्ड में भी, एक पाठक पाठक को सूचित करता है कि यह कुछ ऐसा चेक करता है जो हमेशा सच होता है, यह देखते हुए कि प्रोग्राम बग-फ्री है और इसका कोई भी साइड-इफेक्ट नहीं है। यदि यह उस तरह का संदेश नहीं है जिसे आप संवाद करना चाहते हैं, तो throw
एक अपवाद के रूप में वैकल्पिक त्रुटि हैंडलिंग तंत्र का उपयोग करें । यदि आपको अपने गैर-अभिकथन की जाँच के लिए मैक्रो रैपर लगाना सुविधाजनक लगता है, तो एक लेखन को आगे बढ़ाएं। बस इसे "मुखर", "मान", "आवश्यकता", "सुनिश्चित" या ऐसा कुछ न कहें। इसका आंतरिक तर्क इसके समान हो सकता है assert
, सिवाय इसके कि इसे कभी संकलित नहीं किया जाता है, निश्चित रूप से।
अधिक जानकारी
मुझे जॉन लैकोस की बात डिफेंसिव प्रोग्रामिंग डेम राइट में मिली, जो CppCon'14 ( 1 सेंट भाग , 2 एन डी भाग ) में बहुत ही ज्ञानवर्धक है। वह इस बात को अनुकूलित करने का विचार लेता है कि कौन से दावे सक्षम हैं और कैसे इस जवाब में मैंने पहले भी असफल अपवादों पर प्रतिक्रिया दी।