array

- 저장할 개수가 고정적일때 사용

- 적은양의 자료에 유리

- 크기 변경 불가



vector

- 기본적으로 사용되는 컨테이너

- 저장할 개수가 가변적일때 사용

- 적은양의 자료에 유리

- 크기 변경 가능

- 순차 접근 가능

- 랜덤 엑세스 용이 ([idx]로 접근가능)

- 중간 삽입/삭제 느림

- 검색이 느림

- 많은양의 자료에 불리



deque

- 데이터를 앞뒤에서 삽입, 삭제 가능

- 저장할 개수가 가변적일때 사용

- 검색이 느림

- 랜덤 엑세스 용이

- 저장 데이터가 많고 메모리 할당량이 큰경우 vector에 비해 확장비용이 절감

- 중간 삽입/삭제 느림

- 삭제시 메모리 사용량이 줄어든다

- 앞뒤에 자주 삽입 / 삭제가 일어날 경우 사용



list

- 이중 연결 리스트

- 저장할 개수가 가변적일때 사용

- 이동 / 삭제 / 삽입이 빠름 (최대장점)

- 중간 삽입 / 삭제 용이

- 검색이 느림

- 랜덤 엑세스에 불리

- 순차 엑세스만 가능

- 적은양의 데이터에 유리



forward_list

- 기존의 list 컨테이너 설계에 맞춘다

- 단방향 연결 리스트

- 각 데이터는 다음 데이터를 가리키는 포인터 하나만 가지고있다

- list에 비해 메모리를 적게 사용

- list보다 삽입 / 삭제 속도가 빠르지만 차이는 크지않다

- 다른 컨테이너에서 지원하는 일부 기능이 없다

- 기능적으로는 list가 좋지만 성능면에서는 list가 좋다

- 단방향 만으로 충분할때 사용



map

- 많은 데이터를 정렬할때 사용

- 늘 정렬된 상태로 유지되는 컨테이너

- 많은 자료를 저장하고 검색이 빨라야할때 사용

- 적은양에는 오버헤드로 인해 손해

- 빈번하게 삽입 / 삭제 할때는 불리
- 중복키 사용 불가

- 정렬이 필요하지 않은곳에서 사용할때는 불리

- find() 알고리즘이 아닌 find() 멤버함수를 사용해야한다

- 삽입시 insert()를 애용할것

- 연관 배열이 필요할경우 사용

- 중복키를 허용할때는 multimap을 사용한다

- 정렬되지 않은 map을 사용할때는 unorderd_map을 사용한다



set

- map과 다르게 key만 저장

- 많은 데이터를 정렬할때 사용

- key가 있는지 없는지 알아야 할때 사용

- 많은 데이터를 저장하고 검색이 빨라야할때 사용

- 중복키 사용 불가

- 늘 정렬된 상태로 유지되는 컨테이너

- 적은양에는 오버헤드로 인해 손해

- 검색이 자주 발생될경우 사용

- 중복키를 허용할때는 multiset을 사용한다

- 정렬되지 않은 set을 사용할때는 unorderd_set을 사용한다



hash_map

- 많은 데이터를 저장할때 유리

- 검색속도가 빠름

- 빈번한 삽입 / 삭제에 불리

- 중복키 사용 불가

- map 과 set보다 검색속도가 빠름(최대장점)

- 적은 데이터를 저장하고 검색할때는 list나 vector가 빠를수 있음


'C&C++' 카테고리의 다른 글

Incremental Linking  (0) 2019.07.10
WINAPI VirtualAlloc and VirtualAllocEx Difference  (0) 2019.07.09
Modern C++  (1) 2018.10.02
WINAPI How To Load Windows Tray Icon  (0) 2018.09.08
How To Use Json In C++(jsoncpp)  (0) 2018.09.08

10여년 동안 업데이트가 없던 C++98(C++03) 버전이후 

C++11 => 최신 유행에 맞춰 제정된 새로운 C++표준

C++14 => 11버전에서 추가된 새 기능들을 가꿈

C++17 => 프로그래밍 언어상의 새로운 기능에 주력


현대 추세에 맞춰 표준화된 C++을 Modern C++이라고 한다.

우리가 알던 구 버전의 C++98의 기능이 아닌 코드를 짧고 간결하고 가독성까지 높여주는 새로운 Modern C++의 주요기능에 대해 알아본다.


1. auto 키워드

2. 범위기반 for문

3. 유니폼 초기화

4. decltype 키워드

5. 스마트 포인터

6. 람다표현식(lambda)

7. R-Value Reference(우측값 참조)

8. Move Semantics(이동 시멘틱)

9. Perfect Forwarding(완벽한 전달)

10. std::array

11. nullptr 키워드

12. constexpr 키워드

13. static_assert 키워드

14. 스레드 라이브러리

15. 정규식 라이브러리



1.  auto 키워드

auto를 사용하면 변수를 선언할 때 특정 자료형을 지정하지 않을 수 있다.

어차피 변수의 초기화 값을 보면 자료형을 알 수 있기때문에 굳이 타입을 명시하지 않도록 해주는것이다.

따라서 초기화 값 없이 사용하면 에러가 발생한다.

또한 지역변수 외에 전역변수 및 함수의 매개변수로 사용하면 안된다.


auto는 코드가 짧아지고 가독성이 좋아지는데 특히 STL에서 유용하게 쓰인다.

STL은 사용은 편리하지만 소스 코드의 가독성이 많이 떨어지는데 auto를 통해 가독성 문제도 해결된다.


auto Int = 100;
auto Long = 100L;
auto Double = 100.0;
auto String = "string";
auto Temp; //ERROR!
cs


참조자도 사용가능하다.

auto a=10;
auto& b=a;
cs


포인터와 함수포인터로도 사용가능하다.

auto* Func = printf;
auto String = "Hello World";
Func("%s\n",String);
cs


auto 키워드는 STL의 iterator형을 대신할수도 있어서 매우 편리하다.

vector<int> v;
auto Begin = v.begin();
cs


자료형 뿐만 아니라 구조체 및 클래스도 지정해준다.

class A{
    public:
        int a;
        char* b;
};
auto s = A();
cs

※ 반환형으로도 사용 가능하지만 decltype 키워드와 함께 후에 설명한다.



2. 범위기반 for문

STL의 iterator는 유용하지만 사용하기 매우 귀찮다.


for(vector<int>::iterator i = v.begin(); i != v.end(); ++i);
cs


위 loop를 auto를 사용하면 아래와 같이 사용할수 있다.


for(auto i = v.begin(); i != v.end(); ++i);

cs


위에서 설명한대로 단지 auto 키워드를 사용해 iterator형을 대신하였다.
하지만 Modern C++에서는 Python처럼 범위기반 for문을 지원한다.

for(auto& i : v);
cs

코드의 길이가 확연하게 줄어들었다.
v는 배열이 되도 좋고 컨테이너가 되도 좋다.
i를 참조변수로 사용하면 좋은점이 복사가 발생하지 않기때문에 성능이 좋아진다.
또한 참조변수로 사용하지 않으면 값을 아무리 바꾼다고해도 v안의 값을 변경할 수 없지만
참조변수로 사용하면 v에 직접 접근이 가능해진다.


