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 {
..........
}; }