Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++17 핵심 신기능 총정리: 더 깔끔하고 간결한 코드 작성하기

C++ 표준 위원회는 3년마다 새로운 기능을 담은 표준을 발표하는 데 주력해 왔습니다. C++ 표준의 두 가지 핵심 축은 프로그래밍 언어 자체의 핵심 기능과 표준 템플릿 라이브러리(STL)입니다. 새로운 기능들은 코드를 더 깔끔하고, 읽기 쉽고, 간결하게 만들어 주는 데 초점이 맞춰져 있습니다. 아래에서 C++17에 도입된 주요 기능들을 하나씩 살펴보겠습니다.

1. 폴드 표현식(Fold Expressions)

폴드 표현식은 가변 개수의 인자를 함수에 전달하거나 함수에서 반환할 때 작성해야 하는 코드를 크게 줄여 줍니다. 덕분에 임의 개수의 변수를 함수의 인자나 반환문에서 손쉽게 다룰 수 있습니다.

문법

  • 단항 우측 폴드(Unary Right Fold) - ( pack op1 ... )

  • 단항 좌측 폴드(Unary Left Fold) - ( ... op1 pack )

  • 이진 좌측 폴드(Binary Left Fold) - ( init op1 ... op1 pack )

  • 이진 우측 폴드(Binary Right Fold) - ( pack op1 ... op1 init )

여기서 pack은 임의 개수의 변수로 확장될 수 있는 매개변수 팩(parameter pack)이며, op1은 연산자(-, +, <=, >=, <, >, ==, *, / 등)를 의미합니다. 이진 폴드(binary fold)에서는 두 op1이 반드시 같은 연산자여야 하며, init은 확장되지 않는 초기값 표현식입니다.

예제

#include <iostream>
#include <string>
using namespace std;
template<typename ...Args> auto addition(Args ...args){
    return (args + ... + 0);
}
template<typename ...Args> auto sum2(Args ...args){
    return (args + ...);
}
int main(){
   cout << "Sum is : "<<addition(1,1,1,1,1) << endl;
   cout << "Sum 2 is : "<<addition ( 1,2,3);
}

실행 결과

Sum is : 5
Sum 2 is : 6

2. 구조적 바인딩(Structured Bindings)

구조적 바인딩은 pair, tuple 등에 담긴 여러 값을 한 번에 여러 변수로 초기화할 때 사용합니다. 변수와 초기값의 바인딩이 단 하나의 문장으로 처리되므로 코드가 훨씬 간결해집니다.

케이스 1: 배열 바인딩

식별자 목록의 각 식별자는 배열 요소에 대한 lvalue 이름이 됩니다. 이때 요소의 개수는 식별자의 개수와 일치해야 합니다.

int arry[3] = { 3,4,5 };
auto [a,b,c] = arry;
// 배열이 생성되고, a는 3, b는 4, c는 5를 참조합니다.

케이스 2: tuple과 유사한 타입 바인딩

float fnum{};
char ch1{};
int number{};
std::tuple<float&, char&&, int> tplex( fnum, std::move(ch1), number);
const auto& [ p, q, r] = tplex;
// p는 fnum을 참조하는 구조적 바인딩 이름입니다.
// q는 ch1을 참조하는 구조적 바인딩 이름입니다.
// r는 number를 참조하는 구조적 바인딩 이름입니다.

케이스 3: 데이터 멤버 바인딩

struct structVar {
    mutable int num1 : 2;
    volatile double num2;
};
structVar func();
const auto [ a, b] = func();
// a는 2비트 비트 필드(bit field)에 대한 int lvalue입니다.
// b는 const volatile double lvalue입니다.

3. 직접 리스트 초기화(Direct List Initialization)를 통한 enum 초기화

C++17부터는 열거형(enum)도 중괄호({})를 사용해 초기화할 수 있습니다. 이를 통해 범위를 벗어난 값의 실수 초기화를 컴파일 시점에 방지할 수 있습니다.

enum byte : unsigned char {};
byte b0 {0};            // OK
byte b1 = byte{1};      // OK
byte b2 = byte{256};    // ERROR - 0~255 범위만 허용

4. if문과 switch문 내부의 변수 선언

C++17에서는 if문과 switch문의 조건 영역 안에서 변수를 선언할 수 있습니다. 이를 통해 동일한 이름의 변수를 서로 다른 스코프(scope)에서 충돌 없이 사용하기가 훨씬 쉬워집니다.

// if문 안에서 변수 선언
if (auto data = getValue(); data > 10)
{
   // statements
}

// switch문 안에서 변수 선언
switch (auto key = getKey(); key)
{
   // statements
}

5. if constexpr 문

if constexpr은 템플릿 코드에서 특히 유용한 기능으로, 컴파일 타임에 조건이 평가됩니다. 일반 if-else문과 비교하면 그 차이를 명확히 알 수 있습니다.

일반 if-else문

int var = 10;
if (var >= 10) {
   var = var + 10;
} else {
   var = var - 10;
}

constexpr if-else문

template <typename T>
auto length ( T const& value ) { 
   // T가 정수 타입인지 검사
   if constexpr (is_integral<T>::value) {
       return value;
   } else {
       return value.length();
   }
}

일반 if문을 사용하면 else 분기의 value.length()가 정수 타입 인스턴스화 과정에서도 컴파일되려다 실패할 수 있지만, if constexpr을 사용하면 조건을 만족하지 않는 분기는 컴파일 타임에 아예 제거되므로 안전합니다.

6. 중첩 네임스페이스(Nested Namespaces)

네임스페이스는 서로 연관된 클래스나 함수 같은 코드들을 그룹으로 묶는 데 사용됩니다. C++17에서는 중첩 네임스페이스를 훨씬 간단한 문법으로 작성할 수 있게 되었습니다. 기존에는 중첩 단계가 많아질수록 문법이 지저분해졌지만, 이제는 닫는 중괄호를 여러 개 관리할 필요가 없습니다.

C++17 이전 방식

namespace Earth{

   namespace Continent {
      namespace Country {
         class City {
         ..........
}; } } }

C++17 새 문법

namespace Earth :: Continent :: Country {
   class City {
      ..........
}; }