int s[20= {0,};
for(auto i : s);
cs


이런식으로 반복의 목적을 위해 배열을 사용해도 된다.



3. 유니폼 초기화

유니폼 초기화는 C++0x에서 원하는 초기화가 지원되지 않는 경우가 있었기 때문에 

다양한 초기화 구문을 지원하고 이전에 다룰수 없었던 초기화까지 간단한 코드만으로 구현하게 해주는 기능이다.

같은 방법으로 모든종류의 객체를 일관성있게 리스트형 초기화(std::initializer_lists)를 통해 다양한 방식으로 초기화할수있다.


모든종류의 객체 초기화.

typedef struct Car{
int cost;
int oil;
}Car;
class Book{
private:
string name;
int cost;
public:
Book(const string& name, int cost) : name(name), cost(cost) {}
};
int main(){
int v{5};
int v[]{1,2,3};
vector<int> v {1,2,3};
vector<pair<int,string>> v{{1,"a"},{2,"b"}};
Car car{50,20};
vector<Book> v{{"a",100},{"b",200}};
}


이름없는 임시변수 초기화.

vector<int> {1,2,3};


동적할당시.

int* v = new int[3] {1,2,3};


함수의 리턴값으로 사용.

vector<int> func(){
return {1,2,3};
}
int main(){
vector<int> v = func();
}


함수의 인자로 사용.

void func(vector<int> v){
for(auto i : v)cout<<i<<endl;
}
int main(){
func({1,2,3});
}


※ 유니폼 초기화시 auto가 해주는 타입추론은 문제가 있다.

auto t[]{1,2,3,4,5};//ERROR !
cs

유니폼 초기화를 할때 사용되는 이 리스트형 초기화법은 std::initializer_lists라는 템플릿을 통해 초기화를 해주는데 

auto를 사용하면 std::initializer_lists 템플릿으로 타입추론을 해버린다.

따라서, 유니폼 초기화시 auto를 사용하면 타입추론이 적절히 이루어지지 않기때문에 에러가 발생한다.



4. decltype 키워드

decltype 키워드는 주어진 표현식의 타입을 알려주는 키워드이다.

auto 와는 다른점은 auto는 값에 상응하는 타입을 추론하는 반면,

decltype은 값으로부터 타입을 추출한다.


사용법은 아래와같다.

int main(){
    auto a = 1;
    auto b = 2.5;
    decltype(a+b) c = 3;//int와 double의 합의 자료형을 decltype이 결정해준다.
}
cs


decltype 키워드는 타입을 명시해주는곳은 어디든 사용 가능하나 위 경우에는 auto 키워드를 사용하는것이 훨씬 편해보인다.

decltype은 타입지정상황에서는 효용가치가 없고, 주로 템플릿 함수의 반환타입 결정에 사용된다.


Modern C++에서는 auto 타입 반환이 가능해지고 후행 반환 형식으로 decltype을 사용함으로써, 템플릿 함수의 auto 반환을 상당히 편하게 해준다.

후행반환형식이란 함수의 반환 형식을 기존의 리턴타입의 위치가 아니라 매개변수 목록 다음에 [->] 연산자를 사용해 선언하겠다는 방식이다.


C++11에서 리턴형을 auto로 하려면 반드시 후행 반환 형식으로 decltype을 사용해야한다. 

특히 템플릿 함수들의 경우 타입을 템플릿 인자들로부터 추론해야 하므로 decltype을 활용하지 않으면, 

컴파일 단계에서 auto 반환 형식을 재대로 추론하지 못해 컴파일 에러가 발생하게 된다.


C++14부터는 리턴형을 auto로 할때 후행 반환 형식으로 decltype을 사용하지 않아도 반환 타입을 추론해 준다.

이때, 리턴값으로 리턴형을 유추한다.


auto func1(int a, int b) -> decltype(a+b){
    return a+b;
}
 
template<typename A, typename B>
auto func2(A a, B b) -> decltype(a + b){
    return a + b;
}
 
template<typename A, typename B>
auto func3(A a, B b){// -> decltype(a + b){ => C++14 부터 생략가능.
    return a + b;
}
cs


※리턴값에 decltype을 사용해주는 방법도 있을수 있다고 생각했는데 후행반환형식을 사용하는 이유는

decltype에 들어가는 표현식의 자료형이 아직 정해지지 않아서 그렇다.

 

decltype(a+b) func(int a, int b){return a+b;}
//=>컴파일러 입장에서 decltype만 보면 a+b의 타입을 추론할수 없다.
cs


C++14부터는 auto 반환시 후행 반환 형식을 사용하지 않아도 된다.


하지만, 개발자가 직관적으로 의도한것과는 다르게 반환 형식이 결정될 수 있는 문제가 있다.

리턴형을 auto로 사용하면 auto의 특성상 템플릿 타입추론으로 리턴형이 결정되는데 이 과정이 리턴형을 의도와 다르게 결정할수 있다.


그래서 auto와 decltype을 혼용한 decltype(auto)를 사용하는것이 좋다고한다.
이렇게 되면 auto의 타입추론 방식이 템플릿이 아닌 decltype을 따라서 후행 반환 형식과 같은 형태가된다.



5. 스마트 포인터

C++에서 동적으로 할당받은 메모리는 반드시 delete 키워드를 이용해 메모리 해제를 해주어야한다.

이에 대해서 좀더 편리하고 메모리 릭으로부터 안전성을 보장하기위해 Modern C++에서는 스마트포인터를 제공한다.

스마트포인터는 사용이 끝난 메모리를 소멸자가 자동으로 delete 해주는 클래스 탬플릿이다.

또한 스마트포인터는 댕글링포인터로인한 메모리 릭도 방지해준다.

그리고 스마트포인터는 디폴트 생성자가 자동으로 NULL로 초기화 해주기 때문에 NULL로 초기화할 필요가 없다.

※ 메모리 릭 : 프로그램이 동적할당 후, 메모리를 해제하지 않아서 시스템의 메모리를 고갈시키는 오류.

※ 댕글링 포인터 : 해제된 메모리를 참조하거나, 스택에서 사라진 메모리를 참조하는 포인터


사실 Modern C++이전에도 auto_ptr(C++11부터 삭제)이라는 스마트포인터를 지원했다.

그러나 배열을 지원하지 않고 얕은 복사의 문제점 때문에 Modern C++에서는

unique_ptr, shared_ptr, weak_ptr을 지원한다.

스마트 포인터는 <memory>헤더에 정의되어있다. 


int main() {
    auto_ptr<int> ptr1(new int(1));
    auto_ptr<int> ptr2 = ptr1; // 이때 ptr1은 null_ptr가 되어버린다.
}
cs


unique_ptr

- 하나의 스마트포인터만 특정 객체를 소유하도록 객체에 소유권(오직하나)을 도입한 스마트포인터이다.

- 해당 객체의 소유권을 가지고 있을때만 소멸자가 해당 객체를 해제한다.

- 복사 생성자와 복사 대입 연산자가 구현되어 있지않다.

- 복사가 불가능하고 소유권 이전만 가능하다.

- 소유권이 이전되면 기존의 스마트포인터는 nullptr가 된다.

- 이동은 move 함수로 가능하다.

- 포인터는 get 멤버함수로 얻을수 있고, 메모리 해제는 reset 멤버함수로 그 기능을 한다.


int main(){
    unique_ptr<int> ptr(new int(1));
    cout<<*ptr<<endl;//1
    auto ptr2 = ptr.get();//소유권 이전이 아닌 순수 포인터 얻기.
    *ptr2 = 2;
    cout<<*ptr2<<endl;//2
    cout<<*ptr<<endl;//2
    auto ptr3 = move(ptr);//소유권 이동.
    ptr.reset();//이미 소유권 이동이 발생해서 아무일도 발생하지 않는다.
    //cout<<*ptr<<endl;//Error!
    cout<<*ptr3<<endl;//2
    *ptr3 = 3;
    cout<<*ptr3<<endl;//3
    cout<<*ptr2<<endl;//3
}
cs


※ move 함수는 객체를 단지 R-Value로 캐스팅 해줄뿐인데 소유권 이동이 되는 이유는 unique_ptr 클래스에 이동 시멘틱이 구현되어있기 때문이다.


C++14부터는 make_unique 함수가 제공된다.

이 함수는 전달받은 인자를 통해 지정된 타입의 객체를 생성하고 생성된 객체를 가리키는 unique_ptr을 반환한다.

이 함수를 사용하면 예외 발생에 대해 안전하게 대처할수 있다.

 

int main(){
    unique_ptr<int> ptr = make_unique<int>(3);
    cout<<*ptr<<endl;
}
cs


아래는 배열을 unique_ptr을 통해 표현한 예시이다.

int main(){
    unique_ptr<int[]> ptr(new int[3]{1,2,3});
    for(int i=0;i<3;i++)cout<<ptr[i]<<endl;
    
    unique_ptr<int[]> ptr2 = make_unique<int[]>(3);
    for(int i=0;i<3;)ptr2[i]=++i;
    for(int i=0;i<3;i++)cout<<ptr2[i]<<endl;
}
cs

shared_ptr

- 하나의 특정객체에 대해 참조횟수(reference count)를 통해 참조하는 스마트포인터가 총 몇개인지를 참조한다.

- 참조횟수는 특정 객체에 대해 shared_ptr이 추가될때마다 1씩 증가, 해제할때마다 1씩 감소한다.

- 참조횟수가 0이 되면 delete 키워드를 통해 메모리를 자동 해제한다.

- unique_ptr와 마찬가지로 make_shared함수를 통해 shared_ptr을 안전하게 생성할수 있다.(C++14 이상)

- unique_ptr와 다르게 복사도 마음껏 할수있다.

- use_count 함수를 통해 참조횟수(해당 객체를 참조하고 있는 shared_ptr의 수)를 알수있다.

- 참조횟수를 건드리기 때문에 shared_ptr 객체가 복사가 되어도 메모리 공간은 늘어나지 않는다.

- move 함수를 사용하면 기존의 포인터를 제거하고 그대로 이전하기 때문에 참조횟수가 변하지 않는다.


int main(){
    shared_ptr<int> ptr = make_shared<int>(1);
    cout<<ptr.use_count()<<endl;//1
    auto ptr2 = ptr;
    cout<<ptr.use_count()<<endl;//2
    auto ptr3(ptr);
    cout<<ptr.use_count()<<endl;//3
    auto ptr4 = move(ptr2);//기존의 포인터를 제거하고 그대로 이전하기 때문에 참조횟수가 변하지 않는다.
    cout<<ptr.use_count()<<endl;//3
    ptr4.reset();
    ptr3.reset();
    ptr.reset();
    cout<<ptr.use_count()<<endl;//0
}
cs


배열형태로 shared_ptr을 사용하고자 할때는 자동으로 정의되는 unique_ptr과 다르게 해제방법을 정의해줘야한다.

int main(){
    shared_ptr<int> ptr(new int[3]{1,2,3},default_delete<int[]>());
    for(int i=0;i<3;i++)cout<<*(ptr.get()+i)<<endl;
    
    shared_ptr<int> ptr2( new int[10]{1,2,3,4,5}, []( int *p ) { delete[] p; } );//람다식을 통해 정의.
    for(int i=0;i<3;i++)cout<<ptr2.get()[i]<<endl;
}
cs

weak_ptr

- shared_ptr은 서로 상대방을 가리키는 스마트포인터를 가지고 있다면 참조횟수는 절대 0이 되지 못하므로 메모리가 영원히 해제되지 않는다.(순환참조)

- weak_ptr은 이 순환참조를 제거하기위해 사용한다.

- weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는다.

- weak_ptr이 가리키는 메모리 공간은 shared_ptr의 참조횟수에 카운트되지 않는다.


int main(){
    shared_ptr<int> ptr = make_shared<int>(1);
    weak_ptr<int> ptr2 = ptr;
    cout<<ptr.use_count()<<endl;//weak_ptr을 사용해서 shared_ptr의 참조횟수는 증가하지 않는다. 즉 1이 출력된다.
}
cs



6. 람다표현식(lambda)

Modern C++에서 새롭게 등장한 람다표현식은 함수를 별도의 선언없이 인라인으로 사용하게 해준다.

기존의 boost 라이브러리에서 람다를 지원했지만 C++11부터 표준으로 지정된 이상 굳이 boost 라이브러리를 사용할 이유가없다.

람다 함수는 이름 없는 함수(Anonymous function)로도 불리는데 python, c#, javascript등의 언어에서 편리하게 사용되는 기능이다.


람다표현식의 장점으로는 개발자 입장에서 코딩이 간편해지고 코드의 가독성이 향상된다.

한번 사용하고말 함수는 코드 전체를 볼때 가독성이 떨어지게 할수있다.

이때 해당 scope 내에서 람다함수를 사용해 사용하면 편리할뿐더러 코드의 가독성 또한 향상된다.

또한 STL의 알고리즘을 더 간편하게 사용하게 해준다.

무엇보다도 수십줄의 코드를 단 몇줄로 줄여주는 효과를 준다.

개발자 입장에서 코드를 줄여주는것 만으로도 얼마나 좋은 기능인지 알려주는 부분이다.


기본 문법

[captures](parameters)mutable -> return type { body } (execute);


captures{

람다 식의 본문이 바깥쪽 범위의 변수에 액세스할 변수들을 캡쳐한다.

변수명, =(call by value), &(call by reference)가 들어간다.

비워두면 아무것도 사용하지 않는다는 뜻이다.

ex1) [=] =>모든 변수를 call by value로 캡쳐.

ex2) [&] => 모든 변수를 call by reference로 캡쳐.

