일단 내적을 구하는 다음과 같은 코드를 보자.


double inner_product(const std::vector<double>& x, const std::vector<double>& y)

{

  double ret = 0;


  const auto size = x.size();

  for(std::size_t i = 0; i <= size; ++i)

    ret += x[i] * y[i];


  return ret;

}


일부러 코드에 버그를 넣어놨어. off by 1(링크)이라는 기초적이지만 흔히 발생할 수 있는 버그인데, 프로그램 규모가 커지거나 짜는 사람이 피곤하면 찾기가 쉽지 않은 버그야. c/c++은 바운더리 체크를 안해주기 땜에 더더욱 그렇지.


std::vector를 좀 써본 사람이라면 .at() 이라는 함수를 본 적이 있을텐데, 이 함수는 인덱스 체크를 해서 벡터의 범위를 벗어나면 익셉션을 던져주는 함수야. 이 함수를 쓰면 다음과 같이 짤 수 있고, 실행해보면 실제로 익셉션이 던져져.


  for(std::size_t i = 0; i <= size; ++i)

    ret += x.at(i) * y.at(i);


terminate called after throwing an instance of 'std::out_of_range'

  what():  vector::_M_range_check: __n (which is 2) >= this->size() (which is 2)

Aborted


그런데 이 코드의 단점은, at() 함수가 호출될때마다 인자를 검사하기 때문에 오버헤드가 좀 크고, 그래서 성능 저하가 발생할 수 있다는거야.

디버그할 때는 검사를 하지만, 릴리즈할 때는 검사를 하지 않는 그런 코드가 있으면 좋은데, 그게 바로 assert야.


일단 assert가 어떻게 생겨먹었는지 보자. (링크)


#ifdef NDEBUG

#define assert(condition) ((void)0)

#else

#define assert(condition) /*implementation defined*/

#endif


NDEBUG가 define되어 있으면 아무 일도 하지 않고, define되어 있지 않으면 관련 메시지를 출력하고 프로그램을 종료시키는데,

개인적으로는 이런 매크로야말로 제대로 매크로를 활용한 우아한 방법이라고 생각해.

#define T_T template <class T> 이따위 개족보에도 없을 매크로는 그저 T_T


C에서는 #include <assert.h>, c++에서는 #include <cassert>를 사용하면 assert를 쓸 수 있어.


assert를 써서 위의 코드를 바꿔보자.


  for(std::size_t i = 0; i <= size; ++i) {

    assert( (i < x.size()) && (i < y.size()) );

    ret += x[i] * y[i];

  }


NDEBUG를 정의하지 않고 수행하면 다음과 같이 assertion 메시지가 뜨는 것을 볼 수가 있어.


