C++ 17 에서 업데이트 된 기능 정리.

DevCho1107

·

2023. 4. 25. 14:04

이 글은 17년에 표준화 된 C++ 17 의 새로운 기능에 대한 정리한 글 입니다. 

 

1. Class Template argument Deduction ( 클래스 템플릿 인자 추론 ) 

기존에는 함수 템플릿 인자 추론이 가능했는데, 이제는 클래스 생성자 타입을 통한 인자 추론이 추가적으로 가능하다. 

인스턴스화 시 컴파일러가 자동으로 추론하는 기능이다. 

 

사용하면서 얻게되는 이점으로는 간결한 코드작성과 더불어 코드 변경 시에도 인스턴스화 된 타입을 일일이 수정할 필요가 없으므로 코드 유지보수성이 향상된다는 점이 있다. 더불어 타입이름을 생략할 수 있으므로 코드의 가독성을 높이는데도 도움이 된다. 

 

#include <iostream>
#include <vector>

// 기존 C++ 11 함수 템플릿 인자 추론 
template <typename T>
void print(T t) {
    std::cout << t << std::endl;
}


// C++ 14 의 추가된 클래스 템플릿 인자 추론 
template <typename T>
struct Pair {
    T first;
    T second;
    Pair(T f, T s) : first(f), second(s) {}
};


