Có thể viết quá nhiều khẳng định?
Vâng, tất nhiên nó được. [Hãy tưởng tượng ví dụ đáng ghét ở đây.] Tuy nhiên, áp dụng các hướng dẫn chi tiết sau đây, bạn không nên gặp khó khăn khi đẩy giới hạn đó vào thực tế. Tôi cũng là một fan hâm mộ lớn của các xác nhận, và tôi sử dụng chúng theo các nguyên tắc này. Phần lớn lời khuyên này không đặc biệt đối với các xác nhận mà chỉ áp dụng thực hành kỹ thuật tốt nói chung cho chúng.
Giữ cho thời gian chạy và dấu chân nhị phân trên đầu trong tâm trí
Các xác nhận là tuyệt vời, nhưng nếu chúng làm cho chương trình của bạn chậm không thể chấp nhận được, nó sẽ rất khó chịu hoặc bạn sẽ tắt chúng sớm hay muộn.
Tôi muốn đánh giá chi phí của một xác nhận liên quan đến chi phí của chức năng mà nó được chứa trong đó. Hãy xem xét hai ví dụ sau đây.
// 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();
}
Bản thân hàm là một hoạt động O (1) nhưng các xác nhận chiếm tài khoản O ( n ). Tôi không nghĩ rằng bạn muốn kiểm tra như vậy được hoạt động trừ khi trong những trường hợp rất đặc biệt.
Đây là một chức năng khác với các xác nhận tương tự.
// 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_)));
}
Bản thân hàm là một hoạt động O ( n ) vì vậy sẽ đau hơn rất nhiều khi thêm một chi phí O ( n ) bổ sung cho xác nhận. Làm chậm một hàm bởi một yếu tố nhỏ (trong trường hợp này, có thể ít hơn 3) là một yếu tố chúng ta thường có thể có trong một bản dựng gỡ lỗi nhưng có thể không phải là bản dựng phát hành.
Bây giờ hãy xem xét ví dụ này.
// 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();
}
Mặc dù nhiều người có thể sẽ thoải mái hơn nhiều với khẳng định O (1) này so với hai xác nhận O ( n ) trong ví dụ trước, nhưng chúng tương đương về mặt đạo đức theo quan điểm của tôi. Mỗi bổ sung thêm chi phí theo thứ tự độ phức tạp của hàm.
Cuối cùng, có những khẳng định giá rẻ thực sự của những người bị chi phối bởi sự phức tạp của chức năng mà chúng chứa trong đó.
// 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;
}
Ở đây, chúng ta có hai xác nhận O (1) trong hàm O ( n ). Có lẽ sẽ không có vấn đề gì trong việc duy trì chi phí này ngay cả trong các bản dựng phát hành.
Do giữ trong tâm trí, tuy nhiên, sự phức tạp tiệm cận không phải luôn luôn đưa ra một ước tính đầy đủ bởi vì trong thực tế, chúng tôi luôn làm việc với kích thước đầu vào bao quanh bởi một số hữu hạn liên tục và các yếu tố liên tục ẩn bởi “Big- O ” rất tốt có thể là không đáng kể.
Vì vậy, bây giờ chúng tôi đã xác định các kịch bản khác nhau, chúng tôi có thể làm gì về chúng? Một cách tiếp cận dễ dàng (có lẽ cũng vậy) sẽ tuân theo một quy tắc, chẳng hạn như Không sử dụng các xác nhận chi phối chức năng mà chúng có trong. Hồi Trong khi nó có thể hoạt động cho một số dự án, các dự án khác có thể cần một cách tiếp cận khác biệt hơn. Điều này có thể được thực hiện bằng cách sử dụng các macro xác nhận khác nhau cho các trường hợp khác nhau.
#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;
}
Bây giờ bạn có thể sử dụng ba macro MY_ASSERT_LOW
, MY_ASSERT_MEDIUM
và MY_ASSERT_HIGH
thay vì các thư viện chuẩn của “một kích thước phù hợp với tất cả các” assert
vĩ mô cho khẳng định rằng bị chi phối bởi, không bị chi phối bởi cũng không thống trị và chi phối sự phức tạp của chức năng chứa của họ tương ứng. Khi bạn xây dựng phần mềm, bạn có thể xác định trước biểu tượng bộ xử lý trước MY_ASSERT_COST_LIMIT
để chọn loại xác nhận nào sẽ đưa phần mềm vào thực thi. Các hằng số MY_ASSERT_COST_NONE
và MY_ASSERT_COST_ALL
không tương ứng với bất kỳ macro xác nhận nào và được sử dụng làm giá trị để MY_ASSERT_COST_LIMIT
tắt tất cả các xác nhận hoặc tắt tương ứng.
Chúng tôi dựa vào giả định ở đây rằng một trình biên dịch tốt sẽ không tạo ra bất kỳ mã nào cho
if (false_constant_expression && run_time_expression) { /* ... */ }
và biến đổi
if (true_constant_expression && run_time_expression) { /* ... */ }
vào
if (run_time_expression) { /* ... */ }
mà tôi tin là một giả định an toàn ngày nay.
Nếu bạn sắp sửa mã ở trên, hãy xem xét các chú thích dành riêng cho trình biên dịch như __attribute__ ((cold))
trên my::assertion_failed
hoặc __builtin_expect(…, false)
trên !(CONDITION)
để giảm chi phí cho các xác nhận đã qua. Trong các bản dựng phát hành, bạn cũng có thể xem xét thay thế lời gọi hàm my::assertion_failed
bằng một cái gì đó như __builtin_trap
để giảm dấu chân khi bất tiện mất thông báo chẩn đoán.
Các loại tối ưu hóa này thực sự chỉ có liên quan trong các xác nhận cực kỳ rẻ (như so sánh hai số nguyên đã được đưa ra làm đối số) trong một hàm rất nhỏ gọn, không xem xét kích thước bổ sung của nhị phân được tích lũy bằng cách kết hợp tất cả các chuỗi thông báo.
So sánh cách mã này
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;
}
được biên dịch thành phần sau
_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:
trong khi đoạn mã sau
int
positive_difference_2nd(const int a, const int b) noexcept
{
if (__builtin_expect(!(a > b), false))
__builtin_trap();
return a - b;
}
đưa ra lắp ráp này
_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:
mà tôi cảm thấy thoải mái hơn nhiều (Ví dụ đã được thử nghiệm với GCC 5.3.0 bằng cách sử dụng -std=c++14
, -O3
và -march=native
lá cờ trên 4.3.3-2-ARCH x86_64 GNU / Linux. Không thể hiện trong các đoạn trên là tờ khai của test::positive_difference_1st
và test::positive_difference_2nd
mà tôi thêm __attribute__ ((hot))
vào. my::assertion_failed
Được tuyên bố với __attribute__ ((cold))
.)
Khẳng định các điều kiện tiên quyết trong hàm phụ thuộc vào chúng
Giả sử bạn có chức năng sau với hợp đồng được chỉ định.
/**
* @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;
Thay vì viết
assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);
tại mỗi trang web cuộc gọi, hãy đặt logic đó một lần vào định nghĩa của 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;
}
và gọi nó mà không cần thêm ado.
const auto frequency = count_letters(text, letter);
Điều này có những lợi thế sau.
- Bạn chỉ cần viết mã xác nhận một lần. Vì mục đích chính của các hàm là chúng được gọi - thường là nhiều hơn một lần - điều này sẽ làm giảm tổng số
assert
câu lệnh trong mã của bạn.
- Nó giữ logic kiểm tra các điều kiện tiên quyết gần với logic phụ thuộc vào chúng. Tôi nghĩ rằng đây là khía cạnh quan trọng nhất. Nếu khách hàng của bạn sử dụng sai giao diện của bạn, họ không thể được giả định áp dụng các xác nhận một cách chính xác để chức năng nói với họ tốt hơn.
Nhược điểm rõ ràng là bạn sẽ không nhận được vị trí nguồn của trang gọi vào thông báo chẩn đoán. Tôi tin rằng đây là một vấn đề nhỏ. Một trình sửa lỗi tốt sẽ có thể cho phép bạn truy nguyên nguồn gốc của vi phạm hợp đồng một cách thuận tiện.
Suy nghĩ tương tự áp dụng cho các chức năng đặc biệt của Wap như các nhà khai thác quá tải. Khi tôi viết các trình vòng lặp, tôi thường - nếu bản chất của trình vòng lặp cho phép nó - cung cấp cho chúng một hàm thành viên
bool
good() const noexcept;
cho phép hỏi liệu có an toàn để hủy đăng ký lặp không. (Tất nhiên, trong thực tế, hầu như chỉ có thể đảm bảo rằng nó sẽ không an toàn khi hủy đăng ký. Nhưng tôi tin rằng bạn vẫn có thể bắt được rất nhiều lỗi với chức năng như vậy.) Thay vì xả rác tất cả mã của tôi sử dụng trình lặp với các assert(iter.good())
câu lệnh, tôi muốn đặt một assert(this->good())
dòng làm dòng đầu tiên operator*
trong quá trình thực hiện của trình vòng lặp.
Nếu bạn đang sử dụng thư viện chuẩn, thay vì xác nhận thủ công các điều kiện tiên quyết trong mã nguồn của mình, hãy bật kiểm tra của họ trong các bản dựng gỡ lỗi. Họ thậm chí có thể thực hiện các kiểm tra tinh vi hơn như kiểm tra xem liệu container mà iterator đề cập đến có còn tồn tại hay không. (Xem tài liệu về libstdc ++ và libc ++ (đang tiến hành) để biết thêm thông tin.)
Yếu tố chung điều kiện ra
Giả sử bạn đang viết một gói đại số tuyến tính. Nhiều chức năng sẽ có các điều kiện tiên quyết phức tạp và vi phạm chúng thường sẽ gây ra kết quả sai mà không thể nhận ra ngay lập tức như vậy. Sẽ rất tốt nếu các chức năng này khẳng định điều kiện tiên quyết của chúng. Nếu bạn xác định một loạt các vị từ cho bạn biết các thuộc tính nhất định về cấu trúc, các xác nhận đó sẽ trở nên dễ đọc hơn nhiều.
template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
assert(is_square(m) && is_symmetric(m));
// TODO: Somehow decompose that thing...
}
Nó cũng sẽ cung cấp cho các thông báo lỗi hữu ích hơn.
cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)
giúp đỡ nhiều hơn, nói
detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)
nơi đầu tiên bạn phải xem mã nguồn trong ngữ cảnh để tìm ra cái gì đã được thử nghiệm.
Nếu bạn có một class
bất biến không tầm thường, có lẽ bạn nên thỉnh thoảng khẳng định chúng khi bạn gặp rắc rối với trạng thái bên trong và muốn đảm bảo rằng bạn sẽ rời khỏi đối tượng ở trạng thái hợp lệ.
Với mục đích này, tôi thấy hữu ích khi xác định private
hàm thành viên mà tôi thường gọi class_invaraiants_hold_
. Giả sử bạn đang thực hiện lại std::vector
(Bởi vì tất cả chúng ta đều biết nó không đủ tốt.), Nó có thể có chức năng như thế này.
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;
}
Lưu ý một vài điều về điều này.
- Chính chức năng vị ngữ là
const
và noexcept
, theo hướng dẫn rằng các xác nhận sẽ không có tác dụng phụ. Nếu nó có ý nghĩa, cũng tuyên bố nó constexpr
.
- Vị ngữ không khẳng định bất cứ điều gì. Nó có nghĩa là được gọi là xác nhận bên trong , chẳng hạn như
assert(this->class_invariants_hold_())
. Bằng cách này, nếu các xác nhận được biên dịch, chúng tôi có thể chắc chắn rằng không có chi phí phát sinh trong thời gian chạy.
- Luồng điều khiển bên trong hàm được chia thành nhiều
if
câu lệnh với return
s sớm hơn là một biểu thức lớn. Điều này giúp dễ dàng chuyển qua chức năng trong trình gỡ lỗi và tìm ra phần nào của bất biến đã bị phá vỡ nếu xác nhận kích hoạt.
Đừng khẳng định những điều ngớ ngẩn
Một số điều không có ý nghĩa để khẳng định.
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ác xác nhận này không làm cho mã thậm chí dễ đọc hơn một chút hoặc dễ hiểu hơn. Mỗi lập trình viên C ++ nên đủ tự tin về cách thức std::vector
hoạt động để chắc chắn rằng đoạn mã trên là chính xác chỉ bằng cách nhìn vào nó. Tôi không nói rằng bạn không bao giờ nên khẳng định kích thước của container. Nếu bạn đã thêm hoặc loại bỏ các phần tử bằng cách sử dụng một số luồng điều khiển không tầm thường, thì một xác nhận như vậy có thể hữu ích. Nhưng nếu nó chỉ lặp lại những gì được viết bằng mã không khẳng định ngay phía trên, thì không có giá trị nào đạt được.
Cũng không khẳng định rằng các chức năng thư viện hoạt động chính xác.
auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled()); // probably silly
Nếu bạn tin tưởng thư viện đó ít, tốt hơn nên xem xét sử dụng thư viện khác thay thế.
Mặt khác, nếu tài liệu của thư viện không rõ ràng 100% và bạn có được sự tự tin về các hợp đồng của nó bằng cách đọc mã nguồn, thì sẽ rất có ý nghĩa để khẳng định về điều đó. Nếu nó bị hỏng trong phiên bản tương lai của thư viện, bạn sẽ nhanh chóng nhận thấy.
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());
Điều này tốt hơn giải pháp sau đây sẽ không cho bạn biết liệu các giả định của bạn có đúng hay không.
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();
}
Đừng lạm dụng các xác nhận để thực hiện logic chương trình
Các xác nhận chỉ nên được sử dụng để phát hiện ra các lỗi đáng để giết ngay ứng dụng của bạn. Chúng không nên được sử dụng để xác minh bất kỳ điều kiện nào khác ngay cả khi phản ứng thích hợp với điều kiện đó cũng sẽ được bỏ ngay lập tức.
Do đó, hãy viết bài này
if (!server_reachable())
{
log_message("server not reachable");
shutdown();
}
…thay vì đó.
assert(server_reachable());
Cũng không bao giờ sử dụng các xác nhận để xác thực đầu vào không đáng tin cậy hoặc kiểm tra mà std::malloc
không phải là return
bạn nullptr
. Ngay cả khi bạn biết rằng bạn sẽ không bao giờ tắt các xác nhận, ngay cả trong các bản phát hành, một xác nhận sẽ thông báo cho người đọc rằng nó sẽ kiểm tra một cái gì đó luôn luôn đúng khi chương trình không có lỗi và không có tác dụng phụ rõ ràng. Nếu đây không phải là loại tin nhắn bạn muốn liên lạc, hãy sử dụng cơ chế xử lý lỗi thay thế, chẳng hạn như lấy throw
một ngoại lệ. Nếu bạn thấy thuận tiện khi có một trình bao bọc macro cho các kiểm tra không xác nhận của mình, hãy tiếp tục viết một. Chỉ cần đừng gọi nó là khẳng định, và giả sử, đó là yêu cầu, phải đảm bảo, hay một thứ gì đó tương tự. Tất nhiên logic bên trong của nó có thể giống như đối với assert
, ngoại trừ việc nó không bao giờ được biên dịch ra, tất nhiên.
Thêm thông tin
Tôi thấy nói chuyện John Lakos' Defensive Programming Xong Ngay , được đưa ra tại CppCon'14 ( 1 st phần , 2 nd phần ) rất sáng sủa. Anh ta lấy ý tưởng tùy chỉnh những xác nhận nào được kích hoạt và cách phản ứng với những ngoại lệ thất bại thậm chí còn hơn cả tôi đã làm trong câu trả lời này.