본문 바로가기

c c++ mfc

C++ 17의 주목할만한 Feature

반응형

안녕하세요. 오늘은 잠시 제 본업으로 돌아왔습니다. 업무에 사용중이던 Visual Studio 버전을 2019에서 2022로 올리게 되었고, 동시에 사용하던 C++ 표준이 14에서 20으로 올라가게 되며 그간 새 표준에 추가된 기능 중 유용한 기능에 대해 기록해 볼까 합니다. 우선은 C++ 17에 추가된 내용들을 먼저 정리해 보려고 합니다. 참고로 아직 C++ 23은 정식으로 나온거 같지는 않네요. 그리고 20에서 추가되는 기능도 크지 않을 거라는 전망입니다.

 

아래에 나열한 기능들은 제 개인적인 의견으로 유용할 것 같은 애들만 기술했습니다. 참고해 주세요~

 

1. C++ 17에서 유용할 거 같은 애들

  1. std::optional : 값이 있을 수도 있고 없을 수도 있는 상황, 이를테면 한 자료구조에서 특정 요소를 찾아 해당 요소가 위치하는 index를 반환하는 함수에서 흔히 해당 요소가 존재하지 않을 경우 '-1'을 관행적으로 반환하는 경우가 많습니다. 그리고 -1이 반환되면 이 함수는 실패했다고 간주하자. 라고 사용하게 됩니다. 하지만 만약 다른 기능을 하는 함수인데 -1도 의미가 있는 함수이다. 그러면 어떻게 함수 실패 여부를 판단할 수 있을까요? 이런 경우에 이 std::optional이 유용할 것 같습니다. 아래 예제코드를 먼저 보시죠.
    • #include <iostream>
      #include <optional>
      
      std::optional<int> get_even_number(int num) {
          if (num % 2 == 0)
              return num;
          else
              return {};
      }
      
      int main() {
          auto opt = get_even_number(3);
      
          if (opt.has_value()) {
              std::cout << "Even number: " << opt.value() << '\n';
          } else {
              std::cout << "Not an even number.\n";
          }
      
          return 0;
      }
    • 위 코드에서 함수 get_even_number()는 주어진 숫자가 짝수인 경우 그 숫자를 반환하고, 홀수인 경우 아무런 값도 반환하지 않습니다. 따라서 이 함수의 반환 타입은 '정수값이 존재할 수도, 존재하지 않을 수도 있는' 상황을 잘 나타내기 위해 std::optional로 설정되었습니다.

      따라서 이러한 방식으로 std::optional은 값의 유무에 따른 복잡성을 관리하며, 오류 처리나 특별한 반환 값을 필요로 하는 경우에 코드 가독성과 안정성을 높일 수 있게 돕습니다.
  2. if - initialize : if문과 switch문에 초기화 구문을 추가할 수 있는 기능이 도입되었습니다. 이를 통해 코드의 가독성을 높이고, 변수의 범위를 제한할 수 있습니다. 기본적인 형태는 다음과 같습니다.
    • if (init; condition) {
          // statements
      }
    • 여기서 init은 초기화 구문이며, condition은 조건식입니다. 예를 들어, C++17 이전에는 아래와 같이 작성해야 하는 코드가 있었습니다.
    • std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
      auto itr = m.find(2);
      if (itr != m.end()) {
          std::cout << itr->second;
      }
      C++17에서 도입된 if-초기화 구문을 사용하면 위의 코드를 아래와 같이 간결하게 작성할 수 있습니다.
    • std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
      if (auto itr = m.find(2); itr != m.end()) {
          std::cout << itr->second;
      }
      위의 예제에서 보듯이 if-초기화 구문을 사용하면 변수 itr의 범위가 if 문 내로 제한되므로 외부에서 해당 변수를 실수로 변경하는 것을 방지할 수 있습니다. 이런 특징은 코드의 안정성을 높여줍니다.
  3. Structured Bindings : 복합 데이터 타입을 여러 변수에 한번에 분해할 수 있는 기능입니다. 이를 통해 코드를 더 간결하고 가독성 있게 작성할 수 있습니다. 기본적인 사용 방법은 다음과 같습니다.
    • auto [var1, var2, ..., varN] = expression;
      여기서 expression은 tuple-like 객체, 구조체, 배열 등이 될 수 있습니다. 예를 들어, pair나 tuple을 반환하는 함수의 결과를 각각의 변수로 바로 분해할 수 있습니다.
    • std::pair<int, std::string> foo() {
          return {123, "Hello"};
      }
      
      int main() {
          auto [num, str] = foo();
          std::cout << num << ", " << str << '\n';  // 출력: 123, Hello
      }
    • 구조체도 마찬가지로 분해할 수 있습니다.
    • struct MyStruct {
          int num;
          std::string str;
      };
      
      MyStruct foo() {
          return {123, "Hello"};
      }
      
      int main() {
          auto [num, str] = foo();
          std::cout << num << ", " << str << '\n';  // 출력: 123, Hello
      }
    • 배열 역시 분해 가능합니다.
    • int main() {
          int arr[] = {1, 2};
          
          auto [a,b] = arr;
      
          std::cout<< a <<" "<< b<<'\n'; // 출력 : 1 2 
      }
    • Structured Bindings는 이처럼 복합 데이터 타입을 각각의 변수로 쉽게 분해하고 관리할 수 있게 해주므로 코드의 가독성과 편리성을 크게 높여줍니다.
  4. Nested namespace : C++17에서는 중첩된 네임스페이스를 간결하게 표현할 수 있는 기능이 도입되었습니다. 이전 버전의 C++에서는 네임스페이스를 중첩해서 사용하려면 아래와 같이 작성해야 했습니다.
    • namespace LevelOne {
          namespace LevelTwo {
              namespace LevelThree {
                  // 코드
              }
          }
      }
      하지만 C++17에서 도입된 Nested Namespace Declaration을 사용하면, 위의 코드를 아래와 같이 간결하게 작성할 수 있습니다.
    • namespace LevelOne::LevelTwo::LevelThree {
          // 코드
      }
    • 위의 두 코드는 완전히 동일한 기능을 수행합니다. 즉, LevelOne, LevelTwo, LevelThree라는 세 개의 네임스페이스가 순차적으로 중첩된 것입니다. Nested Namespace Declaration은 중첩된 네임스페이스를 더욱 간결하고 명확하게 표현할 수 있게 해줍니다. 이로 인해 코드의 가독성이 향상되고, 실수로 발생할 수 있는 문제도 줄어들게 됩니다.
  5. std::variant, std::any : C++17에서는 다양한 타입을 다루기 위한 새로운 유틸리티인 std::variant와 std::any가 도입되었습니다.
    1. std::variant는 합집합 타입(union type)으로, 여러 가지 타입 중 하나를 보관할 수 있는 클래스 템플릿입니다. 각각의 std::variant 객체는 언제든지 그 variant가 가질 수 있는 타입 중 하나를 저장할 수 있습니다.
      다음은 std::variant의 사용 예시입니다.
      • #include <iostream>
        #include <string>
        #include <variant>
        
        int main() {
            std::variant<int, float, std::string> v;
            
            v = 10;
            std::cout << std::get<int>(v) << '
        ';  // 출력: 10
        
            v = 1.5f;
            std::cout << std::get<float>(v) << '
        ';  // 출력: 1.5
        
            v = "Hello";
            std::cout << std::get<std::string>(v) << '
        ';  // 출력: Hello
            
            return 0;
        }
    2. 반면에 std:any, 이름에서 알 수 있듯이, 어떤 타입이든 저장할 수 있는 컨테이너입니다. 이를 통해 동적 타입을 지원하게 됩니다. 하지만 이런 유연성은 안정성을 저해할 수 있으므로 주의해서 사용해야 합니다.
      다음은 std:any의 사용 예시입니다.
      • #include <iostream>
        #include <any>
        
        int main() {
          	std::any a = 1;
          	std::cout << "integer: " << std:any_cast<int>(a) << '
        ';
          	
          	a = 'a';
          	std:cout<< "char : "<<std:any_cast<char>(a)<<'
        ';
        
          	a=1.5f; 
          	std:cout<<"float : "<<std:any_cast<float>(a)<<'
        ';
        
        	return 0;
        }
        여기서 주의할 점은 std:any_cast<>를 사용하여 적절한 형태로 변환해주어야 한다는 것입니다. 만약 잘못된 형태로 변환하려고 시도한다면, 프로그램은 예외(bad_any_cast)를 발생시킵니다.
      • std::any는 type safe한 void*에 가까우며 따라서 어떤 형식의 값도 대입이 가능하다. 다만 void* 와 다르게 객체의 적절한 파괴까지 보장해 준다는 점이 차이점이다. 그러나 메모리까지 자동으로 free해주는 것은 물론 아니다.
  6. std::byte : 메모리를 가공할 때 사용하는 타입입니다. 이는 정수형이 아닌, 열거형(enum class)으로 정의되어 있습니다. 따라서 std::byte는 숫자로서의 연산을 수행하지 않고, 비트 단위의 조작만 가능하게 설계되었습니다. 다음은 std::byte에 대한 간단한 예제입니다.
    • #include <cstddef>
      #include <iostream>
      
      int main() {
          std::byte b{0}; // 초기화
      
          b = std::byte{255}; // 255로 설정
          std::cout << static_cast<int>(b) << '\n';  // 출력: 255
      
          b <<= 1; // 왼쪽으로 한 비트 시프트
          std::cout << static_cast<int>(b) << '\n';  // 출력: 254 (비트 패턴이 왼쪽으로 한 칸 이동하여 최하위 비트가 0이 됩니다)
      
          return 0;
      }
    • 다만 이 std::byte는 windows의 기본 자료형인 byte와 표기가 겹치기 때문에, 아래의 구문을 사용하면 ambiguous symbol에러가 발생할 수 있다.
      • using namespace std;
    •  따라서 해당 구문을 사용하지 않고 항상 표준 라이브러리 함수나 자료형은 std::xxx 형태로 사용할 것을 권장하지만 기존 source가 해당 구문을 사용하고 있고 수정이 어려운 경우에는 다음 매크로를 stdafx.h등에 삽입한다.
      • #define _HAS_STD_BYTE 0
  7. ranged for에 변수 선언 및 초기화 가능 : ranged for에 변수 선언과 초기화가 가능해 졌다. 이를테면 이전에는 ranged for안에 순차적으로 증가하는 값이 요구될 경우 다음과 같이 coding해야 했다.
    • int index = 0;
      for(auto& i : values)
      {
            ++i;
            /* ... */
      }
      이제는 변수 선언부를 for안에 포섭할 수 있다.
    • for(int index = 0; auto& i : values)
      {
            ++i;
            /* ... */
      }
      따라서 해당 용도로 사용되는 변수의 scope를 제한할 수 있다.

 

기록하다보니 내용이 꽤 길어지내요. 다음번에는 C++ 20에서 주목할 만한 Feature들을 기록해 보겠습니다.

Bye~~

 

 

 

반응형