코드의 제어 (switch case, halt)

switch문의 fallthrough

우리는 지금까지 switch문에서 case label을 쓰고 (항상 정수여야만 함. 컴파일러 최적화로 설계되었기 때문)
구문 뒤에 return 혹은 break를 사용해왔다.

 

그런데 만약 return이나 break문 없이 코드를 짜면, case와 매칭된 문장 부터 순차적으로 아래로 쭉 전부 실행된다.

이러한 경우를 fallthrough(오버플로) 라고 한다.

 

 

 

의도적으로 코드를 통해 fallthrough를 언급하는 것은 다른 개발자에게 이런 경우가 의도되었음을 알리는 규칙이다.

개발자는 의도를 알아차리지만 컴파일러와 코드 분석 도구는 이런 상황에서 프로그래머에게 경고한다.

이 문제를 해결하기 위해 C++17에서는 [[fallthrough]]속성 이라는 기능을 제공한다.

여기서 속성은 프로그래머가 코드에 대한 추가 데이터를 컴파일러에 제공할 수 있는 기능이다.

속성을 지정하는 방법은 속성 이름을 이중 대괄호 [[ ]]안에 넣는 것이다. [[fallthrough]]속성을 사용하면 컴파일러에 경고가 트리거되지 않는다.

 

 

 

Case 문 내부의 변수 선언 및 초기화

switch (1)
{
    int a; // okay: definition is allowed before the case labels
    int b{ 5 }; // illegal: initialization is not allowed before the case labels

case 1:
    int y; // okay but bad practice: definition is allowed within a case
    y = 4; // okay: assignment is allowed << 초기화가 아니라 할당은 다른가 흠
    break;

case 2:
    int z{ 4 }; // illegal: initialization is not allowed if subsequent cases exist
    y = 5; // okay: y was declared above, so we can use it here too
    break;

case 3:
    break;
}

switch case 문의 예시 코드이다.

 

특히 case2의 경우, 변수를 초기화하려면 런타임에 정의를 실행해야 하는데

(초기화 값은 해당 시점에서 결정되어야 하기 때문) 

마지막 case가 아닌 경우에는 변수 초기화가 허용되지 않는다

(초기화가 있는 문을 뛰어넘어 변수가 초기화되지 않은 상태로 남을 수 있기 때문) 

 

첫 번째 case 이전에도 초기화는 불가능하다. 스위치가 해당 명령문에 도달할 수 있는 방법이 없기 때문에 해당 명령문은 실행되지 않기 때문이다.

switch (1)
{
case 1:
{ // note addition of explicit block here
    int x{ 4 }; // 마지막 case라 초기화 가능
    std::cout << x;
    break;
}

default:
    std::cout << "default case\n";
    break;
}

 

 

 

아까 switch문에서 봤듯이, break을 사용하면 fall-through되지 않고 바로 해당 블럭을 벗어난다. 

이것을 사용해서 for loop 도중 loop를 벗어나고 싶을 때 break을 사용한다.

 

그러면 break랑 return의 차이가 없지 않나? 싶다.

 

break문은 스위치나 루프를 종료하고 스위치나 루프 다음의 첫 번째 문에서 실행이 계속되고

return문은 루프 내에 있는 전체 함수를 종료하고 함수가 호출된 지점에서 실행을 계속한다는 차이가 있다.

 

 

for loop에서 이와 대비되는 개념으로 continue문이 있다.

continue문은 전체 루프를 종료하지 않고 현재 반복을 종료한다.

 

 

while에서 continue를 사용하는 경우 주의해야 한다.

예를들어 5를 제외하고 나머지 숫자를 인쇄하는 프로그램을 생각해보자.

int main()
{
    int count{ 0 };
    while (count < 10)
    {
        if (count == 5)
            continue; // jump to end of loop body

        std::cout << count << '\n';

        ++count; // this statement is never executed after count reaches 5

        // The continue statement jumps to here
    }

    return 0;
}

이 경우, if에서 count가 5로 평가되는 순간 continue로 인해 해당 회차의 가장 아래쪽으로 이동하여 ++count는 실행되지 않기 때문이다. ++count는 영원히 실행되지 않아 이 경우 무한루프를 유발한다.

 

 

많은 책에선 break과 continue를 사용하지 말라고 경고한다. 위 예시와 같이 실행 흐름을 뛰어넘기 때문에 논리 흐름을 따라가기 어려워서 그런 것 같다.

하지만 break과 continue를 사용함으로서, 사용되는 변수의 수를 최소화하고 중첩된 블록의 수를 줄이기 때문에 오히려 코드 이해도를 향상시킬 수 있다. 무조건적인 사용이나 금지는 없다. 다만 코드 작성과 인지에 도움이 되는지 판단할 뿐이다. 이건 남용하지 않는 선에서 적절히 사용하는 것이 좋겠다.

 

 

 

 

 