ex3) [a,&b] => a는 call by value, b는 call by reference로 캡쳐.

ex4) [=,&a,&b] =>a,b는 call by reference, 나머지 변수들을 call by value로 캡쳐.

ex5) [this] => 현재 객체를 call by reference로 캡쳐.


call by value로 캡쳐된 변수들은 lambda body에서 변수가 새로 만들어지고 const 키워드가 붙는다.

그러므로 lambda body에서 변수의 수정이 불가능해진다.(mutable 키워드를통해 해결가능하다.)

그러나 캡쳐되는 변수가 포인터 변수라면 [=]를 통해 캡쳐하더라도 값의 변경이 가능해진다.


전역변수는 캡쳐할수 없다.

전역변수를 캡쳐하려면 [&] or [=]를 이용해야한다.


C++14부터는 [a=1+2]와 같은 초기화 캡쳐 구문이 생겼다.

변수명만 붙이는 일반 캡쳐와 다르게 대부분의 표현식을 다 넣을수 있다.

}

parameters{

기본 함수선언과 마찬가지로 인자값이 들어간다.

C++14부터는 인자를 선언할 때 auto 키워드를 통해 선언할 수 있어서 더욱더 편리해졌다.

인자가 없을경우 생략이 가능하다.

ex) []{cout<<"hello world"<<endl;};

}

mutable{

mutable 키워드를 사용하면 call by value로 캡쳐된 변수들이 lambda body에서 새로 만들어질때 const키워드가 붙지 않는다.

그러나 [=]는 call by value이기 때문에  lambda body 내부에서만 일시적으로 값을 변경할수 있을뿐이다.

}

return type{

리턴형을 명시해줄때는 후행 반환 형식을 사용한다.

또한 리턴형은 생략이 가능하며 추론이 가능하다.

리턴이 한번만 나타나거나 없는경우 자동타입 추론(C++11)

lambda body 내의 모든 반환형이 동일한경우 자동타입 추론(C++14)

}

body{

기존의 함수와 마찬가지로 함수의 내용이 들어간다.

}

execute{

lambda식 선언 즉시, 함수를 실행할 경우 () 연산자를 통해 함수를 실행할 수 있다.

}


기본 사용법

int main(){
    auto a = 5;
    [&](){a = 3;cout<<a<<endl;}();
}
cs


함수포인터로 사용하기(auto 키워드 이용)

int main(){
    auto a = 5;
    auto func = [&](){a = 3;cout<<a<<endl;};
    func();
}

cs


함수포인터로 사용하기(std::function 이용)

int main(){
    auto a = 5;
    function<void()> func = [&](){a = 3;cout<<a<<endl;};
    func();
}
cs


람다를 반환하는 함수

auto a = 5;
auto getLambda() {//C++14
    return [&]() {a = 3;cout<<a<<endl;};
}
int main(){
    auto func = getLambda();
    func();
}
cs

람다 속 람다

int main(){
    auto a = 5;
    auto func = [&](){
        return [&](){a = 3;return a;}();
    };
    cout<<func()<<endl;
}
cs

클래스의 멤버함수에서 쓰이는 람다

class Person {
    private:
        string name;
    public:
        Person(string name) : name(name) {}
        void show() {
            [this]() { cout << "My name is " << name << endl; }();
        }
};
int main() {
    Person p("Lee");
    p.show();
}
cs

클래스 멤버함수에서 람다식을 정의할때 [this]로 현재 객체를 참조할수 있다.

이때 람다는 friend 함수이므로 private 멤버에도 접근 가능하다.


재귀함수로 쓰이는 람다

int main(){
    function<int (int)> func = [&func] (int num) -> int
    {
        if(num <= 1)return 1;
        else return num*func(num-1);
    }; 
    auto a = func(5);
    cout<<a<<endl;
}
cs

보통 람다를 함수포인터로 사용할때 auto 키워드를 사용한다.

그러나 재귀함수로 사용할때는 std::function으로 사용해야 한다.

auto 키워드는 타입을 추론하는 키워드이다. 

그러나 타입이 추론되기 전에 재귀 함수를 사용하면 함수가 제대로 동작할수 없다.

그러므로 재귀함수로 사용할때는 auto 키워드를 사용하면 안된다.



7. R-Value Reference(우측값 참조)

C++03 부터 좌측값(L-Value), 우측값(R-Value)이라는 개념이 생겼다.


좌측값이란 표현식에서 좌측에 있는값이다.

표현식이 종료된 이후에도 없어지지않고 지속되는 개체(변수)라고 할수있다.

변수는 표현식의 좌측과 우측에 모두 나올수 있기때문에 사실상 좌측값은 표현식의 좌측과 우측에 모두 나올수 있다.


int a = 3;
cs

위의 표현식에서 a는 좌측값이라고 할수있다.


우측값은 표현식이 종료되면 더이상 존재하지 않는 임시적인 개체(상수)이다. 좌측값이 아닌값을 우측값이라고 할수있다.

위의 표현식에서 3이 우측값이다. 우측값은 상수이기 때문에 표현식의 우측에만 나올수있다.

이해하기 쉽도록 상수라고 표현했지만 사실 상수는 우측값이지만 우측값은 상수가 아니다. 

