Có thể viết quá nhiều khẳng định?


33

Tôi là một fan hâm mộ lớn của việc viết assertséc bằng mã C ++ như một cách để bắt các trường hợp trong quá trình phát triển không thể xảy ra nhưng có thể xảy ra do lỗi logic trong chương trình của tôi. Đây là một thực hành tốt nói chung.

Tuy nhiên, tôi nhận thấy rằng một số chức năng tôi viết (là một phần của lớp phức tạp) có hơn 5 xác nhận mà có vẻ như nó có khả năng là một thực tiễn lập trình xấu, về khả năng đọc và bảo trì. Tôi nghĩ nó vẫn rất tuyệt, vì mỗi người đòi hỏi tôi phải suy nghĩ về các điều kiện trước và sau của các chức năng và chúng thực sự giúp bắt lỗi. Tuy nhiên, tôi chỉ muốn đưa nó ra khỏi đó để hỏi liệu có một mô hình tốt hơn để bắt lỗi logic trong các trường hợp khi một số lượng lớn kiểm tra là cần thiết.

Nhận xét của Emacs : Vì Emacs là IDE mà tôi lựa chọn, tôi có một chút màu xám trong các tuyên bố khẳng định giúp giảm cảm giác lộn xộn mà chúng có thể cung cấp. Đây là những gì tôi thêm vào tệp .emacs của mình:

; 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)))))

3
Tôi phải thừa nhận rằng đây là một câu hỏi đã vượt qua tâm trí của tôi bây giờ và một lần nữa. Quan tâm để nghe ý kiến ​​của người khác về điều này.
Thuyền trưởng Sensible

Câu trả lời:


45

Tôi đã thấy hàng trăm lỗi sẽ được giải quyết nhanh hơn nếu ai đó đã viết nhiều xác nhận hơn , và không phải là một lỗi sẽ được giải quyết nhanh hơn bằng cách viết ít hơn .

[C] ould [quá nhiều khẳng định] có khả năng là một thực tiễn lập trình tồi, về khả năng đọc và bảo trì [?]

Khả năng đọc có thể là một vấn đề, có lẽ - mặc dù đó là kinh nghiệm của tôi khi những người viết khẳng định tốt cũng viết mã có thể đọc được. Và nó không bao giờ làm phiền tôi khi bắt đầu một hàm bắt đầu bằng một khối xác nhận để xác minh rằng các đối số không phải là rác - chỉ cần đặt một dòng trống sau nó.

Cũng theo kinh nghiệm của tôi, khả năng bảo trì luôn được cải thiện bằng các khẳng định, giống như bằng các bài kiểm tra đơn vị. Các xác nhận cung cấp một kiểm tra độ tỉnh táo rằng mã đang được sử dụng theo cách nó được sử dụng.


1
Câu trả lời tốt. Tôi cũng đã thêm một mô tả cho câu hỏi về cách tôi cải thiện khả năng đọc với Emacs.
Alan Turing

2
"Đó là kinh nghiệm của tôi khi những người viết khẳng định tốt cũng viết mã có thể đọc được" << điểm tuyệt vời. Làm cho mã có thể đọc được tùy thuộc vào từng lập trình viên vì đó là kỹ thuật mà anh ta hoặc cô ta không được phép sử dụng. Tôi đã thấy các kỹ thuật tốt trở nên không thể đọc được trong tay kẻ xấu, và ngay cả những gì hầu hết sẽ coi các kỹ thuật xấu trở nên hoàn toàn rõ ràng, thậm chí thanh lịch, bằng cách sử dụng trừu tượng và bình luận đúng cách.
Greg Jackson

Tôi đã có một vài sự cố ứng dụng được gây ra bởi các xác nhận sai lầm. Vì vậy, tôi đã thấy các lỗi sẽ không tồn tại nếu ai đó (bản thân tôi) đã viết ít xác nhận hơn.
CodeInChaos

@CodesInChaos Có thể cho rằng, lỗi chính tả, điều này chỉ ra một lỗi trong công thức của vấn đề - đó là lỗi trong thiết kế, do đó không khớp giữa các xác nhận và mã (khác).
Lawrence

12

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_MEDIUMMY_ASSERT_HIGHthay 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” assertvĩ 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_NONEMY_ASSERT_COST_ALLkhô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_LIMITtắ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_failedhoặ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_failedbằ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-march=nativelá 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_1sttest::positive_difference_2ndmà 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ố assertcâ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 ++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 classbấ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 privatehà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à constnoexcept, 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 ifcâu lệnh với returns 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::vectorhoạ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::mallockhông phải là returnbạ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 throwmộ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.


