너무 많은 주장을 쓸 수 있습니까?


33

assert개발 중에 발생할 수는 없지만 프로그램의 논리 버그로 인해 발생하는 사례를 포착하는 방법으로 C ++ 코드로 검사 를 작성 하는 것을 좋아합니다. 이것은 일반적으로 좋은 습관입니다.

그러나 필자가 작성한 일부 함수 (복잡한 클래스의 일부)에는 가독성 및 유지 관리 측면에서 프로그래밍이 잘못 될 수 있다고 생각되는 5 개 이상의 어설 션이 있음을 알았습니다. 나는 각각의 기능이 사전 및 사후 조건에 대해 생각해야하고 실제로 버그를 잡는 데 도움이되기 때문에 여전히 훌륭하다고 생각합니다. 그러나 많은 수의 검사가 필요한 경우 논리 오류를 잡기위한 더 나은 패러다임이 있는지 묻기 위해 이것을 제시하고 싶었습니다.

Emacs 의견 : Emacs가 내가 선택한 IDE이기 때문에, 그들이 제공 할 수있는 어수선한 느낌을 줄이는 데 도움이되는 주장 진술을 약간 흐리게 표시합니다. 내 .emacs 파일에 추가 한 내용은 다음과 같습니다.

; 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
나는 이것이 계속해서 내 마음을 넘어서는 질문이라는 것을 인정해야한다. 이것에 대해 다른 사람들의 의견을 듣고 싶습니다.
Captain Sensible

답변:


45

누군가가 더 많은 어설 션 을 작성한 경우 더 빨리 해결 될 수있는 수백 가지 버그를 보았으며 적은 수의 글을 써서 빨리 해결 될 수있는 단일 버그는 아닙니다 .

[C] 가독성과 유지 보수성 측면에서 [너무 많은 주장이] 잠재적으로 나쁜 프로그래밍 관행 일 수 있는가 [?]

가독성은 문제가 될 수 있습니다. 좋은 주장을하는 사람들도 읽을 수있는 코드를 작성하는 것이 저의 경험이긴 했지만요. 그리고 함수의 시작이 인수 블록으로 시작하여 인수가 가비지 않은지 확인하는 것을 결코 귀찮게하지 않습니다. 그 뒤에 빈 줄을 넣으십시오.

또한 내 경험상 단위 테스트와 마찬가지로 어설 션을 통해 유지 관리 성이 항상 향상됩니다. 어설 션은 코드가 의도 된 방식으로 사용되고 있음을 확인합니다.


1
좋은 대답입니다. 또한 Emacs로 가독성을 향상시키는 방법에 대한 설명을 추가했습니다.
Alan Turing

2
"좋은 주장을 쓰는 사람들도 읽을 수있는 코드를 작성하는 것이 저의 경험이었습니다." 코드를 읽을 수있게 만드는 것은 자신이 사용하는 기술이기 때문에 개별 프로그래머에게 달려 있습니다. 나는 좋은 기술을 잘못 읽을 수없는 것으로 보았으며, 추상화와 주석을 적절히 사용하여 나쁜 기술을 완벽하게 명확하고 우아하게 생각하는 것조차 대부분을 보았습니다.
Greg Jackson

잘못된 어설 션으로 인해 몇 가지 응용 프로그램 충돌이 발생했습니다. 그래서 누군가 (나 자신)가 더 적은 주장을 쓴다면 존재 하지 않았던 버그를 보았습니다 .
코드 InChaos

@CodesInChaos 논란의 여지가 있지만, 이것은 문제 의 공식화 에 오류가 있음을 나타냅니다. 즉, 버그는 디자인에 있었으므로 어설 션과 (다른) 코드 사이의 불일치입니다.
로렌스

12

너무 많은 주장을 쓸 수 있습니까?

물론입니다. [여기서 명백한 예를 상상해보십시오.] 그러나 다음에 자세히 설명 된 지침을 적용하면 실제로 그 한계를 뛰어 넘는 데 어려움이 없어야합니다. 나는 또한 많은 주장을 좋아하며 이러한 원칙에 따라 사용한다. 이 충고의 대부분은 단언에 특화된 것이 아니라 일반적인 공학적 관행에만 적용됩니다.