상수는 우측값중 하나이고 정확한 표현은 이름없는 임시개체가 맞다.


int main() {
    int a = 5;
    int b = a;
    int c = a++;
    int d = ++a;
    int& e = b;
    int f = + c;
    int* g = &a;
}
cs

위의 코드에서 좌측값과 우측값을 구별해 본다면 밑줄친 값이 우측값, 나머지를 좌측값이라고 할수있다.

그래도 구별하기 힘들다면 주소값을 가져오는 연산자 &를 붙여서 에러가 난다면 우측값이다.


R-Value의 개념이 도입되기 이전에는 참조라고 하면 당연히 좌측값 참조를 의미했다. (상수는 참조가 불가능하기때문)

그러나 R-Value의 개념 도입후 참조라고하면 좌측값 참조와 우측값 참조로 구분을 해줘야햔다.

C++11에서 도입된 우측값 참조는 말그대로 이름없는 임시객체(상수)를 참조한다.


일반참조(좌측값 참조)는 &하나만 사용한다면 우측값 참조의 사용법은 &&을 사용한다.

int&& a = 5;
cs


우측값 참조변수는 일반참조와 마찬가지로 초기화 없이 사용할수 없으며 

반드시 우측값으로 초기화 해줘야한다.(좌측값 참조는 당연히 좌측값으로 초기화)

    int tmp = 3;
    
    int& Lv = tmp;
    int& Lv = 3;//Error!!
 
    int&& Rv = 3;
    int&& Rv = tmp;//Error!!
cs


그렇다면 우측값 참조변수는 좌측값일까 우측값일까?

int&& a = 5;
int b = a;
cs

위 코드가 가능하므로 우측값 참조변수 a는 좌측값이다.


R-Value Reference는 C++11에서 새롭게 도입된 이동 시멘틱(move semantics)과 퍼펙트 포워딩(perfect forwarding) 적용을 위해 도입된 개념이라고 봐도 무방하다.



8. Move Semantics(이동 시멘틱)

Move Semantics (이동 시멘틱)이란 C++11에서 추가된 문법으로 객체의 메모리 소유권을 이전하는 방식의 문법을 말한다.

기존의 C++에서 사용된 Copy semantics (복사 시멘틱)은 때때로 불필요한 복사때문에 보이지 않는 성능 저하의 원인이 되었다.

데이터 복제 또는 대입이 끝난후 데이터 소멸시 Move Semantics를 사용한다면 얕은복사를 해도 메모리의 소유권 이전이기 때문에 비용이 발생하지 않는다.


Class = B;


위처럼 선언을 하면 디폴트 복사생성자를 컴파일러가 생성해준다.

그러나 디폴트 복사생성자를 사용하면 얕은복사의 문제가 발생한다.

얕은복사의 문제점은 서로 다른 객체의 멤버변수들이 같은 주소를 가리키게 되어 댕글링포인터로 인한메모리릭의 문제가 발생한다.

그러므로 깊은복사의 기능을 하는 복사생성자를 구현해주어야한다.

보통은 별도의 객체를 생성해 복사해주는 방식이다.


하지만 객체 Swapping의 예시에서는 불필요한 복사연산이 매우 많이 발생하게된다.

integer처럼 작은 단위의 객체를 swap 할때는 미미하겠지만

크기가 10만인 vector 또는 대용량 객체를 swap하는 예제라면 단지 swap할 뿐인데 쓸모없는 메모리 낭비, 비용낭비 문제가 발생할것이다.


여기서 Move Semantics의 필요성이 생긴다. (객체 Swapping 예제는 Move Semantics를 설명하기 가장쉬운 예제이다.)

단순히 R-Value(임시객체)의 이동만 필요할 뿐인데 비효율적인 메모리 사용문제가 있다.

어차피 R-Value(임시객체)는 소멸할건데 새로 메모리에 할당후 복사시키지 말고 사라질 값을 복사시 사용하고 소멸키기자는 취지이다.


이부분에서 우측값을 인자로 받는 복사생성자(이동생성자)를 구현해주고 Operator 오버로딩 또한 R-Value에 맞는 오버로딩(이동 대입 연산자)을 구현해준다.



#include <iostream>
#include <cstring>
using namespace std;
class String{
public:
    char *str;
    int len;
    int capacity;
    String(){
        cout << "[+] call constructor ! " << endl;
        len = 0;
        capacity = 0;
        str = NULL;
    }
    String(char *s){
        cout << "[+] call constructor ! " << endl;
        len = strlen(s);
        capacity = len;
        str = new char[len];
        for (int i = 0; i != len; i++)
            str[i] = s[i];
    }
    String& operator=(String &s){
        cout << "[+] copy!" << endl;
        if (s.len > capacity){
            delete[] str;
            str = new char[s.len];
            capacity = s.len;
        }
        len = s.len;
        for (int i = 0; i != len; i++)
            str[i] = s.str[i];
        return *this;
    }
    String& operator=(String &&s){
        cout << "[+] move!" << endl;
        str = s.str;
        capacity = s.capacity;
        len = s.len;

        s.str = nullptr;
        s.capacity = 0;
        s.len = 0;

        return *this;
    }
    String(String &s){// 복사 생성자
        cout << "[+] call copy constructor ! " << endl;
        len = s.len;
        str = new char[len];
        for (int i = 0; i != len; i++)
            str[i] = s.str[i];
    }
    String(String &&s){// 이동 생성자
        cout << "[+] call move constructor !" << endl;
        len = s.len;
        str = s.str;
        capacity = s.capacity;

        s.str = nullptr;
        s.len = 0;
        s.capacity = 0;
    }
    ~String()
    {
        if (str)delete[] str;
    }
    int length(){
        return len;
    }
    void print(){
        for (int i = 0; i != len; i++)
            cout << str[i];
        cout << endl;
    }
};
template <typename T>
void swap(T &a, T &b){
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
    /*T tmp(a);
    a = b;
    b = tmp;*/
}
int main(){
    String str1("CAT");
    String str2("DOG");
    cout << "====== before Swap ======" << endl;
    cout << "[=] str1 : ";str1.print();
    cout << "[=] str2 : ";str2.print();
    cout << "====== after Swap ======" << endl;
    swap(str1, str2);
    cout << "[=] str1 : ";str1.print();
    cout << "[=] str2 : ";str2.print();
    cout<<str1.str<<endl;
}


위 예제의 swap 함수에서 주석처리된 부분으로 swap 함수를 구현했다면 Copy Semantics에 의해 불필요한 복사가 발생했을것이다.

그러나 R-Value를 통해 들어온 인자값 덕분에 이동 생성자와 이동 대입 연산자를 추가해주었다.

위 예제에서 가장 중요한 부분은 R-Value(임시객체)이다. 어차피 소멸될 객체를 복사하고 기존의 메모리를 해제시켜주는 방법이다.

만약 인자로 들어온 R-Value를 초기화 해주지 않는다면 함수 종료시 얕은 복사가 된것이 되버려서 R-Value가 소멸할때 이동된 메모리 주소를 참조하여 소멸시킬것이다.


swap 함수에서 move함수를 통해 대입을 시켜준다. 

여기서 move 함수는 L-Value 또는 R-Value를 R-Value로 캐스팅 해줄뿐, 그 어떠한 이동연산도 해주지 않는다.

즉 move 함수로는 메모리 이동이 일어나지 않고 move 함수로 R-Value로 캐스팅된 객체를 지속적으로 사용하는것이다.

move함수로 이동연산이 모두 구현된다면 굳이 위 예제에서 이동 생성자와 이동 대입 연산자를 정의해주지도 않았을것이다.

또한 이동연산을 수행한 객체는(이동연산의 목적으로 구현했다면) 빈 상태이므로 재사용해서는 절대 안된다.


Copy Semantics(복사 시멘틱)과 Move Semantics(이동 시멘틱)의 가장 큰 차이점으로는

Copy Semantics는 깊은복사를 통해 원본과 똑같은 객체를 생성해 복사하는것으로 메모리 낭비가 심하고,

Move Semantics는 메모리 소유권 이전으로 얕은 복사를 하고 원본을 NULL로 초기화하는것이다.


가장 큰 장점은 보이지 않는 숨은 비용에 대한 성능 향상이겠다.

※ STL에도 Move Semantics 기능이 추가가 되어 R-Value를 받을수 있고, 같은코드여도 Modern C++ 이전의 컴파일러냐 이후의 컴파일러냐에 따라 성능이 좌우된다.



9. Perfect Forwarding(완벽한 전달)

#include <iostream>
using namespace std;
class String{
public:
     string str;
String();
String(string str);
};
template <typename T>
void print(T& s){
cout<<s.str<<endl;
}
int main(){
String str1("test");
print(str1);
print(String("test2"));//error!
}
String::String(){
this->str = "";
}
String::String(string str){
this->str = str;
}