4
Assertions are great, but ... you will turn them off sooner or later.- Hy vọng sớm hơn, giống như trước khi tàu mã. Những thứ cần thiết để làm cho chương trình chết trong sản xuất nên là một phần của mã "thực", chứ không phải trong các xác nhận.
Blrfl

4

Tôi thấy rằng theo thời gian tôi viết ít khẳng định hơn vì nhiều trong số đó là "trình biên dịch đang hoạt động" và "là thư viện hoạt động". Khi bạn bắt đầu suy nghĩ về chính xác những gì bạn đang thử nghiệm, tôi nghi ngờ bạn sẽ viết ít xác nhận hơn.

Ví dụ: một phương thức (nói) thêm một cái gì đó vào bộ sưu tập không cần phải khẳng định rằng bộ sưu tập đó tồn tại - đó thường là điều kiện tiên quyết của lớp sở hữu thông điệp hoặc đó là một lỗi nghiêm trọng khiến nó trở lại với người dùng . Vì vậy, kiểm tra nó một lần, rất sớm, sau đó giả định nó.

Các xác nhận với tôi là một công cụ sửa lỗi và tôi thường sử dụng chúng theo hai cách: tìm lỗi tại bàn của tôi (và chúng không được kiểm tra. Chà, có lẽ là một trong những chìa khóa có thể); và tìm thấy một lỗi trên bàn của khách hàng (và họ đã được kiểm tra). Cả hai lần tôi đều sử dụng các xác nhận chủ yếu để tạo dấu vết ngăn xếp sau khi buộc ngoại lệ càng sớm càng tốt. Xin lưu ý rằng các xác nhận được sử dụng theo cách này có thể dễ dàng dẫn đến heisenbugs - lỗi có thể không bao giờ xảy ra trong bản dựng gỡ lỗi đã kích hoạt các xác nhận.


4
Tôi không hiểu ý của bạn khi bạn nói rằng đó thường là điều kiện tiên quyết của lớp sở hữu tin nhắn hoặc đó là một lỗi nghiêm trọng sẽ khiến nó trở lại với người dùng. Vì vậy, hãy kiểm tra nó một lần, từ rất sớm, sau đó giả sử nó. Bạn đang sử dụng các xác nhận để làm gì nếu không xác minh các giả định của bạn?
5gon12eder

4

Quá ít khẳng định: chúc may mắn thay đổi mã đó đánh đố với các giả định ẩn.

Quá nhiều xác nhận: có thể dẫn đến các vấn đề về khả năng đọc và có khả năng ngửi thấy mã - lớp, hàm, API có được thiết kế đúng không khi có quá nhiều giả định được đặt trong các câu lệnh khẳng định?

Cũng có thể có các xác nhận không thực sự kiểm tra bất cứ thứ gì hoặc kiểm tra những thứ như cài đặt trình biên dịch trong mỗi chức năng: /

Nhằm mục đích cho điểm ngọt ngào, nhưng không kém (như một người khác đã nói, "nhiều hơn" các xác nhận ít gây hại hơn là có quá ít hoặc thần giúp chúng ta - không có gì).


3

Thật tuyệt vời nếu bạn có thể viết một hàm Assert chỉ lấy tham chiếu đến phương thức CONST boolean, theo cách này, bạn chắc chắn rằng các xác nhận của bạn không có tác dụng phụ bằng cách đảm bảo rằng phương thức boolean const được sử dụng để kiểm tra xác nhận

nó sẽ rút ra một chút từ khả năng đọc, đặc biệt vì tôi không nghĩ rằng bạn không thể chú thích một lambda (trong c ++ 0x) để trở thành một const cho một số lớp, có nghĩa là bạn không thể sử dụng lambdas cho điều đó

quá mức nếu bạn hỏi tôi, nhưng nếu tôi bắt đầu thấy một mức độ cảnh giác nhất định do khẳng định tôi sẽ cảnh giác với hai điều:

  • đảm bảo không có tác dụng phụ nào xảy ra trong khẳng định (được cung cấp bởi một cấu trúc như được giải thích ở trên)
  • hiệu suất trong quá trình thử nghiệm phát triển; điều này có thể được giải quyết bằng cách thêm các cấp độ (như đăng nhập) vào cơ sở khẳng định; vì vậy bạn có thể vô hiệu hóa một số xác nhận từ bản dựng phát triển để cải thiện hiệu suất

2
Holy crap bạn thích từ "nhất định" và các dẫn xuất của nó. Tôi đếm 8 lần sử dụng.
Casey Patton

vâng, xin lỗi, tôi có xu hướng cố chấp quá nhiều từ - đã sửa, cảm ơn
lurscher

2