halt문 제어

 

프로그램을 종료하는 흐름 제어문인데, 이것은 키워드가 아닌 함수로 구현되므로 halt문은 함수 호출이 된다.

 

 

halt문인 std::exit()은 프로그램을 정상적으로 종료시키는 함수이다. 

이것은 함수가 끝날 때 암시적으로 호출되지만, 정상적으로 종료되기 전에 프로그램을 중지하기 위해 명시적으로 호출할 수도 있다. 당연하게도 std::exit() 이후의 문은 실행되지 않는다.

 

이 함수를 사용할 때 주의해야 할 점은 종료되지만 현재 함수의 지역 변수나 호출 스택을 정리하진 않는다는 것이다.

 

std::exit()는 프로그램을 즉시 종료하므로, 종료하기 전에 일부 정리 작업을 수동으로 해야 될 수도 있다. 

여기서 정리는 메모리 누수를 막기 위해 할당한 메모리 해제, 데이터베이스나 네트워크 연결 닫기 등이 있다.

 

 

exit()를 호출하기 전에 수동으로 함수를 정리해야 한다는 점은 프로그래머에게 꽤나 부담이다.

이를 지원하기 위해 C++에서는 std::atexit()를 통해 프로그램 종료 시 자동으로 호출되는 함수를 지정할 수 있다.

#include <cstdlib> // for std::exit()
#include <iostream>

void cleanup()
{
    // code here to do any kind of cleanup required
    std::cout << "cleanup!\n";
}

int main()
{
    // register cleanup() to be called automatically when std::exit() is called
    std::atexit(cleanup); // note: we use cleanup rather than cleanup() since we're not making a function call to cleanup() right now

    std::cout << 1 << '\n';

    std::exit(0); // terminate and return status code 0 to operating system

    // The following statements never execute
    std::cout << 2 << '\n';

    return 0;
}

이렇게 main에서 정리 기능(함수)을 지정하면, std::exit()를 호출하기 전에 해당 기능을 명시적으로 호출하는 것을 기억하느라 걱정할 필요가 없다.

 

 

참고로 main()이 종료될 때 std::exit()이 암묵적으로 호출되기 때문에 프로그램이 이렇게 종료되면 std::atexit()에 의해 등록된 함수를 호출한다.

그리고 등록한 함수는 매개변수를 사용해선 안되고 반환값이 없어야 한다.

 

마지막으로, std::atexit()을 사용하여 여러가지 정리 함수를 등록할 수 있으며, 등록한 순서의 역순으로 호출된다.

(마지막에 등록된 함수가 먼저 호출됨) 

 

 

더 주의해야 할 점은 다중 스레드 프로그램에서 std::exit()를 호출하면 프로그램이 충돌될 수 있다는 것이다.

std::exit()를 호출하는 스레드가 다른 스레드에서 여전히 액세스할 수 있는 정적 개체를 정리하기 때문이다.

 

이러한 이유로 C++는 std::exit()와 유사하게 작동하는 다른 함수 쌍 std::quick_exit() 및 std::at_quick_exit()를 도입했다.

 

std:quick_exit()는 프로그램을 정상적으로 종료하지만 정적 개체를 정리하지 않고, 다른 타입의 정리를 수행하거나 수행하지 않을 수 있다. 

 

 

 

중단 std::abort,  종료 std::terminate

 std::abort()함수는 프로그램을 비정상적으로 종료시킨다.(위의 정상적인 종료랑 비교됨)

 

비정상적인 종료는 프로그램에 비정상적인 런타임 오류가 발생하여 프로그램을 계속 실행할 수 없음을 의미한다. 

예를 들어 0으로 나누려고 하면 비정상적으로 종료된다. 그리고 std::abort()는 어떠한 정리도 하지 않는다.

#include <cstdlib> // for std::abort()
#include <iostream>

int main()
{
    std::cout << 1 << '\n';
    std::abort();

    // The following statements never execute
    std::cout << 2 << '\n';

    return 0;
}

 

 

 

std::terminate() 함수는 일반적으로 예외와 함께 사용된다(예외는 나중에 배움).

std::terminate는 명시적으로 호출할 수 있지만 예외가 처리되지 않을 때 암묵적으로 호출되는 경우가 더 많다.

그리고 기본적으로 std::terminate()는 std::abort()를 호출한다.

 

비록 이렇게 공부하긴 했지만 이런 정지문은 거의 전혀 사용하지 않는다.. 대신 나중가서 예외처리를 사용할 것이다.