우선 5일가량 modern c++에서 새로 추가된 개념들이나 업그레이드된 내용들을 쭉 읽어봤다.
근데 솔직히 말해서 거의 아무것도 이해 못했다.
처음 읽고 다음날 다시 읽고 반복해서 읽으면서 정리를 해봐도 그냥 그렇구나 하고 받아들여지는 정도였지
이걸 이런때 쓰면 좋겠네 라던지 저번에 이런게 불편했는데 이런게 나왔네 하는 깨닳음이 없었다
결국 다 읽고 나서 낸 결론은
modern c++로 넘어오면서 추가된 개념들은 대체로 기존에 있던 문제점을 개선했거나, 불편했던 점들을 효율적이게 만들어 준 것이라고 볼 수 있는데 문제는 내가 그 불편함을 겪어본적이 없으니
이게 왜 필요한건지, 이걸 어디에 써먹어야 할지 감을 못잡는다라는 것
그러니 결국 이걸 계속 공부하고 읽기보다는 우선 경험을 더 많이 쌓고 불편한 요소들을 직접 느껴봐야 새로 나온 기능들이 왜 필요했는지, 왜 나왔는지 체감할 수 있을 듯 하다.
그러니 마침 다 읽기도 했고... 이제부터 한동안은 무조건 경험 쌓고 그냥 많이 해보는 쪽으로 가야 더 발전이 가능할 듯 싶다.
우선 몇일간 공부한 내용들을 크게 정리하지 않고 아래에 쭉 복붙은 해두겠지만, 이 내용들은 공부했으니 올린다 라기 보다는
훗날 실력이 쌓인 후 내가 그땐 이런걸 이해 못했었지 회상하는 느낌으로 쓰기 위해 일단 남겨만 두기로 한다.
/*****************************************************************************************************************************************
nullptr : 포인터 주소값 0
c에서는 #defind NULL 0 으로 그냥 NULL은 다 0취급이었는데
이게 0을 의미하는건지, 포인터주소값 0을 의미하는건지 구분이 불가능했음
그래서 char* str = nullptr; 하면 그냥 포인터주소값 0을 의미함
좌측값과 우측값
int a = 3; 처럼 있을때
왼쪽의 a 처럼 &을 이용해서 주소값을 취할 수 있는 값을 좌측값이라고 부름
즉 좌측값은 = 등호의 왼쪽 오른쪽 양쪽 다 갈 수 있음
꼭 좌측에만 있다는 뜻이 아님
반면 오른쪽의 3은 주소값을 취할 수 없고 연산할때 잠깐 존재하고 사라지는 값이며
이런 값을 우측값이라고 부름
우측값은 좌측값과 달리 반드시 오른쪽에만 와야 함
지금까지 다뤄 왔던 레퍼런스는 좌측값에만 레퍼런스를 취할 수 있음
int a;
int& left_a = a; // 가능
int& right_b = 3; // 불가능
위처럼 & 하나를 이용해서 정의하는 레퍼런스를 좌측값 레퍼런스라고 부름
또한 좌측값 레퍼런스 자체 또한 좌측값임
우측값 레퍼런스는 &&처럼 두번써서 사용 가능
int a;
int& l_a = a;
int& ll_a = 3; // 불가능
int&& r_b = 3;
int&& rr_b = a; // 불가능
우측값인 3을 r_b로 레퍼런스 가능
하지만 a는 좌측값이니까 &&로 사용 불가능
우측값에 대한 레퍼런스로만 사용 가능
move함수
#include <utility>
move함수는 좌측값을 우측값으로 바꾸어주는 함수
A c(move(a)); 처럼 쓰면 이동생성자가 호출 됨
move는 이름과 달리 뭘 이동시키는게 아님. 그냥 인자로 받은 객체를 우측값으로 변환해서 리턴함
forward 함수
g(forward<T>(u));
u가 우측값 레퍼런스일 때만 move를 적용한것처럼 작동
RAII패턴
Resource Acquisition Is Initialization
자원의 획득은 초기화다
예외의 발생 혹은 프로그래머의 실수로 할당 해제를 제대로 해주지 못하게 될 경우를 커버하기 위해 스마트포인터 사용
스마트 포인터
기존엔 객체 만들어서 쓰거나 auto_ptr 썼는데 c++이후는 아래 두가지 사용
unique_ptr , shared_ptr, weak_ptr
- unique_ptr
auto ptr = make_unique<Foo>(3, 5); 처럼 만들고 ptr->some처럼 사용
Data* data = new Data();
Date* data2 = data;
delete data;
delete data2;
위처럼 사용시 data와 data2가 가리키고 있던곳은 사실 한곳이라서
delete data로 이미 지운곳을 delete data2로 또 지우고 있으니
double free 오류가 발생하게 됨
그래서 어떤 포인터에 객체의 유일한 소유권을 부여해서 이포인터 말고는 객체를 소멸시킬 수 없게 만드는게 unique_ptr
std::unique_ptr<A> pa(new A());
이건 마치 A* pa = new A();와 같이 쓰이는 셈이고 pa->some 처럼 일반적인 포인터처럼 사용 가능
또한 pa는 스택에 정의된 객체라서 scope가 끝날때 자동으로 소멸자가 호출 됨
이때 unique_ptr은 소멸자 안에서 자신이 가리키고 있는 자원을 해제하기 때문에 자원이 잘 해제 됨
소유권 이전
std::unique_ptr<A> pb = std::move(pa);
소유권 이전 후 pa에는 nullptr이 들어있고 이렇게 소유권이 이전된 unique_ptr을 댕글링 포인터라고 부름
댕글링포인터에 접근하면 런타임 오류가 발생하니 소유권 이전시에는 이전의 unique_ptr에 접근하지 않는다는 확신하에 이동해야함
unique_ptr을 함수의 매개변수로 전달할때는
그냥 call of value나 레퍼런스로 주면 unique하지 않게 되므로 직접 주소값을 전달해 줘야 함
void func(A* ptr) 처럼
c++14부터 unique_ptr을 쉽게 만드는 make_unique 제공
auto ptr = make_unique<Foo>(3, 5);
처럼 쓰면 기존의
unique_ptr<Foo> ptr(new Foo(3, 5)); 방식처럼 임시 객체 생성해서 거쳐갈 필요 없음
- shared_ptr
std::shared_ptr<A> p1 = std::make_shared<A>();
여러개의 스마트 포인터가 하나의 객체를 같이 소유해야 하는 경우
달리 말하면 여러 객체에서 하나의 자원을 사용하고자 할 경우
이 자원을 해제하기 위해서는 이 자원을 사용하는 모든 객체들이 소멸되어야 하는데
어느 객체가 먼저 소멸될 지 알 수 없기때문에 이 자원을 언제 해제해줘야 핧지도 알 수 없음
그래서 특정 자원을 몇개의 객체에서 가리키는지 추적한 다음 그 수가 0이 될때 해제해주는 방식이 필요
unique_ptr은
std::unique_ptr<A> p1(new A());
std::unique_ptr<A> p2(p1); // 컴파일 오류!
위처럼 복사생성자가 막혀있지만
shared_ptr은
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.
위처럼 복사생성자로 같은 객체를 가리 킬 수 있음
shared_ptr 사용시 주의점
shared_ptr은 인자로 주소값이 전달되면 자기가 해당 객체를 첫번째로 소유하는 shared_ptr처럼 행동함
A* a = new A();
std::shared_ptr<A> pa1(a);
std::shared_ptr<A> pa2(a);
이러면 pa1과 pa2가 각각 ref count를 1씩 가져버림
이러면 pa1이 소멸되면 A객체를 소멸시켜버리니 pa2에서 문제가 생김
그래서 기본적으로 포인터를 이용해서 shared_ptr을 만드는건 추천하지 않음
그치만 꼭 써야한다면(this포인터 사용을 해야한다거나)
enable_shared_from_this를 상속받으면 된다.
weak_ptr
순환참조를 막기 위해 사용
일반포인터와 shared_ptr 사이에 위치한 스마트포인터
스마트포인터처럼 객ㅊ를 안전하게 참조할 수 있게 ㅐ주지만
shared_ptr과 달리 참조 개수를 늘리지 않음
어떤 객체를 weak_ptr이 가리키고 있다고 해도, 다른 shared_ptr들이 가리키고 있지 않다면 이미 메모리에서 소멸되어있음
그래서 weak_ptr로는 객체를 참조할수 없고 반드시 shared_ptr로 변환해서 사용해야 함
std::shared_ptr<A> o = other.lock();
위처럼 lock을 사용해서 weak_ptr이 가리키는 객체가 아직 메모리에 살아있으면 해당 객체를 가리키는 shared_ptr을 반환
이미 해제 되었다면 false를 리턴
callable
- 이름대로 호출 할 수 있는 모든것
c++에서는 ()를 붙여서 호출 할 수 있는 모든것을 callable이라고 함
즉 함수도, 생성자도, 람다함수도 다 callable
callable들을 객체의 형태로 보관할 수 있는 std::function 클래스를 제공함
C의 함수포인터는 진짜 함수들만 보관할 수 있지만 std::function은 callable을 모두 보관 가능
#include <functional>
#include <iostream>
#include <string>
int some_func1(const std::string& a) {
std::cout << "Func1 호출! " << a << std::endl;
return 0;
}
struct S {
void operator()(char c) { std::cout << "Func2 호출! " << c << std::endl; }
};
int main() {
std::function<int(const std::string&)> f1 = some_func1;
std::function<void(char)> f2 = S();
std::function<void()> f3 = []() { std::cout << "Func3 호출! " << std::endl; };
f1("hello");
f2('c');
f3();
}
decltype
타입을 알고자 할때 쓸 수 있음
함수처럼 쓰면 되지만 define 매크로처럼 결과가 그 타입으로 치환 됨
예를들어서
int a = 3;
decltype(a) b = 2;
위처럼 쓰면 decltype(a) 부분이 그냥 int로 치환된다고 생각하면 됨
auto와 다른점은 auto는 대체 가능한거면 그냥 사용하지만 decltype은 정확히 같은거로만 씀
예를들어
const int i = 4;
auto j = i; 하면 auto는 const int가 아니라 int로 할 수도 있는데 decltype은 정확히 같게 씀
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
만약
decltype(t + u) add(T t, U u) { ...} 이었으면
t와 u가 선언 되지 않았을때 호출하니 오류가 생김
그래서 위에 -> 사용해서 람다 비슷한 모양새로 구현 가능
모든 c++식에는 "타입"과 "값 카테고리"가 따라다님
타입은 그냥 타입이고
값 카테고리는 좌측값/우측값 등의 총 5가지 카테고리가 있음
이동시킨다가 정확히 무슨 말이지?
declval 함수를 사용해서 원하는 타입의 생성자 호출을 우회해서 멤버 함수의 타입에 접근할 수 있음
템플릿 메타 함수
메타란?
보통의 함수들은 값에 대한 연산을 수행하지만
메타함수는 타입에 대한 연산을 수행함
정규표현식
정규표현식은 문자열에서 패턴을 찾는데 사용
- 주어진 문자열이 주어진 규칙에 맞는지 확인할 때
- 주어진 문자열에서 원하는 패턴의 문자열을 검색할 때
- 주어진 문자열에서 원하는 패턴의 문자열로 치환할 때
정규표현식을 사용하기 위해선 정규표현식 객체를 정의해야 함
std::regex re("db-\\d*-log\\.txt");
뒤에 속성을 부여할 수도 있는데 대소문자를 구분하지 않고 싶다면
std::regex re("db-\\d*-log\\.txt", std::regex::icase); 처럼 속성 부여하면 됨
그 다음 std::regex_match(names, re) 처럼 사용하면 names가 re의 객체와 완전히 매칭되면 true, 아니면 false 리턴
random 라이브러리
#include <iostream>
#include <random>
int main() {
// 시드값을 얻기 위한 random_device 생성.
std::random_device rd;
// random_device 를 통해 난수 생성 엔진을 초기화 한다.
std::mt19937 gen(rd());
// 0 부터 99 까지 균등하게 나타나는 난수열을 생성하기 위해 균등 분포 정의.
std::uniform_int_distribution<int> dis(0, 99);
for (int i = 0; i < 5; i++) {
std::cout << "난수 : " << dis(gen) << std::endl;
}
}
chrono 라이브러리
시간 관련 데이터를 쉽게 계산할 수 있게 도와줌
크게 아래 3가지 요소들로 구성
현재의 시간을 알려주는 시계 - system_clock, high_resolution_clock 등등
특정 시간을 나타내는 - time_stamp
시간의 간격을 나타내는 - duration
system_clock의 작동원리
1970년 1월 1일부터 현재까지 발생한 틱의 횟수를 리턴함
즉 system_clock은 clock의 시작점과, 현재시간과의 duration을 보관하는 객체
#include <chrono>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>
namespace ch = std::chrono;
int main() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, 1000);
for (int total = 1; total <= 1000000; total *= 10) {
std::vector<int> random_numbers;
random_numbers.reserve(total);
ch::time_point<ch::high_resolution_clock> start =
ch::high_resolution_clock::now();
for (int i = 0; i < total; i++) {
random_numbers.push_back(dist(gen));
}
ch::time_point<ch::high_resolution_clock> end =
ch::high_resolution_clock::now();
auto diff = end - start;
std::cout << std::setw(7) << total << "개 난수 생성 시 걸리는 시간: "
<< ch::duration_cast<ch::microseconds>(diff).count() << "us"
<< std::endl;
}
}
현재 시간을 날짜로
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << "현재 시간은 : " << std::put_time(std::localtime(&t), "%F %T %z")
<< '\n';
파일 시스템 라이브러리
<filesystem>
우선 파일 입출력 관련인 fstream은 하나의 파일이 주어지면 그 파일의 데이터를 읽고 쓰는 역할
하지만 fstream은 그 파일에 관한 정보인 파일이름, 위치 등등에 대해 수정할 수는 없음
반면 파일시스템 라이브러리는 파일에 관한 정보들을 다루는 역할이고, 파일을 읽는 일을 하는게 아님
하드디스크 어딘가에 a.txt를 찾고싶으면 filesystem라이브러리를 활용
a.txt를 찾은 후 그 파일 내용을 읽고쓰고 싶으면 fstream을 사용
#include <filesystem>
#include <iostream>
int main() {
std::filesystem::path p("./some_file");
std::cout << "Does " << p << " exist? [" << std::boolalpha
<< std::filesystem::exists(p) << "]" << std::endl;
std::cout << "Is " << p << " file? [" << std::filesystem::is_regular_file(p)
<< "]" << std::endl;
std::cout << "Is " << p << " directory? [" << std::filesystem::is_directory(p)
<< "]" << std::endl;
}
std::cout << "내 현재 경로 : " << fs::current_path() << std::endl;
std::cout << "상대 경로 : " << p.relative_path() << std::endl;
std::cout << "절대 경로 : " << fs::absolute(p) << std::endl;
std::cout << "공식적인 절대 경로 : " << fs::canonical(p) << std::endl;
ofstream을 이용해서 없는 파일에 접근하면 그 파일을 만드는게 가능했음
하지만 디렉토리를 만드는건 불가능해서 ./a/b.txt처럼 접근하면
a디렉터리를 만들 수 없으니 실행이 불가능했음
이런식으로 디렉터리를 만드는건 filesystem으로 가능