위 코드에서 에러가 발생하는 이유를 컴파일러는 이렇게 설명한다.


error: invalid initialization of non-const reference of type 'String&' from an rvalue of type 'String'


쉽게 말해 L-Value Reference를 인자로 받는 함수에 R-Value를 넘기니까 발생하는 에러이다.

위 문제를 해결하기 위해선 함수 오버로딩을 할수도 있겠지만 인자의 수가 n개일때 최대 2^n개 만큼의 함수가 필요하다.

또한 인자의 수가 가변적이라면 답이 안나온다.


이 문제를 해결하기 위해 Universal Reference 라는 개념이 나온다. 이제 내가 아는 참조의 개념은 3개이다.

Forwarding Reference라고도 하는데 예제를 보면 쉽게 이해할수 있다.


//R-Value Reference
Class&& c = Class();

//Universal Reference
auto&& a = b;

//R-Value Reference
void func(Class&& c);

//R-Value Reference
template<typename T>
vector<T> func (vector<T>&& v);

//Universal Reference
template<typename T>
void func(T&& t);


예시를 보면 타입추론의 경우에 Universal Reference로 결정되는것을 볼수있다.

4번째 예시의 경우도 타입추론이 아닌가라고 생각할수 있는데 이때는 함수의 인자인 v의 타입을 추론하는게 아니라 벡터의 자료형을 추론하기때문에 R-Value Reference이다.


Universal Reference는 초기화시 넘어오는 인자에따라 좌측값이면 좌측값 참조로 우측값이면 우측값 참조로 결정해준다.

다시말해 좌측값 참조가 될수도 있고 우측값 참조가 될수도 있다는 말이다.


이 Universal Reference의 개념을 통해 첫번째 예제의 문제를 해결할수 있다.


template <typename T>
void print(T&& s){
cout<<s.str<<endl;
}


※ 다시말하지만 타입추론의 경우에만 Universal Reference로 동작한다고 했으므로 그 경우가 아니면 R-Value Reference로 동작한다. 막쓰면 안된다.


C++ Reference 규칙

1. non const lvalue reference는 lvalue만 참조 가능
2. const lvalue reference는 lvalue / rvalue 둘 다 참조 가능
3. rvalue reference는 rvalue만 참조가능 (C++11부터 사용 가능)

위 예제는 2번 규칙에 의해 아래 함수로도 대체가 가능하다.

template<typename T>
void print(const T& s){
cout<<s.str<<endl;
}



위의 예제를 통해 포워딩 문제를 해결한것처럼 보여도 다음예제를 보면 그렇지 않다는것을 알수있다.


void func(int& i){
cout<<"This is L-Value Reference"<<endl;
}
void func(int&& i){
cout<<"This is R-Value Reference"<<endl;
}
template<typename T>
void proc(T&& t){
func(t);
}
int main(){
int a = 1;
proc(a);
proc(1);
}


//실행 결과
This is L-Value Reference
This is L-Value Reference


인자가 L-Value냐 R-Value냐에 따라서 달라지는 함수를 오버로딩을 통해 구현했을때 원하는대로 동작하지 않는경우가 있다.

위 예제는 proc() 함수에서 인자 t가 L-Value로 들어오든 R-Value로 들어오든 L-Value로 인식한다.


여기서 Perfect Forwarding 이라는 개념이 등장한다.

Move semantics에서는 std::move()함수를 통해 구현했지만 Perfect forwarding은 std::forward() 함수를 통해 구현한다.

std::forward()함수도 std::move() 함수와 마찬가지로 Perfect forwarding자체를 구현해주는게 아니라 조건부로 우측값으로 캐스팅하는 함수일뿐이다.

우측값으로 초기화된 참조변수라면 우측값 참조변수로, 그렇지 않다면 좌측값 참조변수로 캐스팅을 해준다.


template<typename T>
void proc(T&& t){
func(forward<T>(t));
}


//실행 결과
This is L-Value Reference
This is R-Value Reference


std::forward() 함수는 인자로 넘어온 값이 R-Value로 초기화 되었는지 판단하기 위해 template의 타입인자 T를 사용한다.

이 타입안에 인자로 넘어온 값을 통해 R-Value인지 아닌지를 판단해준다.


위 예제를 통해 인자의 완벽한 전달이 이루어졌다고 말할수 있다.




Reference Collapse


Universal Reference는 좌측값 참조인지 우측값 참조인지를 Reference Collapse를 통해 결정한다.


class String{
public:
string str;
String();
String(string str);
};
template <typename T>
void print(T&& s){
cout<<s.str<<endl;
}
int main(){
String str1("test");
print(str1);//T => String&
print(String("test2"));//T => String
}


위 예제의 메인함수에서 2번의 함수호출을 당시 아래와 나타낼수 있다.


print(String& && s);
print(String&& s);


지금까지 나온 개념으로는 참조에 대한 참조라는 개념은 없기때문에 첫번째 함수호출은 허용되지 말아야한다.

첫번째 함수호출처럼 프로그래머가 함수를 정의한다면 컴파일시 에러가 발생하지만 Universal Reference에서는 허용된다.

지금까지 설명한 예제를 보면 


print(String& && s); => print(String& s);


이렇게 된다는것을 볼수있었다.


#define REF auto&
int main(){
auto func = [](REF& i){
cout<<i<<endl;
}
}


매크로들 통해 참조에 대한 참조가 발생하는 경우도 있을수 있다.


이러한 특별한 케이스에 컴파일러는 참조에 대한 참조가 발생했을때 Reference Collapse에 의해 참조로 해석한다.

4가지의 케이스에대해 규칙을 정해놓았는데 아래와 같다.


T& & t => T& t
T& && t => T& t
T&& & t => T& t
T&& && t => T&& t


마치 bool 대수에서 and 연산처럼 보인다.

이러한 규칙을 통해 참조에 대한 참조를 해결할수 있고 Universal Reference 또한 이 규칙을 통해 좌우측값을 결정한다.



10. std::array

Modern C++에서는 C/C++의 기본문법인 배열을 STL로 지원해준다.


기존에는 STL을 사용할때 주로 vector를 사용했다.

그러나 vector는 생성과 소멸에 드는 비용이 꽤 크고(사용해야하는 vector가 많을수록 커진다), 

vector 객체는 32bit를 차지하므로 메모리를 비효율적으로 사용할수 있다.


std::array는 이러한 문제없이 사용할수있다.

vector을 동적배열로 생각한다면 array는 정적배열(크기를 고정하고 사용)이라고 생각할수 있다.

크기가 고정된다는것만 제외하면 vector의 장점을 그래도 취하며 메모리에 순차적으로 저장되어 계산속도가 높아진다.

vector는 동적배열이므로 메모리영역을 힙영역을 사용하는반면, array는 정적배열이므로 스택영역을 사용한다.


#include <iostream>
#include <array>//std::array를 사용하기위한 헤더
int main() {
    std::array<int5> arr {123};//배열의 크기보다 작게 초기화하면 나머지는 0으로 초기화 된다.
    for(auto i : arr)std::cout<<i<<std::endl;
}
cs


int main() {
    array<int5> arr {123};
    auto p = arr.data();//data함수는 첫번째 원소의 주소를 반환하는데 이를통해 포인터를 사용할수있다.
    for(int i=0;i<arr.size();i++)cout<<*(p+i)<<endl;
}
cs


=>기존 배열과 비교시 좋은성능, 컨테이너 멤버함수사용 등 Modern C++에서 배열을 사용한다면 std::array를 사용하는게 좋겠다.



11. nullptr 키워드

C++11부터 추가된 키워드로 널포인터를 뜻한다.

nullptr은 포인터만을 위한 NULL상수이다.

Modern C++이전까지는 널포인터를 나타내기위해 NULL 매크로나 상수 '0'을 사용했다.

그러나 NULL 매크로나 상수 '0'을 함수 인자로 넘기면 정수형으로 추론되는 경우가 있어 문제가 발생한다.

이러한 문제를 해결하기 위해서 nullptr 키워드가 등장했다.


int main(){
    char* ptr = nullptr;
    cout<<sizeof(nullptr);
    /*
    int p = nullptr;
    int p2 = 0;
    if(n2 == nullptr);
    if(nullptr);
    *///=>nullptr은 클래스 이므로 에러가 발생한다.
}
cs



12. constexpr 키워드

기존의 C/C++에서 사용하던 const 키워드와 Modern C++에서 도입된 constexpr의 차이는 에러의 발생시기에 있다.


const 키워드는 변수 초기화시 런타임까지 변수의 초기화를 지연할수 있지만,