Tôi đã viết bằng C # nhiều hơn tôi đã làm trong C ++, nhưng hai ngôn ngữ không quá xa nhau. Trong .Net tôi sử dụng Asserts cho các điều kiện không nên xảy ra, nhưng tôi cũng thường ném ngoại lệ khi không có cách nào để tiếp tục. Trình gỡ lỗi VS2010 cho tôi thấy nhiều thông tin tốt về một ngoại lệ, bất kể bản dựng Phiên bản được tối ưu hóa như thế nào. Nó cũng là một ý tưởng tốt để thêm các bài kiểm tra đơn vị nếu bạn có thể. Đôi khi đăng nhập cũng là một điều tốt để có một trợ giúp gỡ lỗi.

Vì vậy, có thể có quá nhiều khẳng định? Vâng. Lựa chọn giữa Hủy bỏ / Bỏ qua / Tiếp tục 15 lần trong một phút sẽ gây khó chịu. Một ngoại lệ chỉ được ném một lần. Thật khó để định lượng điểm có quá nhiều khẳng định, nhưng nếu các xác nhận của bạn hoàn thành vai trò của các xác nhận, ngoại lệ, kiểm tra đơn vị và ghi nhật ký, thì có gì đó không đúng.

Tôi sẽ bảo lưu các xác nhận cho các tình huống không nên xảy ra. Bạn có thể quá khẳng định ban đầu, bởi vì các xác nhận viết nhanh hơn, nhưng lại xác định lại mã sau - biến một số trong số chúng thành ngoại lệ, một số thành kiểm tra, v.v. Nếu bạn có đủ kỷ luật để dọn sạch mọi bình luận TODO, thì hãy để lại bình luận bên cạnh mỗi người mà bạn dự định làm lại, và KHÔNG QUÊN để giải quyết TODO sau.


Nếu mã của bạn thất bại 15 xác nhận mỗi phút, tôi nghĩ có một vấn đề lớn hơn liên quan. Các xác nhận không bao giờ kích hoạt mã không có lỗi và chúng thực hiện, chúng nên giết ứng dụng để ngăn chặn thiệt hại thêm hoặc thả bạn vào trình gỡ lỗi để xem điều gì đang xảy ra.
5gon12eder

2

Tôi muốn làm việc với bạn! Một người viết rất nhiều assertslà tuyệt vời. Tôi không biết nếu có một thứ gọi là "quá nhiều". Phổ biến hơn nhiều đối với tôi là những người viết quá ít và cuối cùng gặp phải vấn đề UB chết người thỉnh thoảng chỉ xuất hiện trên một mặt trăng tròn có thể dễ dàng sao chép lặp đi lặp lại một cách đơn giản assert.

Tin nhắn thất bại

Một điều tôi có thể nghĩ đến là nhúng thông tin thất bại vào assertnếu bạn chưa làm điều đó, như vậy:

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

Bằng cách này, bạn có thể không còn cảm thấy mình có quá nhiều nếu bạn chưa làm điều này, vì bây giờ bạn đang nhận được sự khẳng định của mình để đóng vai trò mạnh mẽ hơn trong việc ghi lại các giả định và điều kiện tiên quyết.

Tác dụng phụ

Tất nhiên assertthực sự có thể bị lạm dụng và giới thiệu lỗi, như vậy:

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

... Nếu foo()gây ra tác dụng phụ, vì vậy bạn nên rất cẩn thận về điều đó, nhưng tôi chắc chắn rằng bạn đã là một người khẳng định rất tự do (một "người xác nhận có kinh nghiệm"). Hy vọng quy trình kiểm tra của bạn cũng tốt như sự chú ý cẩn thận của bạn để khẳng định các giả định.

Tốc độ gỡ lỗi

Mặc dù tốc độ gỡ lỗi thường nằm ở cuối danh sách ưu tiên của chúng tôi, nhưng một lần cuối cùng tôi đã khẳng định rất nhiều trong một cơ sở mã trước khi chạy bản dựng gỡ lỗi thông qua trình gỡ lỗi chậm hơn 100 lần so với phát hành.

Chủ yếu là vì tôi có các chức năng như thế này:

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]
    );
}

... Trong đó mỗi cuộc gọi operator[]sẽ thực hiện xác nhận kiểm tra giới hạn. Cuối cùng tôi đã thay thế một số trong những thứ quan trọng về hiệu năng bằng các tương đương không an toàn, không khẳng định chỉ để tăng tốc độ gỡ lỗi một cách quyết liệt với chi phí nhỏ để chỉ an toàn ở mức độ chi tiết thực hiện và chỉ vì tốc độ của nó bắt đầu làm giảm năng suất rất rõ rệt (làm cho lợi ích của việc gỡ lỗi nhanh hơn chi phí mất một vài khẳng định, nhưng chỉ đối với các chức năng như chức năng sản phẩm chéo này được sử dụng trong các đường dẫn quan trọng nhất, được đo lường, không phải operator[]nói chung).