런타임 및 이진 풋 프린트 오버 헤드를 명심하십시오

어설 션은 훌륭하지만 프로그램을 허용 할 수 없을 정도로 느리게 만들면 매우 성가 시거나 조만간 끌 수 있습니다.

포함 된 함수 비용과 관련된 어설 션 비용을 측정하고 싶습니다. 다음 두 가지 예를 고려하십시오.

// 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();
}

함수 자체는 O (1) 연산이지만 어설 션은 O ( n ) 오버 헤드를 설명합니다. 매우 특별한 상황이 아닌 한 그러한 검사가 활성화되기를 원치 않습니다.

유사한 어설 션을 가진 또 다른 함수가 있습니다.

// 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 ( n ) 어설 션 보다이 O (1) 어설 션에 훨씬 더 편할 것 같지만, 필자의 견해로는 도덕적으로 동일합니다. 각각은 기능 자체의 복잡성에 따라 오버 헤드를 추가합니다.

마지막으로, 포함 된 기능의 복잡성에 의해 지배되는“정말로 싼”주장이 있습니다.

// 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) 어설 션이 있습니다. 릴리스 빌드에서도이 오버 헤드를 유지하는 것은 문제가되지 않습니다.

그러나 점근 적 복잡성이 항상 적절한 추정치를 제공하는 것은 아니라는 점을 명심하십시오. 실제로 "Big- O "에 의해 숨겨진 일부 유한 상수 및 상수 요소에 의해 제한된 입력 크기를 다루는 것은 무시할 수 없을 수 있기 때문입니다.

이제 우리는 서로 다른 시나리오를 식별했습니다. 어떻게해야합니까? "아마도 너무 쉬운"접근 방식은 "포함 된 기능을 지배하는 주장을 사용하지 마십시오"와 같은 규칙을 따르는 것입니다. 일부 프로젝트에서는 작동하지만 다른 프로젝트에서는보다 차별화 된 접근 방식이 필요할 수 있습니다. 다른 경우에 다른 어설 션 매크로를 사용하여 수행 할 수 있습니다.

#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))on my::assertion_failed또는 __builtin_expect(…, false)on 과 같은 컴파일러 특정 주석을 고려 !(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:

나는 훨씬 더 편안하다고 느낍니다. (예를 들어 4.3.3-2-ARCH x86_64 GNU / Linux 에서 -std=c++14, -O3-march=native플래그를 사용하여 GCC 5.3.0으로 테스트했습니다 . 위의 스 니펫에는 선언되어 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;

반복자를 역 참조하는 것이 안전한지 묻습니다. (물론 실제로 는 반복자를 역 참조하는 것이 안전 하지 않다는 것을 보장하는 것이 거의 항상 가능 합니다. 그러나 그러한 함수로 여전히 많은 버그를 잡을 수 있다고 생각합니다.) 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;
}

이것에 대해 몇 가지를 주목하십시오.

  • 술어 함수 자체이다 constnoexcept주장은 부작용이 없다라는 지침에 따라. 의미가 있다면 선언하십시오 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는 물론 컴파일되지 않는다는 점을 제외하고는와 동일 할 수 있습니다 .

더 많은 정보

나는 존 Lakos '이야기 발견 방어 프로그래밍 완료를 마우스 오른쪽 CppCon'14 (에 주어진 1 개 번째 부분 , 2 부분 ) 매우 계몽을. 그는이 어설 션에서 사용 가능한 어설 션과 실패한 예외에 대한 대응 방법을 사용자 지정하는 아이디어를 취합니다.


4
Assertions are great, but ... you will turn them off sooner or later.-코드가 출시되기 전과 같이 빨리 바랍니다. 프로그램을 프로덕션 환경에서 죽게 만드는 것은 어설 션이 아닌 "실제"코드의 일부 여야합니다.
Blrfl

4

