
Flow Control
int main()
{
int x{};
std::cin >> x;
std::cout << x << std::endl;
return 0;
}
이런 코드들은 main에서 시작해서 명령들을 순차적으로 실행하는 직선 프로그램이다 (실행할 때마다 항상 동일한 flow)
이때 만약 입력된 값이 10 이하이면 출력하지 않고 10 초과일때만 출력하는 프로그램을 만든다고 한다면 이는 Flow Control Statement가 필요하다
C++에는 다양한 Flow Control Statement가 존재한다
조건문, 점프, 함수호출, 반복문, 종료/중단, 예외가 있다
조건문
C++에서 두개의 기본적인 조건문을 지원한다 (if, switch)
if, else, else if
if (조건)
{
명령;
}
else
{
명령;
}
if()의 조건이 참이라면 if의 명령을 실행하고 if()의 조건이 거짓이라면 else의 명령을 실행한다
int main()
{
int x{};
std::cin >> x;
if (x > 10)
{
std::cout << x << std::endl;
}
else
{
std::cout << "Not Print" << std::endl;
}
return 0;
}
if, else의 명령문이 단일 문장일 경우 복합 명령문인 { }가 없어도 잘 실행되지만 단일 문장이어도 { }로 묶는것을 권장한다 (복합 명령문 { }으로 단일 명령문인것 처럼 처리)
우선 코드 수정 시 의도치 않은 동작을 만들 수 있다
if (age >= minDrinkingAge)
purchaseBeer();
//여기에 함수를 추가할 때
if (age >= minDrinkingAge)
purchaseBeer();
gamble(); //if-statement에 영향을 받지 않아 무조건 실행된다
두번째는 디버깅이 어려워진다
if (age >= minDrinkingAge)
//purchaseBeer(); 디버깅용으로 주석처리 시
gamble(); 조건에 들어가게 됨
이렇게 디버깅용으로 주석처리 시 바로 밑의 명령이 조건에 들어가게 되버린다
추가로 if, else의 단일 블록 명령에서 변수를 선언 시 로컬 변수로 들어가게 되어 다른 라인에서 해당 변수 사용이 불가능해진다

