너무 많은 주장을 쓸 수 있습니까?
물론입니다. [여기서 명백한 예를 상상해보십시오.] 그러나 다음에 자세히 설명 된 지침을 적용하면 실제로 그 한계를 뛰어 넘는 데 어려움이 없어야합니다. 나는 또한 많은 주장을 좋아하며 이러한 원칙에 따라 사용한다. 이 충고의 대부분은 단언에 특화된 것이 아니라 일반적인 공학적 관행에만 적용됩니다.
런타임 및 이진 풋 프린트 오버 헤드를 명심하십시오
어설 션은 훌륭하지만 프로그램을 허용 할 수 없을 정도로 느리게 만들면 매우 성가 시거나 조만간 끌 수 있습니다.
포함 된 함수 비용과 관련된 어설 션 비용을 측정하고 싶습니다. 다음 두 가지 예를 고려하십시오.
// 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;
}
이것에 대해 몇 가지를 주목하십시오.
- 술어 함수 자체이다
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
는 물론 컴파일되지 않는다는 점을 제외하고는와 동일 할 수 있습니다 .
더 많은 정보
나는 존 Lakos '이야기 발견 방어 프로그래밍 완료를 마우스 오른쪽 CppCon'14 (에 주어진 1 개 번째 부분 , 2 차 부분 ) 매우 계몽을. 그는이 어설 션에서 사용 가능한 어설 션과 실패한 예외에 대한 대응 방법을 사용자 지정하는 아이디어를 취합니다.