constexpr 키워드는 컴파일타임에 변수의 초기화가 이루어져야 한다.

이를 어긴다면 오류가 발생할것이다.

컴파일 타임 : 컴파일러가 바이너리 코드를 만들어내는 시기.

런타임 : 프로그램이 실제로 동작하는 시기.


즉, const 키워드는 런타임 에러를 발생시키고 constexpr 키워드는 컴파일 에러를 발생시킨다.

constexpr 키워드를 사용한다면 런타임에 수행할 작업을 컴파일 타임에 미리 수행하여 상수화 하므로

컴파일 시간은 늘어날수 있지만 런타임 수행능력은 향상된다.


constexpr 변수는 반드시 상수식으로 초기화 되어야한다.


int main(){
constexpr int a = 1;
constexpr int b = {1};
constexpr int c; // Error!
int d = 1;
constexpr int e = d + 2;// Error!
constexpr int f = time(NULL);//Error!
}


constexpr 키워드를 함수에도 사용할수 있다.

constexpr 함수는 컴파일타임에 리턴값을 계산할수 있다면 그렇게 하고, 

그렇지 않다면 일반적인 함수처럼 런타임시 리턴값을 결정하여 두가지의 경우를 하나의 함수로 구현할수 있는 편리함을 제공한다.


constexpr int func(int n){
return n*n;
}

int main(){
int n;
cin>>n;
cout<<func(n)<<endl;//런타임
cout<<func(1)<<endl;//컴파일타임
}


하지만 constexpr 함수에는 제약사항이 있다.

1. 정의후에 사용이 가능하다.

2. 증감연산(++/--)을 사용할수 없다. (C++14부터 가능.)

3. return구문은 single state이어야만 한다. (삼항연산자는 사용가능.) (C++14부터 가능.)

4. 인자에 constexpr을 사용할수 없다.

5. 지역변수를 사용할수 없다. (C++14부터 가능.)

6. 가상함수로 사용할수 없다.



13. static_assert 키워드

C언어는 assert 함수를 제공한다. 

assert(조건식);
cs

assert 함수의 조건식이 참일경우 그냥 넘어가지만 거짓일 경우 프로그램이 종료되고 에러메세지를 출력해준다.

그러나 assert 함수는 런타임시 검증을 한다.

즉 컴파일시에는 검증이 안된다.

컴파일시 검증이 된다면 디버깅 시간을 매우 줄여줄것이다.


static_assert 함수는 이러한 문제를 해결해준다.

사용법은 간단하다.

static_assert( 조건식, "에러메세지");
cs

static_assert의 조건식에서 문제되는 구문이 컴파일시 검증이 되는데 조건식이 거짓이 되는경우 런타임 에러가 아닌 컴파일 에러로 처리된다.

런타임 에러보다 컴파일 에러를 디버깅하는 시간이 훨씬 짧으므로 assert 함수보다 효율적이라고 할수있다.

중요한것은, 조건식에는 상수말고는 들어갈수 없다. 변수가 들어갈수 없다.

컴파일시 검사하기때문에 어찌보면 당연할수 있지만 이러면 static_assert를 왜쓰나 싶다...

assert는 변수도 들어갈수 있으니 적절히 조율해서 쓰는것이 좋겠다.


※ 참고로 에러메세지는 멀티바이트 캐릭터셋으로 정의 되어있기때문에 캐릭터셋을 유니코드로 설정했더라도 반드시 멀티바이트로 사용해야한다.



14. 스레드 라이브러리

기존의 C/C++에서 스레드를 사용하려면 윈도우는 Win32 API 리눅스는 pthread를 사용해야했다. 

하지만 C++11 부터 스레드에 관한 라이브러리가 표준으로 포함되었다.

사용 방법도 OS별 API에 비해서 매우 간단하므로 스레드를 다루기에 매우 편리해졌다.

뿐만아니라 공유자원 관리에 사용하는 뮤텍스 또한 표준 라이브러리에 포함되어 자원관리도 더욱 편리해진다.


std::thread 사용법

#include <iostream>
#include <thread>//스레드를 사용하기위한 헤더
using namespace std;
class Class{
public:
static void func(int a, int b){
for (int i = 0; i < b; i++)
cout << a << endl;
}
};
void func(int a, int b){
for (int i = 0; i < b; i++)
cout << a << endl;
}
int main(){
thread t1(func, 1, 5);//첫번째 인자는 함수, 두번째 인자부터는 매개변수가 들어간다.
thread t2([](int a,int b){//람다함수도 지원한다.
for (int i = 0; i < b; i++)
cout << a << endl;
}, 2, 5);
thread t3;

t1.join();//join() 함수를 호출하지 않으면 스레드 함수가 종료되기전에
t2.join();//메인함수가 종료되는 경우가 있으므로 join() 함수를 호출한다.
//해당 스레드 함수가 종료될때까지 join() 함수의 다음코드는 실행되지 않는다.
t3 = thread(Class::func,3,5);//선언만 해놓고 원하는 타이밍에 스레드를 실행시킬수 있다.
//클래스의 멤버함수도 호출가능하다.
//static함수 또는 전역함수만 가능하다.
/*
Class c;//일반 클래스 멤버함수를 호출하기 위해서는
t3 = thread(&Class::func, &c ,3, 5);//다음과 같이 호출한다.
*/
t3.join();
}


스레드 일시정지

chrono::system_clock::time_point start_time;
void func(int a, int b){
this_thread::sleep_until(start_time + 3s);//프로그램 시작후 3초뒤 실행.
for (int i = 0; i < b; i++){
this_thread::sleep_for(1s); //1초 동안 sleep
cout << a << endl;
}
}
int main(){
start_time = chrono::system_clock::now();//프로그램 시작시간
thread t1(func, 1, 5);

t1.join();
}


위 코드에서는 "1s" 처럼 숫자뒤에 초를 뜻하는 문자를 붙여줬는데 이는 c++14부터 지원한다.

c++14 이전에는 chrono 시간 객체를 생성해 시간을 표현해주면 된다.


스레드 객체 식별하기

get_id() 함수를 통해 식별하는데 주로 멀티스레딩에서 공유자원에 대한 접근을 특정 스레드만 허용하거나 할때 사용할수있다.


스레드 함수의 리턴값 받기

참조를 통해 리턴값을 받을수도 있겠지만 promise 객체와 future 객체를 이용해 리턴값을 받는법을 설명해본다.

#include <iostream>
#include <thread>
#include <future>//promise 객체와 future 객체를 사용하기위한 헤더

using namespace std;
void sum(promise<int>& ret, int t){
this_thread::sleep_for(5s);
int sum = 0;
for(int i=1;i<=t;i++)sum+=i;
ret.set_value(sum);
}
int main(){
promise<int> ret;
future<int> value = ret.get_future();//future<int> => auto 로 대체가능.
thread t1(sum,ref(ret),10);//참조 형태로 넘기고 싶다면 ref() 함수를 사용한다.

cout<<value.get()<<endl;//get() 함수는 set_value()가 호출될때까지 기다린다.
//그러므로 join()함수를 이 다음줄에 호출해도 에러가 발생하지 않는다.
//스레드의 종료를 기다리는것이 아니다.
t1.join();
}

※future 객체는 Modern C++에서 새롭게 지원하는 비동기처리(std::async)를 할때도 사용된다.


공유자원 관리

#include <iostream>
#include <fstream>
#include <thread>
#include <mutex>//뮤텍스를 사용하기위한 헤더

using namespace std;

mutex file_mutex;
void func(string str){
ofstream file("test.txt", ios::app);
file_mutex.lock();
for (auto i : str)
file << i;
file << endl;
file_mutex.unlock();
}
int main(){
thread t1(func, "thread 1");
thread t2(func, "thread 2");

t1.join();
t2.join();
}


개발자의 실수로 lock()을 호출한뒤에 unlock()을 호출해주지 않으면 교착상태(deadlock)에 빠진다. 

이를 해결해주는게 lock_guard이다. lock_guard 객체는 해당 scope를 벗어날때 자동으로 unlock을 해준다.

mutex file_mutex;
void func(string str){
ofstream file("test.txt", ios::app);
{
lock_guard<mutex> guard(file_mutex);
for (auto i : str)
file << i;
file << endl;
}
}


※ std::mutex는 Windows OS의 경우 OS에서 제공하는 커널모드 객체(Mutex, Semaphore, Event)가 아닌 유저모드 객체인 Critical Section으로 구현되어있다.


std::atomic

atomic 객체를 사용하면 별다른 동기화 작업 없이 공유자원을 사용할수 있다.

쉽게말해서 한 스레드만이 접근 가능한 변수이다.

