-
Modern C++ 알아보기PROGRAMMING/C++ 2024. 3. 17. 20:26
Modern C++의 기본원칙 RAII(Resource Acquisition is Initialization)
: 객체와 자원의 라이프 사이클을 일치시키자.
C++이후 나온 언어들은 Garbage Collector(GC)라는 자원 청소기가 있어 프로그램에서 더 이상 사용하지 않는 자원을 자동으로 해제해주는 역할을 한다. 하지만 C++의 경우 사용자가 한 번 설정한 자원은 직접 해제해야만 하는데, 이 과정에서 메모리 누수가 일어날 수 있다.
메모리 누수를 방지하기 위해서는, std::unique_ptr와 vector을 이용하는 것도 좋다.
- 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
* 병렬 부분은 나중에 다시 살펴보자.
2. unique_ptr
3. auto (⭐)
https://dydtjr1128.github.io/cpp/2019/06/04/Cpp-auto.html
4.for loop (⭐)
https://boycoding.tistory.com/210
5. constexpr (⭐)
* constexpr 생성자부터 다시 보기
6. tmp
* tmp 프로그래밍 강좌 들어보기
7. lambda (⭐)
https://blog.koriel.kr/modern-cpp-lambdayi-teugjinggwa-sayongbeob/
8. callable, std::function
'PROGRAMMING > C++' 카테고리의 다른 글
(백준 16562번 친구비 C++) 라이님 블로그 대회 알고리즘 따라잡기 12) Union Find 3 (0) 2024.05.09 절차형 재귀함수로 permutation 구현하기 (0) 2024.03.18 윤성우 열혈 C++ 프로그래밍 16장) C++의 형 변환 연산자 (2) 2024.01.15 윤성우 열혈 C++ 프로그래밍 15장) 예외처리(try, catch, throw, 스택풀기, 예외클래스, 예외 객체) (0) 2024.01.15 윤성우 열혈 C++ 프로그래밍 13,14장) 템플릿(template)(함수 템플릿, 템플릿 함수, 클래스 템플릿, 템플릿 클래스, 특수화, 부분 특수화) (0) 2024.01.12