Nguyên tắc trách nhiệm duy nhất

Mặc dù tôi không nghĩ rằng bạn thực sự có thể sai với nhiều khẳng định hơn (ít nhất là rất xa, tốt hơn là sai ở phía quá nhiều so với quá ít), bản thân các khẳng định đó có thể không phải là vấn đề nhưng có thể chỉ ra một.

Ví dụ, nếu bạn có 5 xác nhận cho một lệnh gọi hàm, thì nó có thể làm quá nhiều. Giao diện của nó có thể có quá nhiều điều kiện tiên quyết và tham số đầu vào, ví dụ: tôi cho rằng không liên quan đến chủ đề cấu thành một số xác nhận lành mạnh (mà tôi thường trả lời, "càng nhiều càng tốt!"), Nhưng đó có thể là một lá cờ đỏ có thể (hoặc rất có thể không).


1
Vâng, có thể có "quá nhiều" khẳng định trong lý thuyết, mặc dù vấn đề đó trở nên rõ ràng rất nhanh: Nếu khẳng định đó mất nhiều thời gian hơn đáng kể so với thịt của hàm. Phải thừa nhận rằng, tôi không thể nhớ rằng đã tìm thấy rằng trong tự nhiên, vấn đề ngược lại là phổ biến.
Ded repeatator

@Ded repeatator Ah yeah, tôi đã gặp trường hợp đó trong các thói quen toán học vectơ quan trọng đó. Mặc dù nó chắc chắn có vẻ tốt hơn rất nhiều để sai ở phía của quá nhiều hơn là quá ít!

-1

Rất hợp lý để thêm kiểm tra vào mã của bạn. Đối với xác nhận đơn giản (trình biên dịch được tích hợp trong trình biên dịch C và C ++) mô hình sử dụng của tôi là một xác nhận thất bại có nghĩa là có một lỗi trong mã cần được sửa. Tôi diễn giải điều này một chút hào phóng; nếu tôi mong đợi một yêu cầu web để trả lại trạng thái 200 và khẳng định cho điều đó mà không xử lý các trường hợp khác sau đó một sự khẳng định thất bại không thực sự hiển thị một lỗi trong mã của tôi, do đó khẳng định là hợp lý.

Vì vậy, khi mọi người nói một khẳng định rằng chỉ kiểm tra những gì mã làm là thừa thì điều đó không hoàn toàn đúng. Điều đó khẳng định kiểm tra những gì họ nghĩ rằng mã làm, và toàn bộ quan điểm của khẳng định là kiểm tra xem giả định không có lỗi trong mã là đúng. Và khẳng định có thể phục vụ như là tài liệu là tốt. Nếu tôi giả sử rằng sau khi thực hiện một vòng lặp i == n và nó không rõ ràng 100% từ mã, thì "khẳng định (i == n)" sẽ hữu ích.

Tốt hơn là có nhiều hơn là chỉ "khẳng định" trong tiết mục của bạn để xử lý các tình huống khác nhau. Ví dụ, tình huống tôi kiểm tra xem có điều gì đó không xảy ra sẽ chỉ ra lỗi hay không, nhưng vẫn tiếp tục khắc phục tình trạng đó. (Ví dụ: nếu tôi sử dụng một số bộ đệm thì tôi có thể kiểm tra lỗi và nếu xảy ra lỗi bất ngờ, có thể an toàn để sửa lỗi bằng cách ném bộ đệm đi. Tôi muốn một cái gì đó gần như khẳng định, điều đó cho tôi biết trong quá trình phát triển , và vẫn để tôi tiếp tục.

Một ví dụ khác là tình huống mà tôi không mong muốn điều gì đó xảy ra, tôi có một cách giải quyết chung, nhưng nếu điều này xảy ra, tôi muốn biết về nó và kiểm tra nó. Một lần nữa một cái gì đó gần giống như một khẳng định, điều đó sẽ cho tôi biết trong quá trình phát triển. Nhưng không hoàn toàn khẳng định.

Quá nhiều khẳng định: Nếu một xác nhận làm hỏng chương trình của bạn khi nó nằm trong tay người dùng, thì bạn không được có bất kỳ xác nhận nào bị sập vì âm bản giả.


-3

Nó phụ thuộc. Nếu các yêu cầu mã được ghi lại rõ ràng, thì xác nhận phải luôn phù hợp với các yêu cầu. Trong trường hợp đó là một điều tốt. Tuy nhiên, nếu không có yêu cầu hoặc yêu cầu bằng văn bản xấu, thì các lập trình viên mới sẽ khó chỉnh sửa mã mà không phải tham khảo bài kiểm tra đơn vị mỗi lần để tìm ra yêu cầu là gì.


3
điều này dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong 8 câu trả lời trước
gnat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.