a.out: assertion.cpp:12: double inner_product(const std::vector<double>&, const std::vector<double>&): Assertion `(i < x.size()) && (i < y.size())' failed.

Aborted


그럼 딱 봐도 assertion.cpp의 12번째 줄에 i가 잘못됐구나 라는 걸 알 수가 있고, 디버깅하기도 쉽겠지? 거기다 NDEBUG만 정의하면 릴리즈모드에서 오버헤드가 전혀 없고.


NDEBUG를 정의하는 방법은 컴파일러마다 다르지만 대충 다음과 같아.


gcc : gcc -DNDEBUG

clang : clang -DNDEBUG

icc : icc - DNDEBUG

msvc : 프로젝트 속성 - C/C++ - 전처리기 - 전처리기 정의 : NDEBUG 추가.



그럼, 언제 익셉션을 던지고 언제 assert를 사용해야하는가? 이게 좀 고민해볼만한 문제인데, 이건 딱히 정답은 없는 것 같지만 대략 내 경험으로는


유저 입출력이나 파일 입출력에서 발생하는 외부적 요인 : 에러 코드를 리턴하거나 익셉션

구현 내부에서 발생가능한 내부적 요인 : assert로 지뢰밭을 깔아둠


정도가 적합하지 않는가 싶어.


다시 위의 inner_product 함수에서, 저 함수의 사용자가 벡터 두개를 입력으로 주는데 두 벡터의 크기가 다르면 내적이 정의가 안되잖아? 이런 경우에는 에러코드를 리턴하거나 익셉션을 던지는게 더 적합하다고 보는거야.

그럼 요 기준으로 inner_product를 짜면 다음과 같이 짤 수 있어.


double inner_product(const std::vector<double>& x, const std::vector<double>& y)

{

  double ret = 0;


  if(x.size() != y.size())

    throw std::invalid_argument("x.size() != y.size()");


  const auto size = x.size();

  for(std::size_t i = 0; i < size; ++i) {

    assert( (i < x.size()) && (i < y.size()) );

    ret += x[i] * y[i];

  }


  return ret;

}


assert는 잘 쓰면 그 자체로 코드 내부에서 지켜져야할 불변식을 나타낼 수 있는 게 큰 장점이야. 릴리즈 모드에서 NDEBUG만 정의하면 비용도 0이고.



assert 사용할 때는 주의사항이 좀 있는데, 절대로 assert 내부에서 값을 변경하지 말아야해. 다음과 같은 코드를 보자.


int a = 1;

assert(++a != 0);

std::cout << a << std::endl;


이런 코드는 NDEBUG를 정의할 때와 정의하지 않을 때의 동작이 달라지겠지? 디버깅 쉽게 하려고 assert를 썼는데 오히려 거기서 버그가 발생할 수 있어. 그러니까 값을 바꾸는 코드와 무조건 assert문은 분리하는게 좋아.


int a = 1;

++a;

assert(a != 0);  // 분리함

std::cout << a << std::endl;



그리고 또 주의사항은ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

프로그래밍 30년 넘게 했다는 새끼갘ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

성능 비교하는데 assert문도 안끄고 비교를ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

assert문 안끄고 돌려봤떠니 그 븅신새끼랑 결과가 비슷함ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

자칭 고수지만 assert문 끄는 방법을 모르거낰ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

아님 일부러 안끈 개새끼라는 건뎈ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

둘 다 개 웃겨섴ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

내가 오늘 아침에 좆나 웃겨서 깸ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ


오늘 밤이나 내일은 잘못된 최적화의 예를 써볼까 한다.



3줄 요약:


디버거도 좋지만 assert 지뢰밭은 버그 찾기 더 쉬움.

예외 처리할 때와 assert 쓸 떄를 구별하자.

assert 내부에선 절대로 값을 변경하지 말자.





========== 내용 추가 ==========


#define assert(x) (void(0)) 이 문장은 x를 (void(0))으로 치환한다는거야.


assert(a == 0);


이 문장이 있다고 치면,


NDEBUG가 정의가 안되어 있으면 일반적으로


if(!(a == 0)) {

  // __FILE__과 __LINE__을 출력하고

  // 프로그램을 강제종료시킨다.

}


요런식으로 바뀌게 돼.


gcc -DNDEBUG로 NDEBUG를 정의해서 빌드하면


assert(a == 0); 는 전처리기에 의해 (void(0)); 으로 치환되고, 그럼 컴파일러는 아무 의미없는 이 문장을 그냥 버려. 그러니까 assert 문장이 아무것도 없는 것처럼 처리되는거지.





assert()는 기본적으로 런타임 검사야. 실행시에 주어진 조건을 검사하는데, 컴파일 타임 assertion을 원하면 C11의 _Static_assert(링크)나 c++11의 static_assert(링크)를 쓰면 돼.


내가 짠 코드가 CHAR_BIT 이 8인 시스템에서만 정상적으로 돌아가는 코드라면,


_Static_assert(CHAR_BIT == 8, "It doesn't work with CHAR_BIT!=8");


이런식으로 컴파일 타임에 assertion을 수행할 수 있어. 물론 이 compile time assertion은 생성되는 코드에 전혀 영향이 없으니까 켜고 끌 것도 없지.



========== 내용 또 추가 ==========


음 내가 내부적 요인 외부적 요인이라고 써놨는데 이건 공식용어가 아니라 그냥 내가 표현한것 뿐이야. 이것 땜에 좀 헷갈리는것 같은데 내가 설명을 못한다는걸 새삼 느낀다 ㅋㅋ


doubly linked list를 짠다고 생각해보자.


node* insert(T value, node* pos)

{

  node* new_node = malloc(sizeof(node));


  if(new_node == NULL) {

    // 내가 잘못 짜서 생기는 현상이 아니므로 익셉션을 던지든 에러코드를 리턴하든 한다.

  }


  node->value = value;


  node* prev = pos->prev;


  // prev와 new_node를 연결

  // new_node와 pos를 연결


  // 내가 잘못 짜서 버그가 생길 수 있으니 assertion을 수행한다.

  assert(prev->next->prev == prev);

  assert(pos->prev->next == pos);

  assert(new_node->prev->next == new_node);

  assert(new_node->next->prev == new_node);

}


메모리 할당이 안되는 경우는 외부적 요인이니까 에러코드를 리턴하든 익셉션을 던지는게 좋을거고,

내가 잘못 짜서 생기는건 assertion으로 디버그 모드에서 검사하고 릴리즈모드에선 비활성화하는게 좋겠지.