int main() {
    print(1); // TAD를 사용하여 print<int>(1)을 호출함
    print("Hello, world!"); // TAD를 사용하여 print<const char*>("Hello, world!")을 호출함
     
    auto p = Pair(1, 2); // CTAD를 사용하여 Pair<int> 타입을 추론함
    std::cout << p.first << " " << p.second << std::endl;
    
    // 클래스 템플릿 인자 추론을 통한 벡터 선언과 초기화.
	std::vector v = { 1, 2, 3 }; // CTAD를 사용하여 std::vector<int> 타입을 추론함
    for (auto i : v) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

 

 

2. if with initializer ( if문 초기화식 사용 )

C++ 17 부터는 'if' 문에서 초기화식을 사용할 수 있게 되었다. 좀 더 자세히 말하자면, if 스코프 내에서 변수를 선언하고 초기화 할 수 있는 기능이다. 

사용하면서 얻게 되는 이점으로는 코드의 가독성 향상 및 초기화와 조건식을 동시에 처리할 수 있어서 유용하다는 점이다.

 

#include <iostream>

int main() {
    if (int i = 42; i > 0) { // 초기화식을 사용하여 i를 42로 초기화함
        std::cout << "i is positive." << std::endl;
    }
    return 0;
}

 

3. constexpr if ( 컴파일 조건문 )

constexpr if 는 컴파일 시간에 조건을 평가하여 해당 조건이 참인 경우에만 해당 코드 블록을 실행하는 기능이다. 

덕분에 컴파일 단계에서 불필요한 코드를 제거할 수 있고, 컴파일 시간에 코드를 최적화 하거나 제어할 때 유용하게 사용 가능하다.

 

#include <iostream>
#include <type_traits> // 타입검사를 위한 클래스 제공 

template <typename T>
void foo(T t) {
    if constexpr (std::is_integral_v<T>) { // T가 정수 타입인 경우
        std::cout << "T is integral." << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) { // T가 부동소수점 타입인 경우
        std::cout << "T is floating point." << std::endl;
    } else { // 그 외의 경우
        std::cout << "T is neither integral nor floating point." << std::endl;
    }
}

int main() {
    foo(42);
    foo(3.14);
    foo("hello");
    return 0;
}

 

4. Structed Bindings ( 구조화된 바인딩 ) 

구조화 바인딩은 튜플,구조체,배열과 같은 데이터 타입을 분해하여 개별 변수에 저장하는 기능을 제공한다. 

사용하면서 얻게되는 이점으로는 간결한 코드작성과 더불어 가독성을 향상 시킬 수 있다. 

문법은 아래와 같다. 

 

#include <iostream>
#include <tuple>

std::tuple<int, std::string, double> get_data() {
    return std::make_tuple(42, "hello", 3.14);
}

int main() {
	
    // auto [variable1, variable2, ...] = expression;
    auto [value, text, number] = get_data(); // 구조화된 바인딩 
    std::cout << "Value: " << value << ", Text: " << text << ", Number: " << number << std::endl;
    
    // C++ 17 이전 튜플의 값을 불러오는 경우 
    std::tuple<int, std::string, double> data = get_data();
    int value = std::get<0>(data);
    std::string text = std::get<1>(data);
    double number = std::get<2>(data);
}

 

5. inline variables ( 인라인 변수 )

이전에 존재하던 inline 키워드는 주로 함수를 인라인 함수로 선언하여 사용했다. 애초에 inline 함수를 작성자가 붙여도 컴파일러가 판단해서 함수 본문으로 대체할지 여부를 판단했다. 

기존에도 변수에 inline 키워드를 사용 할 수 있었지만, 변수에 대해서는 일반적으로 사용하지 않았다. 

그런데 C++ 17 부터는 변수에 대한 inline 키워드 사용이 확장되었다. 

또한, 헤더파일에서 정적멤버 변수를 선언만 할 수 있었는데, 초기화 또한 선언과 동시에 할 수 있게 되었다. 

 

class MyClass {
public:
    static inline int static_member = 42;
};


// C++ 17 이전 MyClass.h
class MyClass {
public:
    static int static_member;
};
// C++ 17 이전 MyClass.cpp
#include "MyClass.h"

int MyClass::static_member = 42;

 

6. Nested Namespace Definition ( 중첩된 네임스페이스 정의 ) 

기존에 네임스페이스는 각각의 네임스페이스를 별도로 만들어야 했는데, 이게 한번에 가능하도록 변경되었다. 

이로인해 간결한 코드작성이 가능해졌다. 

 

// C++ 17 이전 네임스페이스 정의 
namespace A {
    namespace B {
        namespace C {
            int my_function();
        }
    }
}

// C++ 17 에 추가된 네임스페이스 중첩 방식 
namespace A::B::C {
    int my_function();
}

 

7. Fold Expressions ( 폴드 표현식 ) 

C++ 11 에 추가됐던 Variadic Templete 에서 가변 인수 템플릿이 가능해지면서, 이를 사용하여 연산을 수행하려면 보통 재귀 템플릿을 사용해야 했고, 마치는 함수를 만들어야 하는 번거로움이 있었다. 때문에 코드가 복잡해지고 가독성이 떨어지는 문제들이 있었는데, 이를 해결하고자 추가된 기능이라고 볼 수 있다. 

 

// C++17 이전
// 기본 템플릿 (재귀 종료)
template<typename T>
T sum(T value) {
    return value;
}

// 재귀 템플릿
template<typename T, typename... Args>
T sum(T first, Args... rest) {
    return first + sum(rest...);
}

int main() {
    int result = sum(1, 2, 3, 4, 5); // result = 15
}

// C++17 이후 Fold Expression 사용
template<typename... Args>
auto sum(Args... args) {
    return (... + args); // Unary left fold
}

int main() {
    int result = sum(1, 2, 3, 4, 5); // result = 15
}

fold Expression 에서 사용가능한 이항 연산자들은 다음과 같다. 

  • +, -, *, /, % 
  • &, |, ^, <<, >>
  • &&, ||
  • ==, !=, <, >, <=, >=
  • =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

위에 예제에서 볼 수 있듯, Fold Expression 을 통해 가변 인수 템플릿과 관련된 코드를 더 간결하게 알아볼 수 있게되었다. 

 

8. New Attributes (nodiscard ,maybe_unused ,fallthrough )

우리가 함수를 만들때 파라미터를 선언하고 사용하지 않는다던지, 반환값을 사용하지 않는 경우가 종종 있다. 

이를테면 기존 함수를 변경한다던지, 정책상 필요없어진 값에 대한 미사용이라던지.. 

혹은 반대로 사용해야 할 변수와 반환값을 사용하지 않는 경우도 있을 것 이다. 

이 경우 nodiscard 속성을 명시하여 경고를 생성하도록 할 수 있다. 

 

[[nodiscard]] int foo() {
    return 42;
}

int main() {
    foo(); // 경고: 반환 값을 사용하지 않음
}

 

maybe_unused  속성은 변수, 함수 혹은 타입이 사용되지 않을 수도 있음을 명시하는 속성이다. 

컴파일러가 사용되지 않는 변수나 함수에 대한 경고를 생성하지 않게 할 수 있다. 

무지성으로 해당 경고에 대한 Ignore 혹은 #pragma warning 을 사용하지 않는 것 이 바람직하다. 

 

[[maybe_unused]] void unused_function() {
    // 사용되지 않을 수 있는 함수
}

int main() {
    [[maybe_unused]] int unused_variable = 42;
    // 사용되지 않을 수 있는 변수
}

 

fallthrough 속성은 switch 문에서 고의로 두개 이상의 의도된 through을 명시할 때 쓰인다. 

이를 통해 컴파일러가 의도하지 않은 fallthrough 에 대한 경고를 생성하지 않게 할 수 있다. 

 

void process(int value) {
    switch (value) {
        case 1:
            // 일부 작업 수행
            [[fallthrough]];
        case 2:
            // 다른 작업 수행
            break;
        default:
            // 기본 작업 수행
    }
}

 

 

9. optional

이것도 boost 라이브러리에서 넘어온 기능인데, 값이 존재하지 않을 수 있는 타입을 나타내는 템플릿 클래스이다. 

우리는 함수에서 한가지 값을 반환 할 수 있다. 

그 이상의 반환값을 원하는 경우 출력 매개변수를 사용하거나 아니면 tuple, pair 등의 클래스를 사용해야 했다. 

예를 들어 pair의 경우 해당되는 값이 없더라도 객체의 생성이 불가피하다. 

이를 위해 좀 더 깔끔하고 명확하게 작성 할 수 있는 기능으로 도입된 것이 optional 이다. 

단순히 int 정도의 변수를 예제로 들었지만, 더 큰 객체의 경우는 optional 과 비교해서 생성자에서 소모되는 비용이 많은지를 고려해 최적화를 할 수 있을거같다. 

#include <iostream>
#include <optional>

std::optional<int> get_optional_value(bool has_value)
{
    if (has_value)
        return 42;
    else
        return std::nullopt;
}

int main()
{
    auto value1 = get_optional_value(true);
    if (value1.has_value())
        std::cout << "Value 1 is " << value1.value() << std::endl;

    auto value2 = get_optional_value(false);
    if (!value2.has_value())
        std::cout << "Value 2 is empty" << std::endl;
    
    return 0;
}

위 코드에서 get_optional_value() 함수는 has_value 매개변수가 true 면 42를, false 면 std::nullopt 를 반환한다. 

std::nullopt 는 객체가 값을 가지고 있지 않음을 나타내는 특별한 값이다. 

아래 main 함수를 보면 optinal객체 value1 에 반환값을 저장하고 value1.has_value() 를 통해 해당 객체가 값을 가지고 있는지를 확인하고 있다. 

 

 

10. variant

이것도 boost에서 넘어온 라이브러린데, 다양한 타입의 변수에 대한 값을 저장할 수 있는 공간을 제공하는 기능이다. 

union 이랑 비슷한데, variant 같은 경우는 union 처럼 멤버 타입의 크기, 정렬사항 등이 모두 같을 필요가 없는게 특징이며, 생성자와 소멸자등을 각각의 멤버타입마다 정의할 필요가 없다. 

variant 의 가장 큰 특징으로는 반드시 어떠한 값을 들고 있어야 한다는 점이다. 

맨 첫번째로 선언한 타입의 디폴트생성자가 호출되어 variant 객체에 할당된다. 

 

#include <iostream>
#include <variant>
#include <string>

int main() {
  std::variant<int, double, std::string> my_variant;

  my_variant = 3;
  std::cout << "Variant contains int: " << std::get<int>(my_variant) << std::endl;

  my_variant = 3.14;
  std::cout << "Variant contains double: " << std::get<double>(my_variant) << std::endl;

  my_variant = "Hello, variant!";
  std::cout << "Variant contains string: " << std::get<std::string>(my_variant) << std::endl;

  return 0;
}

추가적으로 variant는 위에 설명한 optional 기능과 마찬가지로 객체의 대입 시 어떠한 동적 할당도 발생하지 않는다. 

 

'< Programming > > C++' 카테고리의 다른 글

emplace_back  (0) 2023.05.08
constexpr ( generalized constant expressions )  (0) 2023.05.04
RingBuffer 구현예제.  (0) 2023.04.20
std::Funtion 정리  (0) 2023.04.06
C++ Window IOCP Server  (0) 2023.04.06