만약 if, else 외 추가로 다른 조건들을 넣고 싶다면 else if()를 사용한다
else if(조건)
{
명령;
}
bool a { true };
bool b { false };
if (a)
{
}
else if (b)
{
}
else
{
}
else if의 조건이 참이면 해당 블록의 명령이 실행되게 된다
if문은 다른 if문과 중첩이 가능하다
if (x >= 0)
{
if (x <= 20)
{
}
}
위 코드는 x가 0이상 20이하일때 조건을 체크해서 명령을 수행하게 된다
단 if()문의 중첩이 적을수록 좋다, 에러 발생 확률이 줄어든다 (early return이나 ||나 &&와 같은 논리 연산자로 묶어서 최대한 적은 중첩의 조건문을 사용하는걸 지향한다)
if (x >= 0 && x <= 20)
{
}
Null Statement
조건문의 명령에 ;만 사용하여 Null Statement를 표현할 수 있다, 이러한 Null Statement는 아무 명령도 수행하지 않는다
constexpr int x{ 100 };
if (x >= 0)
{
;
}
if ()의 끝을 실수로 ;으로 끝내버린다면 Null Statement가 사용될 수 있으니 조심하자
이러한 Null Statement는 ;으로 사용하면 혼동이 올 수 있기 때문에 전처리기를 사용하여 매크로로 사용이 가능하다
#define PASS
constexpr int x{ 100 };
if (x >= 0)
{
PASS;
}
constexpr if
일반적인 if문의 조건은 런타임에 평가된다, 하지만 이때 조건이 상수 표현식 비교라면 컴파일 타임에 평가할 수 있다
constexpr double pi{ 3.14159 };
if (pi == 3.14159)
{
std::cout << pi << std::endl;
}
이러한 조건은 충분히 컴파일 타임에 평가가 가능하기 때문에 constexpr if문을 사용하는게 좋다
(런타임에 상수 표현식 평가는 낭비)
C++17부터 constexpr if문이 도입되었다, 가장 중요한 점은 조건문의 조건이 무조건 상수 표현식이어야 한다는 점이다 (컴파일 타임에 조건문 평가)
constexpr double pi{ 3.14159 };
if constexpr (pi == 3.14159)
{
std::cout << pi << std::endl;
}
else
{
std::cout << "false" << std::endl;
}
위 코드가 컴파일 될 때 컴파일러는 컴파일 타임에 조건문을 평가하고 항상 true이기 때문에 std::cout << pi << std::endl; 단일 문장만 유지한다
따라서
constexpr double pi{ 3.14159 };
std::cout << pi << std::endl;
이렇게 컴파일 된다
조건이 상수 표현식인 경우 constexpr if를 사용하여 컴파일 타임 최적화를 하는것을 권장한다
switch-case
조건이 여러개일경우 if-else, else if를 사용하게 되면 가독성이 떨어지며 비효율적이다, 이럴때 switch-case를 사용한다
void foo(int x)
{
if (x == 1)
std::cout << "One";
else if (x == 2)
std::cout << "Two";
else if (x == 3)
std::cout << "Three";
else
std::cout << "Unknown";
}
이 경우에 x는 최대 3번까지 평가되게 된다 (비효율적임)
void foo(int x)
{
switch (x)
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three";
break;
default:
std::cout << "Unknown";
break;
}
}
switch문이 컴파일 되면 컴파일러는 일반적으로 jump table을 통해 구현된다 (input으로 들어온 값을 사용하여 결과로 jump함, 따라서 if문의 순차적 검사보다 효율적임)
switch의 평가대상과 일치한 case로 jump하여 명령을 실행한다
switch의 평가대상에는 정수형,열거형이 들어가야 한다 (부동소수점, 문자열 등 비정수형 표현식 사용이 불가)
(jump table을 사용하기 때문)
case의 값들은 고유해야 한다 (중복X)
case 1:
break;
case 1:
break;
//error
case의 값들은 상수 표현식만 가능하다 (상수가 아닌 변수는 사용이 불가능함, const나 constexpr은 가능함)
default는 if의 else와 같이 맞는 case가 없을때 실행된다 (선택 사항이며 하나만 존재할 수 있다)
보통 default는 switch문의 마지막에 작성한다
switch의 평가대상과 맞는 case가 존재하지 않고 default가 없다면 아무것도 실행되지 않는다
break는 바로 switch문을 끝내고 다음 명령을 실행해야 한다고 컴파일러에게 알려준다
switch문의 case는 들여쓰기 하지 않는다 (명령과 break;만 들여쓰기 한다)
switch, if-else
동일한 상황에서 switch는 if-else보다 가독성이 좋고 효율적이다 (표현식을 한번만 평가하기 때문), 하지만 if-else가 훨씬 더 유연하게 사용이 가능하다
여러가지 복합적인 조건을 테스트할때는 if-else, 값을 평가하여 같은지를 평가할때는 switch문을 사용하는걸 권장한다
Fallthrough
switch-case를 사용하며 break를 사용하지 않는다면 일치한 case의 명령이 실행되고 밑의 case로 넘어가 또 명령이 실행되게 된다
switch (x)
{
case 1:
std::cout << "One";
case 2:
std::cout << "Two";
case 3:
std::cout << "Three";
default:
std::cout << "Unknown";
}
x에 2가 들어온다면 Two, Three Unknown이 전부 호출된다는 의미이다 (break로 일치 후 switch문을 빠져나가지 않았기 때문)
이러한 현상을 fallthrough 라고 한다 (break, return이 이러한 fallthrough를 방지한다)
하지만 이러한 fallthrough를 의도적으로 사용해야할 때가 존재할 수 있다
C++17에서는 [[fallthrough]]라는 속성을 추가해 사용이 가능하다
이러한 속성으로 컴파일러에게 코드에 대한 추가 데이터를 제공한다
속성은 [[ ]]안에 속성 이름을 넣어 사용한다
int main()
{
switch (2)
{
case 1:
std::cout << 1 << '\n';
break;
case 2:
std::cout << 2 << '\n';
[[fallthrough]];
case 3:
std::cout << 3 << '\n';
break;
}
return 0;
}
이때 [[fallthrough]]속성은 ;(null statement)와 함께 사용한다
결과는 fallthrough가 되어 2, 3이 출력되게 된다
switch-case를 사용하며 헷갈릴 수 있는 점은 case는 각 { }복합 명령문이 생성되지 않는다는 점이다
위의 case에서 std::cout << 1 << '\n';은 case1의 { }안에만 존재하는게 아니다, 모든 case들은 switch의 { }내부에 존재한다
추가로 switch내부에서 case의 앞과 뒤에서 변수를 선언은 가능하지만 초기화는 불가능하다, 물론 case들은 각각의 { }안에 존재하는게 아닌 switch의 { }에 존재하기 때문에 case밑에서 생성한 변수들은 다른 case에서도 접근이 가능하다
switch (1)
{
int a;
int b{ 10 }; //error (switch문은 case로 jump하기 때문에 실행되지 않을 우려가 있어 허용되지 않음)
case 1:
int y{ 50 };//error (마찬가지로 switch의 jump 방식으로 인해 초기화가 건너뛰어질 가능성이 있기 때문에 허용되지 않는다)
y = 5;
case 2:
int x{ 50 }; //Ok (마지막 case이기 때문에 초기화를 건너뛸 가능성이 없다, 아예 사용을 하지 않는건 가능하지만 초기화가 안될일은 없음)
y = 10; //ok
}
만약 case내부에서 변수 선언 및 초기화가 필요하다면 { }로 감싸서 처리한다 (초기화 된 변수의 범위를 { }로 제한하기 때문에 undefined behavior가 발생할 일이 없다)
case 1:
{
int x{ 10 };
std::cout << x;
break;
}