#include <iostream>
#include <thread>
#include <atomic> //atomic 클래스를 사용하기위한 헤더
using namespace std;
void func(atomic<int> &a, int t, int n){
for (int i = 0; i < t; i++){
cout << "thread" << n << ":" << a++ << endl;
this_thread::sleep_for(1s);
}
}
int main(){
atomic<int> a(1); //atomic은 클래스이기 때문에 클래스 초기화 하듯 초기화해야한다.
//유니폼 초기화, 선언후 초기화도 가능하다.
thread t1(func, ref(a), 5, 1);
thread t2(func, ref(a), 5, 2);
thread t3(func, ref(a), 5, 3);

t1.join();
t2.join();
t3.join();
}



15. 정규식 라이브러리

정규식은 boost 라이브러리를 통해 정규식이 지원은 되었으나 Modern C++에서 표준 라이브러리에 포함되었다.

특히나 문자열을 다루기 힘든 C++인데 개발자에게 더욱 편리해졌다.


Modern C++에서 정규식을 사용하는데 필요한것은 클래스와 함수이다.


[클래스]

std::regex

- 정규식 패턴

std::match_results(cmatch, smatch, wsmatch), std::sub_match(csub_match, ssub_match, wcsub_match)

- 정규식이 하위 표현식으로 구성되어있고 이를 역참조 할때 사용.

- sub_match는 match_results에 저장된 개별항목이며 match_results는 sub_match를 vector로 관리한다.

- char*, wchar_t*, string의 자료형을 구분해서 사용해야한다.

std::regex_iterator

- match_results의 sub_match를 위한 반복자.


[함수]

std::regex_match

- 문자열이 정규식에 정확하게 일치하는지 여부를 bool형식으로 반환


int main(){
    string s;
    regex reg("^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$");//휴대폰번호 정규식
cin>>s;
    if(regex_match(s, reg))cout<<"match!!"<<endl;
    else cout<<"not match.."<<endl;
}


괄호를 통해 그룹화 하여 match_results 객체에 그룹화 되어 저장된다.

int main(){
    string s;
    regex reg("^([0-9]{3})-([0-9]{3,4})-([0-9]{4})$");//휴대폰번호 정규식에서 괄호()를 통해
smatch m;//string형을 담는 match_results //match_results 그룹화되어 저장된다.
cin>>s;
    if(regex_match(s, m, reg)){
cout<<"match!!"<<endl;
for(int i=0;i<m.size();i++)
cout<<m[i]<<endl;//첫번째 배열에는 매칭된 문자열, 두번째 부터는 그룹화된 문자열.
//for(auto& i : m)cout<<i<<endl;
}
    else cout<<"not match.."<<endl;
}

실행결과

std::regex_search

- 문자열이 정규식에 부분적으로 매치되는지 여부를 bool 형식으로 반환


int main(){
regex reg("(\\w+[\\w\\.]*@\\w+[\\w\\.]*\\.[A-Za-z]+)");//이메일로 그룹화한 정규식
string s("MAILTO:tester@naver.com\nMAILFROM:trudy@gmail.com\n");
smatch m;

if (regex_search(s, m, reg)){
cout<<m[0]<<endl;
}
//출력
//tester@naver.com
}


문자열에 포함된 모든 이메일을 가져오려면 아래와 같이 코드를 수정해야 한다.

int main(){
regex reg("(\\w+[\\w\\.]*@\\w+[\\w\\.]*\\.[A-Za-z]+)");//이메일로 그룹화한 정규식
string s("MAILTO:tester@naver.com\nMAILFROM:trudy@gmail.com\n");
smatch m;
while(regex_search(s, m, reg)){
cout<<m[0]<<endl;
s = m.suffix();
}
//출력
//tester@naver.com
//trudy@gmail.com
}


regex_token_iterator 객체를 사용해 가져올수도 있다.

int main(){
    string s="MAILTO:tester@naver.com\nMAILFROM:trudy@gmail.com\n";
    regex reg("(\\w+[\\w\\.]*@\\w+[\\w\\.]*\\.[A-Za-z]+)");//이메일로 그룹화한 정규식
sregex_token_iterator it(s.begin(), s.end(), reg), end;
while(it!=end)cout << *it++ << endl;
//출력
//tester@naver.com
//trudy@gmail.com
}



std::regex_replace

- 매치된 정규식을 대체 문자열로 수정 또는 교체하고, 교체완료된 문자열을 반환


그룹화하여 match_results 객체에 저장된 요소들을 그룹화 순서대로 $1, $2, $3... 형식으로 표현할수 있다.

이를 통해 regex_replace 함수를 사용하여 문자열의 교체 및 추가도 가능하다.


int main(){
regex pattern("([0-9]{1})([a-z]{1})([A-Z]{1})");
string str("0sS");
string result = regex_replace(str, pattern, string("Result : $3$1$2$1$3$2$3$1"));
cout << result << endl;
//출력
//Result : S0s0SsS0
}