시간이 지남에 따라 더 많은 어설 션을 작성한다는 사실을 알았습니다. 그 중 다수는 "컴파일러 작동"및 "라이브러리 작동"에 해당하기 때문입니다. 정확히 당신이 테스트하는 것에 대해 생각하기 시작하면 나는 당신이 적은 주장을 쓸 것이라고 생각합니다.

예를 들어, 컬렉션에 무언가를 추가하는 메소드는 컬렉션이 존재한다고 주장 할 필요가 없습니다. 일반적으로 메시지를 소유하는 클래스의 전제 조건이거나 사용자에게 다시 보내야하는 치명적인 오류입니다 . 따라서 매우 일찍 한 번 확인한 다음 가정하십시오.

나에게 주장하는 것은 디버깅 도구이며, 일반적으로 두 가지 방법으로 사용할 것이다. 내 책상에서 버그를 찾는다. 그리고 그들은 체크인되지 않는다. 고객의 책상에서 버그를 발견하고 체크인합니다. 두 번 모두 가능한 빨리 예외를 강제 실행 한 후 스택 추적을 생성하기 위해 어설 션을 사용하고 있습니다. 이 방법으로 사용 된 어설 션은 heisenbugs로 쉽게 이어질 수 있습니다 . 어설 션이 활성화 된 디버그 빌드에서는 버그가 발생하지 않을 수 있습니다.


4
당신이 말할 때 나는 당신의 요점을 파악하지 않는 중 하나 일반적으로 메시지를 소유하는 클래스의 전제 조건이다하거나 사용자에게 다시 그것을 확인해야합니다 치명적인 오류가 있다고. " 따라서 한 번 확인한 다음 가정하십시오.” 가정을 검증하지 않은 경우 어설 션을 사용하는 것은 무엇입니까?
5gon12eder

4

주장이 너무 적습니다. 숨겨진 가정으로 수수께끼를 바꾼 행운을 빌어 요.

어설 션이 너무 많음 : 가독성 문제 및 코드 냄새가 발생할 수 있음-어설 션 문에 가정이 너무 많을 때 클래스, 함수, API가 올바르게 설계 되었습니까?

실제로 각 기능에서 컴파일러 설정과 같은 것을 확인하거나 확인하지 않는 어설 션이있을 수 있습니다.

