다음 코드의 흐름제어(Control Flow)에 관련되어서 나올수 있는 많은 문제를 찾아보고, 어떻게 고쳐야 되는지 생각해 보자.




분석

int fileIdCounter;는 전역변수이다. 전역변수 초기화 순서가 정해있지 않기 때문에
초기화 전에 이것을 사용하는 코드가 있다면, 문제가 발생한다. 즉 잠재적인 지뢰가....

가이드 라인
전역 변수와 정적 변수의 사용을 피하자. 왜냐하면 언제 초기화가 될지 정해있지 않으니까, Effective C++ 에서는 전역 변수나 정적 변수를 scope를 두어서 사용하면 된다고 했는데, 그 방법이 바로 함수 안에 static 으로 선언해 두고, 그 객체의 참조형태로 리턴하여 외부에서 받아 사용 하는 방법으로 해결할수 있다고 설명했다.

예)
int& test() { static int fileIdCounter; return fileidCounter; }


바로 ++fileIdCounter 이 부분이 잠재적인 문제가 발생 될수 있다. 왜냐하면 fileIdCounter 초기화가 AASert 보다 늦게 초기화 된다면, localFileId 값은 어떤 값이 될지 예측하가 힘들다. 즉, 0이 될지, 다른 값이 될지..



허브 셔터는 아이디어는 좋지만, assert는 non-debug 모드 일 경우 없어지므로, 디버깅 모드에서만 사용 가능하다는 문제점을 지적한다. 여기서 좋은 아이디어란 AINVARIANT_GUARD 를 이용하여, 현재 자신이 변경되었느지 체크 할수 있다는 점이다. 최종적으로 p.Invariant() 로 인하여 체크한다.



이 부분에선 허브 셔터는 2번째 라인에서 ArrayBase, Container 순으로 초기화 되기 때문에, 8번째 라인이 먼저초기화가 되므로 문제가 될 수 있음을 지적한다. Effective C++ 에서도 언급했던적이 있다. 이때는 클래스 상속순에 대한 언급은 없었지만, 여기서는 자세히 설명해 준다.

경우 1. Container::GetType() 가 정적 함수이거나 this 포인터를 사용하지 않은 멤버 함수라면, 어떠한 부작용도 일어나지 않지만, 좋은 코딩 스타일이 아니라는 지적

경우 2. 경우 1의 반대 라면, 어떤 값이 나올지 예측할수 없다. 뻑 날수도..

가이드라인
http://www.ikpil.com/405 참조 하길 바라며, 클래스 상속순은 왼쪽부터 오른쪽 이다. 이것은 초기화 순서에 영향을 미친다.



여기서 buffer_가 제일 먼저 초기화가 되는데(클래스 정의 순이 맨 처음이다) size_ 가 초기화가 되지 않았으므로 new 는 엄청큰 값을 초기화 하려고 하던지 아니면 -값이던지 0값이던지 알수 없다.



이렇게만 바꾼다고 쳐도 역시 size_는 맨 뒤에 초기화 되므로, 클래스 변수 선언을 바꾸던지 해야 한다.

가이드 라인
몇몇 프로그래머들은 이런 문제를 대처하기 위해서, 초기화 함수를 만들고, 거기서 초기화 한다고 한다. (나의 경우가 이렇다;;)

초기화 순서를 확인하기 다음 코드로 확인해 보길..

브레이크 포인트를 찍어서 확인해 보길.. a에 분명 쓰래기 값이 들어 갈 것이다.



AINVARIANT_GUARD 것은 .. invariantChecker 객체가 생성자와 소멸자 사이에서 두번 경고메세지를 보여준다. 하지만 항목 47에서 말하고자 하는 주제와 다르므로 살작쿵 무시해 주자.



이 부분에서 흐름제어에 문제가 있다.(MSVC2005 에선 경고 메세지가 다분히 발생한다.)

우선 5번째 라인에서 메모리 할당이 실패하면 std::bad_alloc 예외가 발생되면서 Resize() 내에서 뛰쳐 나오게 되므로 여기까지는 그런데로 괜찮다. 하지만 6번째 라인에서 copy 즉 복사 생성자를 이용하여 복사하게 될때, 예외가 발생되면, .. 역시 Resize()를 뛰쳐 나오게 된다. 이때 문제가 생길수 있다.

왜냐하면 buffer_ 는 이미 새로운 포인터를 가리킨 상태가 되고, 기존의 메모리 공간을 잃어버려 메모리 릭도 발생되고, 기존 값들도 잃어 버리게 된다. 그러므로 항상 안전한 코딩을 해야 한다.

코드를 고친다면,

이런식으로.. 1~ 18 라인은 다른곳으로 빼고, .. 사용하면 된다. 뭐 이런 식이다라는 거지 버기에 대해서는 할말 없다.



이 코드는 컴파일러마다 다른 결과 값을 뱉어 낸다. 즉, operator+ 가 무엇을 먼저 실행하느냐에 따라서 결과값이 다르다. 왜냐하면 buf를 공유해서 작업을 한것을 string 에 더 해지기 때문이다. 만약, itoa가 먼저 두번 실행되고 string 이 호출된다면, .. 다른 값이여도 똑같은 값이 나온다는 것이다.

가이드라인 : 절대 함수의 매개변수의 연산 순서에 의존하는 코드를 만들지 말자. Effective C++ 에서 함수 매개변수 전달시 한 줄로 만드는것 보다, 함수의 반환값을 객체에 담아 함수의 매개변수에 전달하는게 더 좋다 라고 나와 있다.



이 코드는 Resize 코드를 추적해 보면 알겠지만,
경우 1. 무한 재귀함수 재귀 함수 호출을 할수가 있다.
경우 2. assert() 호출할 경우 또는 이와 비슷한 것을 호출할 경우(DEBUG 모드에서만 되는것)에 대해서 미칠수 있는 영향을 생각해서 코딩해야 한다는것



이 코드에서는 f의 두번째 매개변수 y = x 를 넣고 있다. 이것은 컴파일 단계 막아주기도 하나, 다른 컴파일러는 안막아 준다. 그러므로 사용하면 안된다.

y에 기본적으로 1값을 넣고~ int f( int& x, int y = 1) { return x+= y; }

다음을 보게 되면

여기서도 매개변수에 따른 호출 순서가 명확하지 못하다. 즉 f(i) 와 g(i) 중 어느것이 먼저 호출 될지 모르므로 확신이 안되는 코딩이 되겠다. MSVC2005 에서 확인해 본결과 f(22) = 22, g(21) = 21 로 나온다.

이런 결과는 컴파일러 특성을 갖으므로 OTL 을 외쳐주고 피해가자.



위에서 말했던 버그 때문에 역시 20 20 씩 출력 하게 된다.


총평
빈번히 발생할수 있는 흐름제어 오류에 대해서 이야기를 듣고 감명을 받았다. 함수 호출 규약에 따라 매개변수의 실생 순이 가변적이라는 것과 class 의 상속순에 따라 초기화 순으로 결정된다는것에 대해서는 새롭게 알게 되었다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 라이프코리아트위터 공유하기
  • shared
  • 카카오스토리 공유하기