ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Modern C++ 알아보기
    PROGRAMMING/C++ 2024. 3. 17. 20:26

    Modern C++의 기본원칙  RAII(Resource Acquisition is Initialization)

    : 객체와 자원의 라이프 사이클을 일치시키자.

    C++이후 나온 언어들은 Garbage Collector(GC)라는 자원 청소기가 있어 프로그램에서 더 이상 사용하지 않는 자원을 자동으로 해제해주는 역할을 한다. 하지만 C++의 경우 사용자가 한 번 설정한 자원은 직접 해제해야만 하는데, 이 과정에서 메모리 누수가 일어날 수 있다. 

     

    메모리 누수를 방지하기 위해서는,  std::unique_ptrvector을 이용하는 것도 좋다. 

    - std::unique_ptr은 자원의 소유권을 객체 하나에게만 위임한다. 

    - vector은 동적 배열의 할당과 해제를 직접한다. 

     

    즉, RAII에서는 직접적인 new의 사용과 raw pointer의 사용을 피하는 것이 좋다.

     

    ★ std::unique_ptr

    이미 소멸된 객체를 다시 소멸시키는 버그를 범할 수 있다. 이런 버그를 double free 버그라고 부르는데, 이런 문제가 발생하는 이유는 객체의 소유권이 명확하지 않기 때문이다. unique_ptr은 특정 객체에 유일한 소유권을 부여하는 포인터 객체로 double free 버그를 방지할 수 있다. 

     

    정의 : std::unique_ptr<Widget> pw(new Widget())

    1. 복사 불가능

    ※ 아래는 복사 생성자를 명시적으로 삭제한 코드로 C++11부터 함수의 삭제 가능하다.

    //출처 : https://modoocode.com/229 
    #include <iostream>
    
    class A {
     public:
      A(int a){};
      A(const A& a) = delete;
    };
    
    int main() {
      A a(3);  // 가능
      A b(a);  // 불가능 (복사 생성자는 삭제됨)
    }

     

    2. 소유권 이전이 가능

    std::unique_ptr<Widget> pw1(new Widget());
    
    // pw2로 소유권 이전
    std::unique_ptr<Widget> pw2 = std::move(pw1);
    
    // 0(nullptr)을 가지고 있음
    std::cout << pw1.get();

     

    Dangling Pointer : 소유권이 이전된 unique_ptr로 재참조하면 런타임 오류가 발생

    3. unique_ptr의 참조자를 사용하지 말고, get을 통해 해당 객체의 포인터를 전달하자. 

    (unique_ptr의 get 함수는 실제 객체의 주소값을 리턴한다.)

    4. std::make_unique

    #include <memory>
    #include <iostream>
    
    class Foo {
    	int a, b;
    
    public:
    	Foo(int a, int b) : a(a), b(b) {};
    	void print() {
    		std::cout << "a : " << a << "b : " << b;
    	}
    	~Foo();
    };
    
    int main() {
    	// C++ 14부터 지원
    	auto ptr = std::make_unique<Foo>(3, 5);
    	// cf)
    	// auto ptr = std::unique_ptr ptr<new Foo(3, 5)>;
    	ptr->print();
    }

     

    C 스타일의 문자열 대신 std::string을 이용하자.

    또한 읽기 전용으로만 문자열을 사용하는 경우 std::string_view를 사용하면 성능상에 훨씬 큰 이점이 존재한다. 

     

    C스타일의 배열 대신 std::array와 벡터를 사용하자.

    출처 : 코드없는 프로그래밍

    그리고 벡터에 원소를 넣을 때는 push_back대신 emplace_back을 사용하자!

     

    명시적 형식 이름 대신 auto

    1. auto는 자료형(bool, char, wchar_t, char const *, int, double) 뿐만 아니라 참조형, 포인터,  함수형 포인터로도 사용가능하다.

    // 출처 : https://dydtjr1128.github.io/cpp/2019/06/04/Cpp-auto.html
    // 매개변수 없는 함수, Lambda
    auto printme = []{cout << "hello" << endl;};
    printme();
    
    // 매개변수 있는 함수, Lambda
    auto plus2 = [](int i) {return i+1;};
    cout << plus2(3) << endl;
    
    // 함수 자체(함수 내부에서 외부 변수 참조시 & 대입), Lambda
    int cnt = 0;
    auto k = [&](int i){cnt += i;};
    
    // 함수 자체(참조자를 리턴하는 경우), Lambda
    auto l = [](int *i)->int&{return *i};

    2. 다만 다음의 경우 auto를 사용할 수  없다.

    - 함수 매개변수

    - 구조체나 클래스 등의 멤버변수(그러나 리턴형으로는 사용 가능)

    //출처 : https://dydtjr1128.github.io/cpp/2019/06/04/Cpp-auto.html
    auto A(){     //에러 후행 반환 형식을 지정하지 않음
        return 3.2;
    }
    auto A()->double{  //정상, 함수 뒤에 후행 반환 형식 이라는 것을 지정해 주어야 한다.
        return 3.2;
    }
    // 출처 : https://dydtjr1128.github.io/cpp/2019/06/04/Cpp-auto.html
    template<typename T1, typename T2>
    auto A(T1 t1, T2 t2)->decltype(t1+t2){
        return t1+t2;
    }

     

    ▲ 위의 A라는 함수에서 int형과 double형이 매개변수로 들어오면 decltype에서 t1, t2를 더해서 double이 되고, auto가 자동적으로 double형태로 리턴이 된다. 즉, decltype을 이용해서 리턴 시킬 자료형을 예측 가능하도록 만들어 줄 수 있다(!!)

    (이 경우 자료형이 들어오는 순서를 고려하지 않아도 된다.)

     

    범위 기반 for 루프

    1. range-based for loops는 array, vector, list, set, map과 같은 구조에서도 작동한다.

    2. 포인터로 변환된 배열에서는 사용 불가능하다.(배열의 크기를 알지 못하기 때문!)

    #include <iostream>
    #include <vector>
    
    int main()
    {
        std::vector<int> v {1,2,3};
    
        // C-style
        for(int i = 0; i < v.size(); ++i)
            std::cout << v[i];
    
        // Modern C++:
        // 1. num에 v의 원소들이 '복사'되기 때문에 비용 多
        for(int num : v)
            std::cout << num;
        
        //--------추천하는 방법들------------
        //2. num에 v의 원소들이 '참조'되기 때문에 비용 小
        for(auto& num : v)
            std::cout << num;
            
        //3. num에 v의 원소들이 '상수 참조'되기 때문에 비용 小 + 읽기 전용
        for(const auto& num : v)
            std::cout << num;   
    }

    매크로 대신 constexpr식

    매크로 #define : 컴파일 전 전처리기에서 처리 / 오류가 발생하기 쉽고 디버깅이 어렵다

    그래서 Modern c++에서는 컴파일 시간 상수에 constexpr변수를 사용한다.

    #define SIZE 10 // C-style
    constexpr int size = 10; // modern C++

     

    * 상수식(constant expression) : 컴파일러가 컴파일 타임에 식의 값을 결정할 수 있는 식

    * 정수 상수식(integral constant expression) : 상수식 중 값이 정수인 식

    * tmp방식(template meta programming) : 템플릿을 사용하는 프로그래밍 기법으로 컴파일러에게 프로그램 코드를 생성하도록 하는 방식이다. 이러한 기법은 컴파일 시점에 많을 것을 결정하도록 하여, 실행 시점의 계산을 줄여준다. 

     

    함수 constexpr

    함수의 리턴타입에 constexpr을 추가해한다면 조건이 맞을 때 해당 함수의 리턴값을 컴파일 타임 상수로 만들어버릴 수 있다. 

    #include <iostream>
    
    constexpr int Factorial(int n) {
      int total = 1;
      for (int i = 1; i <= n; i++) {
        total *= i;
      }
      return total;
    }
    
    template <int N>
    struct A {
      int operator()() { return N; }
    };
    
    int main() { 
      // 컴파일 타임에 계산되어서 클래스 A의 템플릿 인자로!
      A<Factorial(10)> a;
      std::cout << a() << std::endl;
      
      // constexpr 함수에 인자로 컴파일 타임 상수가 아닌 값 전달시 일반 함수로 동작
      //런타임에 동작
      int num;
      std::cin >> num;
      std::cout << Factorial(num) << std::endl;
    }

    단  리터럴 타입이 아닌 변수 또는 초기화 되지 않은 변수의 정의를 포함할 수 없다.

     

    균일한 초기화

    Modern C++에서는 모든 형식에 중괄호 초기화를 사용할 수 있다. 

        // Modern C++:
        std::vector<S> v2 {s1, s2, s3};
    
        // or...
        std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

     

    이동 의미 체계

    move 작업은 복사본을 만들지 않고 한 개체에서 다름 개체로 리소스의 소유권을 이전한다.

     

    람다 식

    람다식은 captures, parameters, return type, body로 이루어져있다.

    // 출처 : https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/
    
    [captures](parameters) -> return type { body }
    
    /*
    * captures: comma(,)로 구분된 캡처들이 들어갑니다.
    * parameters: 함수의 인자들이 들어갑니다.
    * return type: 함수의 반환형입니다.
    * body: 함수의 몸통입니다.
    */
    // no parameter
    [](){std::cout << "Hello lambda!" << std::endl;}();
    
    // parameter
    [](std::string name){std::cout << name << std::endl;}("Jenny:);

     

    lambda는 std::function에 대입하거나 함수가 lambda를 반환하거나 STL container에 저장하는 것이 가능하다. 

    * Callable : ()를 붙여서 호출할 수 있는 모든 것

    * std::function : callable들을 객체의 형태로 보관하는 클래스

    C에서의 함수포인터가 진짜 함수들만 보관할 수 있는 객체라면, std::function은 함수 뿐만 아니라 모든 callable 들을 보고나할 수 있는 객체이다.

     

    또한 lambda는 값을 반환할 수 있고 그 반환형을 명시할 수 있다. 

     

    lambda의 캡처

    lambda 외부에 정의되어 있는 변수나 상수를 lambda 내부에서 사용하기 위해 캡처를 사용한다.(참조 또는 복사)

    //https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/
    //참조 방법
    #include <array>
    #include <algorithm>
    
    int main() {
        std::array<int, 5> numbers = { 1, 2, 3, 4, 5 };
        int sum = 0;
        
        // sum을 참조로 캡처
        // sum의 값은 15가 된다
        std::for_each(numbers.begin(), numbers.end(), [&sum](int& number) { 
            sum += number; 
        });
        
        return 0;
    }

     

    lambda의 지정자 

    지정자를 이용해 캡터된 변수를 몸통 안에서 어떻게 쓸 것인지 지정(mutable / constexpr; default = constexpr)

    • [a,&b] a를 복사로 캡처, b를 참조로 캡처.
    • [this] 클래스 멤버 함수 안 lambda 정의 시 현재 객체를 참조로 캡처 (private멤버 접근 가능)
    • [&] 몸통에서 쓰이는 모든 변수나 상수를 참조로 캡처하고 현재 객체를 참조로 캡처.(global)
    • [=] 몸통에서 쓰이는 모든 변수나 상수를 복사로 캡처하고 현재 객체를 참조로 캡처.(global)
    • [] 아무것도 캡처하지 않음.
    • [&, a] : 모든 변수나 상수 캡처, a만 복사로 캡처
    • [=, &b] : 모든 변수나 상수 캡처, b만 참조로 캡처

    lambda 재귀함수 : 대입된 std::function 함수를 참조로 캡처한 후 몸통에서 호출

    ※ lambd를 대입시킬 함수의 타입을 auto 키워드로 추론하면 안됨.

    #include <iostream>
    #include <functional>
    
    int main() {
        std::function<int (int)> factorial = [&factorial](int x) -> int {
            return x <= 1 ? 1 : x * factorial(x - 1);
        };
        
        std::cout << "factorial(5): " << factorial(5) << std::endl;
    }
     

     

    [추가로 알아보기]

    ☑️ 예외 및 오류 처리

    ☑️ std::atomic

    ☑️ std::variant

     

     

    0. 최신 C++

    https://learn.microsoft.com/ko-kr/cpp/cpp/welcome-back-to-cpp-modern-cpp?view=msvc-170

    1. RAII

    https://nx006.tistory.com/40

    * 병렬 부분은 나중에 다시 살펴보자.

    2. unique_ptr

    https://modoocode.com/229

    3. auto  (⭐)

    https://dydtjr1128.github.io/cpp/2019/06/04/Cpp-auto.html

    4.for loop (⭐)

    https://boycoding.tistory.com/210

    5. constexpr  (⭐)

    https://modoocode.com/293

    * constexpr 생성자부터 다시 보기

    6. tmp

    https://msjo.kr/2021/03/26/1/

    * tmp 프로그래밍 강좌 들어보기

    7. lambda  (⭐)

    https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/

    8. callable, std::function

    https://modoocode.com/254

     

    댓글

Designed by Tistory.