스위트 스폿을 목표로하되 그 이상은 아닙니다. (다른 사람이 이미 말했듯이 어설 션의 "더 많은 것"은 너무 적거나 신의 도움을받는 것보다 덜 해 롭습니다.


3

부울 CONST 메소드에 대한 참조 만 취하는 Assert 함수를 작성할 수 있다면 정말 좋을 것입니다. 이런 식으로 부울 const 메소드를 사용하여 어설트를 테스트함으로써 어설트에 부작용이없는 것이 확실합니다.

가독성에서 조금 끌 것입니다. 특히 람다 (C ++ 0x에서)에 주석을 달아 클래스의 const가 될 수 없다고 생각하기 때문에 람다를 사용할 수 없습니다.

당신이 나에게 물어 보면 과잉이지만, 주장으로 인해 일정 수준의 오염을 보게되면 두 가지를 조심해야합니다.

  • assert에서 부작용이 발생하지 않도록 확인 (위에서 설명한 구성으로 제공)
  • 개발 테스트 중 성능; 이는 어설 션 기능에 레벨 (예 : 로깅)을 추가하여 해결할 수 있습니다. 따라서 성능을 향상시키기 위해 개발 빌드에서 일부 어설 션을 비활성화 할 수 있습니다

2
거룩한 쓰레기는 "확실한"이라는 단어와 그 파생어를 좋아합니다. 8 번 사용합니다.
Casey Patton

예, 죄송합니다. 너무 많은 단어를 수정하는 경향이 있습니다-고정, 감사합니다
lurscher

2

C ++에서보다 훨씬 더 C #으로 작성했지만 두 언어가 크게 멀지 않습니다. .Net에서는 발생하지 않아야하는 조건에 대해 어설 션을 사용하지만 계속할 방법이없는 경우 종종 예외가 발생합니다. VS2010 디버거는 릴리스 빌드의 최적화에 관계없이 예외에 대한 많은 정보를 보여줍니다. 가능하면 단위 테스트를 추가하는 것도 좋습니다. 때로는 로깅도 디버깅 지원으로 사용하는 것이 좋습니다.

그렇다면 너무 많은 주장이있을 수 있습니까? 예. 중단 / 무시 / 계속 사이에서 1 분 안에 15 번을 선택하면 성 가실 수 있습니다. 예외는 한 번만 발생합니다. 어설 션이 너무 많은 지점을 정량화하기는 어렵지만 어설 션이 어설 션, 예외, 단위 테스트 및 로깅의 역할을 수행하는 경우 문제가 있습니다.

발생하지 않아야 할 시나리오에 대한 어설 션을 예약합니다. 어설 션을 작성하는 것이 더 빠르기 때문에 초기에 과도하게 어설 션 할 수 있지만 나중에 코드를 리팩터링하십시오. 일부는 예외, 일부는 테스트 등으로 바꾸십시오. 모든 TODO 주석을 정리하기에 충분한 규율이 ​​있으면 재 작업 할 각 항목 옆에 주석을 달고, 나중에 TODO를 다루는 것을 잊지 마십시오.


코드가 분당 15 개의 어설 션에 실패하면 더 큰 문제가 있다고 생각합니다. 어설 션은 버그가없는 코드에서 실행되지 않아야하며, 그렇게하면 추가 손상을 방지하기 위해 응용 프로그램을 종료하거나 디버거로 넘어 가서 진행 상황을 확인해야합니다.
5gon12eder

2

당신과 함께 일하고 싶습니다! 많이 쓰는 사람asserts 은 환상적입니다. "너무 많은 것"같은 것이 있는지 모르겠습니다. 너무 적은 글을 쓰고 궁극적으로 가끔씩 치명적인 UB 문제를 겪는 사람들이 훨씬 더 흔합니다 assert.

실패 메시지

내가 생각할 수있는 한 가지는 실패 정보를 assert아직 수행하지 않은 경우 에 포함시키는 것입니다 .

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

이렇게하면 가정과 전제 조건을 문서화하는 데있어 더 강력한 역할을 수행 할 수있게되므로 아직이 작업을하지 않았다면 더 이상 너무 많이 느끼지 않을 수 있습니다.

부작용

당연하지 assert 실제로 잘못 사용되어 다음과 같은 오류가 발생할 수 있습니다.

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

... 만약 foo() 부작용을 유발할 매우주의해야하지만, 이미 귀하가 매우 자유로이 주장하는 사람 ( "경험있는 어설 터")이라고 확신합니다. 테스트 절차가 가정을 주장하는 데주의를 기울이는 것만 큼 좋기를 바랍니다.

디버깅 속도

디버깅 속도는 일반적으로 우선 순위 목록의 맨 아래에 있어야하지만 한 번은 디버거를 통한 디버그 빌드 실행이 끝나기 전에 코드베이스에서 너무 많이 주장했습니다. 것이 릴리스보다 100 배 이상 느리기 했습니다.

주로 다음과 같은 기능이 있기 때문입니다.

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

... 매번 호출 할 때마다 operator[]경계 검사 어설 션을 수행합니다. 나는 성능에 중요한 것들 중 일부를 안전하지 않은 동등한 것으로 대체하여 사소한 비용으로 구현 세부 사항 수준의 안전을 위해 디버깅 비용을 크게 높이 지 않는 것이 안전하지 않은 동등한 것으로 대체했습니다. 생산성을 매우 현저하게 떨어 뜨리기 위해 (더 빠른 디버깅의 이점은 몇 가지 주장을 잃는 비용보다 중요하지만, operator[]일반적으로가 아니라 가장 중요하고 측정 된 경로에서 사용되는이 교차 제품 기능과 같은 기능에 대해서만 ).

단일 책임 원칙

나는 당신이 더 많은 주장으로 잘못 갈 수 있다고 생각하지는 않지만 (적어도 너무 많은 것보다 잘못하는 것이 훨씬 낫습니다.) 주장 자체는 문제가되지 않지만 하나를 나타낼 수 있습니다.

예를 들어 단일 함수 호출에 5 개의 어설 션이있는 경우 너무 많은 작업이 수행 될 수 있습니다. 인터페이스에 너무 많은 전제 조건과 입력 매개 변수가있을 수 있습니다. 예를 들어, 건강한 수의 주장을 구성하는 주제와 관련이없는 것으로 간주합니다 (일반적으로 "더 많은 메리 어"라고 응답합니다). 가능한 붉은 깃발 (또는 가능하지 않은).


1
이론 상으로는 "너무 많은"주장이있을 수 있지만, 그 문제는 정말 빠르다. 주장이 기능의 고기보다 상당히 오래 걸리는 경우. 분명히, 나는 아직 야생에서 반대의 문제가 만연하다는 것을 발견 한 것을 기억할 수 없다.
중복 제거기

@Deduplicator 아 그래, 그 중요한 벡터 수학 루틴에서 그 사건을 만났다. 너무 적은쪽에 너무 잘못하는 것이 훨씬 나을 것 같습니다!

-1

코드에 검사를 추가하는 것이 매우 합리적입니다. 일반 어설트 (C 및 C ++ 컴파일러에 내장 된 어설 션)의 경우 내 사용 패턴은 어설트 실패는 코드에 수정해야 할 버그가 있음을 의미합니다. 나는 이것을 조금 관대하게 해석한다. 웹 요청이 상태 200을 반환하고 다른 경우를 처리하지 않고 주장 할 경우 실패한 주장 실제로 내 코드에 버그를 표시하므로 주장이 정당화됩니다.

따라서 사람들이 코드를 수행하는 것만 검사한다고 주장하는 것은 불필요합니다. 그 assert는 코드가 생각하는 것을 확인하고, assert의 핵심은 코드에 버그가 없다는 가정이 옳다는 것을 확인하는 것입니다. 또한, assert는 문서로도 사용될 수 있습니다. 루프 i == n을 실행 한 후 코드에서 100 % 명확하지 않다고 가정하면 "assert (i == n)"이 도움이됩니다.

다른 상황을 처리하기 위해 레퍼토리에 "어설 션"이상을 갖는 것이 좋습니다. 예를 들어 버그를 나타내는 무언가가 발생하지 않았 음을 확인하지만 여전히 그 조건을 해결하는 상황입니다. (예를 들어, 캐시를 사용하는 경우 오류를 확인할 수 있으며, 오류가 예기치 않게 발생하면 캐시를 버려서 오류를 수정하는 것이 안전 할 수 있습니다. 개발 중에 알려주 는 거의 단언 이되는 것을 원합니다. 계속 진행하겠습니다.

또 다른 예는 어떤 일이 일어나지 않을 것으로 예상되는 상황이며, 일반적인 해결 방법이 있지만이 일이 발생하면 그것에 대해 알고 조사하고 싶습니다. 다시 한번 어설트와 같은 것이 개발 중에 말해줘야합니다. 그러나 확실히 주장하는 것은 아닙니다 .

어설 션이 너무 많음 : 어설 션이 사용자의 손에있을 때 프로그램이 충돌하는 경우 잘못된 부정으로 인해 충돌하는 어설 션이 없어야합니다.


-3

따라 다릅니다. 코드 요구 사항이 명확하게 문서화되어 있으면 어설 션이 항상 요구 사항과 일치해야합니다. 어떤 경우에는 좋은 것입니다. 그러나 요구 사항이 없거나 잘못 작성된 요구 사항이 있으면 새로운 프로그래머가 요구 사항을 파악하기 위해 매번 단위 테스트를 참조하지 않고 코드를 편집하기가 어렵습니다.


3
이것은 이전의 8 가지 답변에서 제시되고 설명 된 점들에 비해 실질적인 것을 제공하지 않는 것 같습니다
gnat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.