#define ICON_ID 0x1234
#define WM_TRAY 0x2345
#define MENU_INFO_MESSAGE 0x101
#define MENU_OPEN_MESSAGE 0x102
#define MENU_QUIT_MESSAGE 0x103
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){
int wmEvent, wmId;
HMENU hMenu, hPopupMenu, hMenubar;
hMenu = CreateMenu();
hMenubar = CreateMenu();
//hMenu에 메뉴 리스트를 추가해준다.
//3번째 인자는 매크로로 설정한 메세지로 이벤트 발생시 WM_COMMAND에서 처리.
AppendMenu(hMenu, MF_STRING, MENU_INFO_MESSAGE, TEXT("Info"));
AppendMenu(hMenu, MF_STRING, MENU_OPEN_MESSAGE, TEXT("Open"));
AppendMenu(hMenu, MF_STRING, MENU_QUIT_MESSAGE, TEXT("Quit"));
AppendMenu(hMenubar, MF_POPUP, (UINT_PTR)hMenu, TEXT("Menu"));
switch (iMessage){
case WM_CREATE:
NOTIFYICONDATA nid;
ZeroMemory(&nid, sizeof(nid));
nid.cbSize = sizeof(nid);
nid.hWnd = hWnd;
nid.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE;
nid.uCallbackMessage = WM_TRAY; //트레이 아이콘 클릭시 발생될 윈도우 메세지
nid.uID = ICON_ID; //트레이 아이콘 식별자
nid.hIcon = (HICON)LoadImage(NULL, TEXT("icon.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED);
lstrcpy(nid.szTip, TEXT("Sample")); //트레이 아이콘에 마우스를 올렸을때 보일 메세지
Shell_NotifyIcon(NIM_ADD, &nid);
/*
NIM_ADD : 트레이에 새로운 아이콘 추가
NIM_DELETE : 트레이 영역의 아이콘 제거
NIM_MODIFY : 트레이 영역에 있는 아이콘 수정
*/
return 0;
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
switch (wmId){
case MENU_INFO_MESSAGE:
//Info를 클릭했을시 처리할 루틴
break;
case MENU_OPEN_MESSAGE:
//Open을 클릭했을시 처리할 루틴
break;
case MENU_QUIT_MESSAGE:
//Quit을 클릭했을시 처리할 루틴
exit(0);
}
break;
return 0;
case WM_TRAY://마우스와 관련된 메세지만 처리한다.
if (wParam == ICON_ID){//등록시 설정한 트레이 아이콘 식별자
switch (lParam){
case WM_RBUTTONUP:
hPopupMenu = GetSubMenu(hMenubar, 0);
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hWnd);
TrackPopupMenu(hPopupMenu, TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, pt.x, pt.y, 0, hWnd, NULL);
SetForegroundWindow(hWnd);
PostMessage(hWnd, WM_NULL, 0, 0);
break;
}
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return (DefWindowProc(hWnd, iMessage, wParam, lParam));
}


'C&C++' 카테고리의 다른 글

Features And Uses Of STL Container In C++  (0) 2018.11.04
Modern C++  (1) 2018.10.02
How To Use Json In C++(jsoncpp)  (0) 2018.09.08
Get Window Focus On Other Window  (0) 2018.07.09
WINAPI Deny System Key Code  (0) 2018.06.26

How To Install jsoncpp


C++에서 json을 사용해야할때 많은 라이브러리들이 있지만 가장 유명한 jsoncpp 라이브러리를 사용해보았다.


깃허브에 있는 jsoncpp의 공식 깃(https://github.com/open-source-parsers/jsoncpp)에서 클론을 뜬뒤 


amalgamate.py 를 실행시키면 dist 폴더가 생기고 그안에 헤더 두개와 cpp파일 하나가 생성된다. dist.zip


 #include "json/json.h" 를 선언해 include 하고 이 파일들을 사용할 cpp파일과 함께 컴파일하면 된다.


※ jsoncpp는 C++11 버전부터 사용 가능하다.


How To Use jsoncpp


read json


//test.json
{
    "integer_key" : 
    [
        1,
        2,
        3
    ],
    "string_key" : 
    [
        "value1",
        "value2",
        "value3"
    ]
}
cs


//Read Json
#include <iostream>
#include <fstream>
#include "json/json.h"
 
using namespace std;
int main() {
    Json::Value root;
    Json::Reader reader;
    ifstream json("test.json", ifstream::binary);
    
     reader.parse(json,root);//json >> root; 두 구문 모두 json을 파싱하는데 사용할 수 있다.
 
    //Json::Value 객체는 begin(), end() 둘다 정의되서 범위기반 for문도 사용 가능하다.
    for(auto& value:root)
        cout<<value<<endl;
    for(auto& value:root["string_key"])
        cout<<value.asString()<<endl;//string으로 가져오기
    for(auto& value:root["integer_key"])
        cout<<value.asInt()<<endl;//integer로 가져오기
        
 
    cout<<root.get("string_key","UTF-8");//get 함수를 통해 value를 가져올수도 있다.
}
cs


write json


//Write Json
#include <iostream>
#include <fstream>
#include "json/json.h"
 
using namespace std;
int main(){
    Json::Value root;
    Json::Value string_value;
 
    for (int i = 0; i < 3; i++)
        root["integer_key"].append(i+1);
 
    string_value["string"][0= "value1";
    string_value["string"][1= "value2";
    string_value["string"][2= "value3";
 
    string_value["cstring"][0= "cvalue1";
    string_value["cstring"][1= "cvalue2";
    string_value["cstring"][2= "cvalue3";
 
    root["string_key"= (string_value);//append() 사용가능
    ofstream outFile("test.json", ios::out);
    outFile << root;
    /*
    Json::StyledWriter writer;
    string json = writer.write(root);
    ofstream outFile("test.json", ios::out);
    outFile << json;
    */
}
cs


//test.json
{
    "integer_key" : 
    [
        1,
        2,
        3
    ],
    "string_key" : 
    {
        "cstring" : 
        [
            "cvalue1",
            "cvalue2",
            "cvalue3"
        ],
        "string" : 
        [
            "value1",
            "value2",
            "value3"
        ]
    }
}
cs



좀더 구체적인 사용법 : http://open-source-parsers.github.io/jsoncpp-docs/doxygen/index.html#_advanced

'C&C++' 카테고리의 다른 글

Modern C++  (1) 2018.10.02
WINAPI How To Load Windows Tray Icon  (0) 2018.09.08
Get Window Focus On Other Window  (0) 2018.07.09
WINAPI Deny System Key Code  (0) 2018.06.26
"" , L"" , TEXT("") , _T("")  (0) 2018.06.15

 

일반적으로 GetFocus() 함수는 현재 윈도우에서 포커스를 가진 핸들을 가져온다.

 

그말인 즉슨 다른 윈도우를 대상으로 이함수를 사용하면

 

포커스를 가진 윈도우가 다른 스레드의 메시지큐에 있기 때문에 NULL을 반환한다는 말이다.

 

이에대한 해결법은 아래와 같다.

 

1
2
3
4
5
HWND hWnd = GetForegroundWindow();
DWORD fromId = GetCurrentThreadId();
DWORD toId = GetWindowThreadProcessId(hWnd, NULL); 
AttachThreadInput(fromId, toId, TRUE);
HWND focus = GetFocus();
cs

 

참고 : http://ditongs.egloos.com/1353649 

'C&C++' 카테고리의 다른 글

WINAPI How To Load Windows Tray Icon  (0) 2018.09.08
How To Use Json In C++(jsoncpp)  (0) 2018.09.08
WINAPI Deny System Key Code  (0) 2018.06.26
"" , L"" , TEXT("") , _T("")  (0) 2018.06.15
Using scanf() As gets()  (0) 2018.06.15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LRESULT CALLBACK KeyHook(int nCode, WPARAM wParam, LPARAM lParam){
    KBDLLHOOKSTRUCT *pk = (KBDLLHOOKSTRUCT*)lParam;
    if(nCode == HC_ACTION){
        switch(wParam){
            case WM_KEYDOWN:
            case WM_SYSKEYDOWN:
            case WM_SYSKEYUP:
            case WM_KEYUP:
                if(((pk->vkCode==0x09)&&(pk->flags=0x20))||//alt + tab
                ((pk->vkCode==0x1b)&&(pk->flags=0x20))||//alt + esc
                ((pk->vkCode==0x1b)&&(pk->flags=0x00))||//ctrl + esc
                ((pk->vkCode==0x73)&&(pk->flags=0x20))||//alt + f4
                ((pk->vkCode==0x09)&&(pk->flags=0x01))||//window + tab
                ((pk->vkCode==0x5b)&&(pk->flags=0x01))||//window + left
                ((pk->vkCode==0x5c)&&(pk->flags=0x01)))return 1;//window + right
        }
    }
    return CallNextHookEx(0, nCode, wParam, lParam);
}
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow){
    HHOOK hook;
    hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyHook, hInstance, 0);
}
cs


'C&C++' 카테고리의 다른 글

How To Use Json In C++(jsoncpp)  (0) 2018.09.08
Get Window Focus On Other Window  (0) 2018.07.09
"" , L"" , TEXT("") , _T("")  (0) 2018.06.15
Using scanf() As gets()  (0) 2018.06.15
WINAPI GetClipboardData()  (0) 2018.05.17

"string" : 멀티바이트코드

L"string" : 유니코드


TEXT("string") : 멀티바이트일때 "", 유니코드일때 L""로 변환하는 매크로. (a.k.a TCHAR)

_T("string") : TEXT 매크로와 같으나 주로 MFC에서 사용. 


※ 참고 : #define UNICODE를 사용하려면 windows.h 헤더파일 선언전에 선언해야한다.

'C&C++' 카테고리의 다른 글

Get Window Focus On Other Window  (0) 2018.07.09
WINAPI Deny System Key Code  (0) 2018.06.26
Using scanf() As gets()  (0) 2018.06.15
WINAPI GetClipboardData()  (0) 2018.05.17
Parameter Split in WinMain (Argc, Argv)  (0) 2018.05.10
1
2
3
4
5
int wmain(int argc, wchar_t* argv[]){
    wchar_t str[100];
    wscanf(L"%[^\n]",&str);
    wprintf(L"%s\n",str);
}
cs


'C&C++' 카테고리의 다른 글

WINAPI Deny System Key Code  (0) 2018.06.26
"" , L"" , TEXT("") , _T("")  (0) 2018.06.15
WINAPI GetClipboardData()  (0) 2018.05.17
Parameter Split in WinMain (Argc, Argv)  (0) 2018.05.10
C++ WinSock Sample Code  (0) 2018.04.09
1
2
3
4
5
6
7
8
OpenClipboard(0);
if(IsClipboardFormatAvailable(CF_UNICODETEXT)||IsClipboardFormatAvailable(CF_OEMTEXT)){
    HANDLE hClipboard;
    hClipboard = GetClipboardData(CF_UNICODETEXT);
    wchar_t* data = (wchar_t*)GlobalLock(hClipboard);
    MessageBoxW(0,data,L"MessageBox",MB_OK);
}
CloseClipboard();
cs


'C&C++' 카테고리의 다른 글

"" , L"" , TEXT("") , _T("")  (0) 2018.06.15
Using scanf() As gets()  (0) 2018.06.15
Parameter Split in WinMain (Argc, Argv)  (0) 2018.05.10
C++ WinSock Sample Code  (0) 2018.04.09
[MinGW] Using DLL and Compile Options  (0) 2018.04.09
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<windows.h>
#include<tchar.h>
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdPram, int nCmdShow){
    TCHAR cmdline[4096] ;
    TCHAR* argv[4096] ;
    int argc = 0 ;
    _tcscpy( cmdline, GetCommandLine() ) ;
    argv[argc] = _tcstok( cmdline, TEXT(" \t") ) ;
    while( argv[argc] != 0 )
        argv[++argc] = _tcstok( 0, TEXT(" \t") ) ;
    for(int i=0;i<argc;i++)
        MessageBox(NULL, argv[i], "MessageBox caption", MB_OK);
}
cs

 

'C&C++' 카테고리의 다른 글

Using scanf() As gets()  (0) 2018.06.15
WINAPI GetClipboardData()  (0) 2018.05.17
C++ WinSock Sample Code  (0) 2018.04.09
[MinGW] Using DLL and Compile Options  (0) 2018.04.09
swap using exclusive-or  (0) 2017.02.15

+ Recent posts