본문 바로가기
C,C++/C++

[C++] VS2019 16.7 Ver. 안전성을 위한 새로운 코딩 규칙을 알아보자

by woohyeon 2020. 9. 11.
반응형

devblogs.microsoft.com/cppblog/new-safety-rules-in-c-core-check/

 

New safety rules in C++ Core Check | C++ Team Blog

Rust and C++ are two popular systems programming languages. For years, the focus of C++ has been on performance. We are increasingly hearing calls from customers and security researchers that C++ should have stronger safety guarantees in the language. C

devblogs.microsoft.com

 

Visual Studio 2019 16.7 버전부터 적용되는 실수 방지를 위한 코딩 규칙이 추가 되었습니다. 위 글에선 Rust라는 프로그래머의 실수에 대한 안전성을 중요시하는 언어와 비교하며 몇가지 규칙에 대한 예제를 설명합니다. 여기선 요약해서 간단하게 알아볼게요.

우선 다음과 같이 대략 4개의 규칙이 있습니다.

  1. switch 문에 default 라벨이 없을 경우 경고 발생
  2. switch 문에 의도를 알기 어려운 fallthrough가 존재할 경우 경고 발생
  3. range-based for문에서의 높은 비용의 복사 시 경고 발생
  4. auto 키워드와 함께 높은 비용의 복사 시 경고 발생

 

1. switch 문에 default 라벨이 없을 경우 경고 발생

switch 문의 마지막에 default를 사용하지 않으면 버그를 야기하기 쉽습니다. 예를 들어 아래 코드에서 val은 1과 2 중 하나가 될 것이라 생각했는데 의도치 않게 다른 값이 나올 수 있습니다. 그러면 아래 switch 문은 제 기능을 못하죠. 아래 코드는 간략해서 switch 문이 실행되지 않았단 것을 알지만 코드가 복잡할 경우 찾기 어려울 수 있습니다. 

    switch (val) 
    { 
        // warning C26818: Switch statement does not cover all cases. 
        // Consider adding a 'default' label (es.79) 
        case 1: 
            std::cout << "one\n"; 
            break; 
        case 2: 
            std::cout << "two\n"; 
            break;
    } 

이를 위해 default 라벨을 생성하지 않으면 위와 같이 경고를 발생시킵니다. 이는 default 키워드를 추가함으로써 사라지게 할 수 있습니다.

 

 

2. switch 문에 의도를 알기 어려운 fallthrough가 존재할 경우 경고 발생

fallthrough란 switch 문에서 임의의 case에 break를 사용하지 않는 것을 의미합니다. 즉 다음과 같은 코드에서 food의 값이 Food::ORANGE 일 경우 peel(food) 함수를 실행하고 끝나는 것이 아닌 쭉 떨어지면서 CAKE case까지 실행합니다. 이것이 작성자가 의도한 것일 수 있지만, 실수일 수도 있습니다. 따라서 이러한 경우 경고를 발생시킵니다. 만약 의도된 것이라면, 의도를 명확히 하기 위해 [[fallthrough]] 키워드를 추가합니다. 그러면 경고가 발생하지 않습니다.

switch (food) 
{ 
    case Food::BANANA: // empty case, fallthrough annotation not needed 
    case Food::ORANGE: // warning C26819: Unannotated fallthrough between switch labels 
        peel(food);
        // [[fallthrough]]  // 해당 주석을 풀면 경고 없어짐    
    case Food::PIZZA:  // empty case, fallthrough annotation not needed 
    case Food::CAKE: 
        eat(food); 
        break; 
    case Food::CELERY: 
        throwOut(food);         
        break; 
} 

 

 

3. range-based for문에서의 높은 비용의 복사 시 경고 발생

범위 기반 for문은 다음과 같이 매우 편리하게 특정 컨테이너의 요소를 사용할 수 있습니다. 예를 들어 아래 코드에서 for문은 employees의 각 요소를 p에 저장합니다. 이때, p는 복사된 값을 사용하게 됩니다. 즉 Person 타입의 크기가 크다면 복사 비용이 높습니다. (비용이 높고 낮음에 대한 기준은 위 링크에 따로 있습니다.) 게다가 p를 수정하지 않기 때문에 컴파일러는 안전하고 더 효율적인 const 참조를 사용하라고 경고를 발생시킵니다.   

void emailEveryoneInCompany(const std::vector<Person>& employees) 
{ 
    Email email; 
    for (Person p: employees) 
    {   
        // Warning C26817: Potentially expensive copy of variable 'p' in range-for loop.
        // Consider making it a const reference
        email.addRecipient(p.email_address); // 값을 수정하지 않는다면 const 참조를 사용하라

        // 대신 값을 수정한다면 경고를 발생시키지 않겠다.
        // email.first_name = "John";
    } 
    email.addBody("Sorry for the spam"); 
    email.send(); 
}

그러나 값이 수정된다면 실제로 원본의 값을 수정할 것인지 아닌지 컴파일러가 판단하지 못합니다. 따라서 값이 수정될 경우 따로 개입하지 않습니다.

 

 

4. auto 키워드와 함께 높은 비용의 복사 시 경고 발생

auto 키워드는 편하지만 실수를 유도하기 쉽습니다. 아래 클래스에서 getPassword() 함수는 const 참조 타입을 반환합니다. 그리고 stealPassword() 함수에서 auto 키워드로 pm의 객체를 받습니다. 컴파일러는 password가 참조로 받을지 값으로 받을지 판단하지 못하므로 기본적인 값으로 받습니다. 그런데 해당 타입의 단순 값 복사 비용은 참조로 넘기는 것보다 크며, 값을 수정하지도 않습니다. 따라서 const 참조가 효율적이라 판단하고 const auto&로 받도록 경고를 발생시킵니다.

class PasswordManager 
{
    std::string password;
public:
    const std::string& getPassword() const { return password; }
};

void stealPassword(const PasswordManager& pm) 
{
    // Warning C26820: Assigning by value when a const-reference would suffice,
    // use const auto& instead.
    // getPassword()가 참조가 아닌 포인터를 반환할 경우 경고를 발생시키지 않는다.
    auto password = pm.getPassword(); 
    

    // 값이 수정된다면 복사를 원하는지 참조를 원하는지 모르므로 굳이 체크하지 않겠다.
    // password += "salt";
}

만약 값이 수정된다면 3번과 같은 이유로 경고를 발생시키지 않습니다.

 




댓글