본문 바로가기

꿀팁!

modern C++ 공부했던것

백업용으로 블로그에 저장하려고 합니다.

 

1.  Auto 키워드

Auto

변수 정의 때 명시적으로 타입을 지정하지 않아도 된다.->phpvar느낌

Auto로 정의한 변수는 초기화 할 때 type이 결정된다.->초기화 값 없이 사용하면 에러 발생

컴파일 타임 때 type이 결정된다.

Stl에 주로 쓰고 코드 가독성이 좋아진다.->iterator에 자주 쓰인다.

매개변수로 사용하면 안된다.->대신 template를 사용하면 된다.

함수의 return 형식으로 사용하는 것은 가능하다.->대신 return형이 동결 되어야 한다.(returnint면 모든 return값이 int형이여야한다.)

구조체나 클래스도 auto로 받을 수 있다.

#include<iostream>
#include<vector>
using namespace std;
auto nana() {
	cout << "함수의 리턴형도 가능" << endl;
	return 0;
}
class a {
	int b;
}c;
int main() {
	auto nam = "nam";
	cout << nam << endl;
	auto nam2 = 100 / 3;
	auto nam3 = 100 / 3.0;
	auto nam4 = &nam;
	auto nam5 = new int[10];
	auto nam6 = c;
	vector<int>nam7;
	vector<int>::iterator it = nam7.begin();
	auto nam8 = it;
	cout << "요놈은 int:"<<nam2 << endl;
	cout << "이놈은 double:"<<nam3 << endl;
	cout<<"포인터도 가능:"<<*nam4<<endl;
	cout << "동적 할당해도 int*로 가능:"<<nam5 << endl;
	
}

 

.  범위기반 for

범위기반 for문이란?

기존의 for문보다 더 간단하고 안전하다.->파이썬의 for in ~문과 비슷한 것 같다.

보통 auto 키워드와 함께 쓰인다.

값을 복사해서 변수에 넣게 되는데 &을 사용하면 복사를 하지 않아서 성능면에서 좋아진다.(추가로 값에 직접 접근이 가능하다.)

값을 읽기전용으로 하고싶으면 const명령어를 사용하면 된다.

기본적인 사용예시는 for(auto i:v)이다.(v는 배열,컨테이너이다.)

#include<iostream>
#include<vector>
using namespace std;

int main() {
	int nam[5] = { 1,2,3,4,5 };
	for (int na : nam)
		cout << na;
	for (auto na : nam)
		cout << na;
	for (auto &na : nam)
		na+=1;
	for (auto &na : nam)
		cout << na;
	vector<int> nu = { 1,2,3,4,5 };
	for (const auto& nim : nu) {
		cout << nim;
	}
}

 

3.  유니폼 초기화

유니폼 초기화란?

객체 등을 {} 중괄호로 초기화 한다.

해당 변수의 타입과 다른 것을 못쓴다.->타입에 맞게 써야한다.->auto에는 불가능하다.(int a=3.14가 가능했지만 유니폼 초기화에선 에러가 뜬다.)

동적 할당,함수의 인자 및 리턴 값 등으로 활용가능하다.

#include<iostream>
#include<vector>
using namespace std;

typedef struct nam {
	int a;
	int b;
}Na;
vector <int> nanu() {
	return { 1,2,3,4 };
}
void nini(vector<int>a) {
	for (auto i : a)
		cout << i << endl;
}
int main() {
	int v{ 1 };
	int v2[]{ 1,2,3 };
	vector<int>v3{1, 2, 3};
	Na nu{ 5,6 };
	cout << nu.a << nu.b << endl;
	auto nana = new int[5]{ 1,2,3,4,5 };
	vector<int> nanananana = nanu();
	nini({ 1,2,3 });
}

 

4.  decltype키워드

decltype이란?

Decltypedeclared type(선언된 형식)의 줄임말으로써 주어진 이름이나 표현식의 구체적인 타입을 알려준다.

auto와의 차이점은 auto는 값에 상응하는 타입을 추론시켜주는 키워드이고 decltype은 값의 값부터 타입을 추출해 낼 수 있는 키워드이다.

변수 선언시에는 auto를 쓰는게 더 편하고 주로 return 형식에서 의도한 것과 다른 형식으로 결정 될 수 있기 때문에 decltype으로 auto반환을 편하게 해준다.

반환 형식으로 auto를 써주기 위해 decltype을 사용하지만 14++형식부터는 decltype을 생략이 가능하지만 decltype(auto)로 해주는게 더 좋다.

#include<iostream>
#include<vector>
using namespace std;

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>
decltype(auto) func3(A a, B b) {
	return a + b;
}
int main() {
	auto a = 1;
	auto b = 2.5;
	decltype(a + b)c=3;
	cout << typeid(c).name() << endl;
}

.  스마트 포인터

스마트 포인터란?

C++ 프로그램에서 new키워드를 사용하여 동적으로 할당 받은 메모리는 delete 키워드를 사용하여 해제해야 한다.

C++ 에서는 메모리 누수로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공하고 있다.

스마트 포인터는 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해체해 준다.

C++11 이전에는 auto_ptr이라는 키워드를 사용했고 c++11부터는 unique_ptr, shared_ptr, weak_ptr이라는 새로운 포인터를 제공했다.

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

#include<iostream>
#include<vector>
#include<memory>
using namespace std;


int main() {
	auto_ptr<int> pt(new int(1));
	auto_ptr<int> pt2 = pt;
}

Unique_ptr이란?

하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터이다.

해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있다.

Move() 멤버 함수를 통해 소유권을 이전할 수 없지만, 복사할 수는 없다.->소유권을 이전하면 이전 인스턴스는 더는 해당 객체를 소유하지 않게 재설정된다.

Reset() 멤버 함수를 통해 가르키고 있는 메모리 영역을 삭제한다.

대입연산자를 이용한 복사는 오류를 발생시킨다.

C++14 이후부터 Make_unique() 함수를 사용하면 unique_ptr의 인스턴스를 안전하게 생성할 수 있습니다.

#include<iostream>
#include<vector>
#include<memory>
#include<string>
using namespace std;

class nunu {
private:
	string ni_;
	int na_;
public:
	nunu(const string& ni, int na);
	void soge();
};
nunu::nunu(const string& ni, int na) {
	ni_ = ni;
	na_ = na;
}
void nunu::soge() {
	cout <<ni_<<na_<< endl;
}
int main() {
	unique_ptr<int> ptr01(new int(5)); 
	auto ptr02 = move(ptr01);         
	ptr02.reset();                   
	ptr01.reset();            
	unique_ptr<nunu> ha = make_unique<nunu>("nam", 20);
	ha->soge();
}

Shared_ptr이란?

하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터이다.

참조하고 있는 스마트 포인터의 개수를 (참조 횟수)라고 한다.

참조 횟수는 shared_ptr이 추가될 때마다 1씩 증가하고 수명이 다할 때 1씩 감소한다.->수명이 다하여 참조 횟수가 0이되면 delete키워드를 통해 메모리를 자동으로 해제한다.

Use_count() 함수는 참조 횟수를 보여준다.

Make_shared() 함수를 이용하면 shared_ptr의 인스턴스를 안전하게 생성할 수 있다.

Reset()함수를 통해 해제가 가능하다.(rootreset하게 되면 전체가 싹 지워진다.)

 Weak_ptr이란?

Shared_ptr 인스턴스가 소유하는 객체에 접근이 가능하고 소유자의 수에는 포함되지 않는 스마트 포인터이다.

서로가 상대방을 가르키고있는 shared_ptr(절대 0이 될 수 없다.)->순환참조 상태를 제거하기 위해 사용된다.

#include<iostream>
#include<vector>
#include<memory>
#include<string>
using namespace std;

class nunu {
private:
	string ni_;
	int na_;
public:
	nunu(const string& ni, int na);
	void soge();
};
nunu::nunu(const string& ni, int na) {
	ni_ = ni;
	na_ = na;
}
void nunu::soge() {
	cout <<ni_<<na_<< endl;
}
int main() {
	shared_ptr<int> ptr1(new int(5));
	cout << ptr1.use_count() << endl;
	auto ptr2(ptr1);
	cout << ptr1.use_count() << endl;
	auto ptr3 = ptr1;     
	cout << ptr1.use_count() << endl;
	shared_ptr<nunu> ha = make_shared<nunu>("nam", 20);
	cout << ha.use_count() << endl;
	auto nl = ha;
	cout << ha.use_count() << endl;
	nl.reset();
	cout << ha.use_count() << endl;
	weak_ptr<nunu> wk = ha;
	cout << ha.use_count() << endl;
}

 

 

6.  람다표현식

람다표현식이란?

함수를 별도의 선언없이 사용하게 해준다.

개발자 입장에서 코딩이 간편해지고 코드의 가독성이 향상된다.

한번 사용하고말 함수는 코드 전체를 볼 때 가독성이 떨어지게 할 수 있다.->이때 람다함수를 사용해 표현하면 코드의 가독성이 상향되게 된다.(수십줄의 코드를 단 몇줄로 줄여주는 효과)

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

재귀함수를 호출 할때에는 auto를 사용하면 안된다->std:function을 사용해야한다.(std:function을 사용할때에는 functional 헤더파일을 포함시켜야 한다.)

captures란?

람다 함수 외부에 선언된 변수에 엑세스할 변수들을 캡쳐한다.

변수명,=(call by value),&(call by reference)가 들어간다.->비워두면 아무것도 사용하지 않는다는 뜻이다.

Ex1)[=]->모든 변수를 call by value로 캡쳐한다.

Ex2)[&]->모든 변수를 call by reference로 캡쳐한다.

Ex3)[a,&b]->acall by value, bcall by reference로 캡쳐한다.

Ex4)[=,&a,&b]->a,bcall 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 키워드를 통해 선언이 가능하다.

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

mutable이란?

Call by value로 캡쳐된 변수들이 lambda body에서 새로 만들 때 const키워드가 붙지 않는다.

[=]call by value이기 때문에 lambda body 내부에서만 일시적으로 값을 변경하는 것이다.

Return type이란?

후행반환 형식을 사용한다.

생략이 가능하며 추론이 가능하다.

리턴을 한번만 하거나 없는 경우 ->자동타입 추론(c++11)

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

 

body란?

함수의 내용이 들어간다.(기존 알고있는 함수와 동일하다.)

execute란?

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

 

#include<iostream>
#include<vector>
#include<memory>
#include<string>
using namespace std;

class nunu {
private:
	string ni_;
	int na_;
public:
	nunu(const string& ni, int na);
	void soge() {
		[this]() {cout << ni_ << na_ << endl; }();
	}
};
nunu::nunu(const string& ni, int na) {
	ni_ = ni;
	na_ = na;
}
auto a = 5;
auto lambda() {
	return [&]() {a = 3; cout << a<<endl; };
}
int main() {
	auto a = 5;
	[&](){a = 3; cout << a << endl;}();
	auto func = [&]() {a = 3; cout << a << endl; };
	func();
	auto func2 = lambda();
	func2();
	auto func3 = [&]() {
		return [&]() {a = 3; return a; }(); };
	cout << func3() << endl;
	nunu n("na", 20);
	n.soge();

}

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

우측값 참조란?

이전 c언어03에서는 식의 우측에 있는값을 의미했고, c++에서는 식이 끝나고 계속존재하면 좌측값, 식이 끝나면 존재하지않는 임시값은 우측값이다.

주소값을 가져오는 연산자 &를 붙여서 에러가 난다면 우측값이다.

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

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

초기화 없이 사용할 수 없으며 반드시 우측값으로 참조해줘야한다.(임시객체-상수)

우측값 참조변수는 좌측값이다.

이동 시멘틱과 퍼펙트 포워딩을 위해 도입된 개념이라고 볼 수 있다.

#include<iostream>
#include<vector>
#include<memory>
#include<string>
using namespace std;
int main() {
	int &&a = 5;
}

8.  Move semantics(이동 시멘틱)*

이동 시멘틱이란?

C++11에서 추가된 문법으로 객체의 메모리 소유권을 이전하는 방식의 문법을 말한다.

Move() 생성자와 move대입 연산자를 사용한다.->둘다 우측값 참조인 (&&)을 사용한다.

C++98/03에선 Vector의 크기를 키울 때 메모리를 확보하고 데이터를 복사 후 삽입을 했다. 하지만 c++0x부턴 성능 부하가 큰 복사대신 메모리상으로 이동을 한다.->이것이 이동 시멘틱이다.

Std:move는 좌측 값을 우측 값으로 타입 캐스팅(형변환) 하기 위해 제공된다.

Copy semantics는 깊은 복사를 통해 원본과 똑 같은 객체를 생성해 복사하는것으로 메모리 낭비가 심하고, move semantics는 메모리 소유권을 이전으로 얕은 복사를 하고 원본을 NULL로 초기화 한다.

#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(const 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 << "[+] movedeip!" << 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 swap1(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;
	swap1(str1, str2);
	cout << "[=] str1 : "; str1.print();
	cout << "[=] str2 : "; str2.print();

}

swap함수를 보면 move()를 통해 우측값으로 만들어주고 이동 생성자와 이동 대입 연산자의 부분을 만들 때 인자 값이 우측값일때의 조건으로 만들어 준것이다.

tmp에서 새로운 객체를 만들 때 이동생성자가 적용되고 =형태로 이동할때는 위에서 만들어준 이동 대입 연산자로 이동하는 것이다.

이동연산의 목적으로 이동연산을 수행한 객체는 빈 상태 이므로 사용해서는 안된다.

 

9.  Perfect forwarding(완벽한 전달)*

완벽한 전달이란?

#include <iostream>
#include<string>
using namespace std;
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); 
}

이렇게 인자가 좌측값이냐 우측값이냐 에 따라서 달라지는 함수를 오버로딩을 통해 구현했을때 원하는데로 작동하지 않는 경우가 있다. 이럴때 perfect forwarding의 개념이 필요하다.

Move semanticsmove()가 있듯이 perfect forwarding에도 Forward()가 있다. Forward()는 우측값으로 초기화된 참조변수라면 우측값 참조변수로, 그렇지 않다면 좌측값 참조변수로 변환해준다.

#include <iostream>
#include<string>
using namespace std;
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(forward<T>(t));
}
int main() {
	int a = 1;
	proc(a);
	proc(1);
}

Void proc 부분이이 바로 forward()를 이용한 예로써 제대로 구별 할 수 있게된다.

Forward()는 우측값으로 초기화 되었는지 확인하기 위해 templateT를 사용한다.

 

 

Universal reference는 초기화시 넘어오는 인자에 따라 좌측값이면 좌측값 참조로 우측값이면 우측값 참조로 결정해준다.->타입 추론에만 universal reference로 동작한다 이외는 rvalue만 가능하다. ex(auto&& a=b;),(template<typename T>void func(T&& t);)

Reference Collapse

Universal reference는 좌측참조인지 우측참조인지를 reference collapse를 통해 결정한다.

func(String& && s);

위는 참조에 대한 참조라는 의미를 가지고있다. 하지만 지금까지 배운 개념으로는 없기 때문에 허용이 되면 안된다. 하지만 위처럼 프로그래머가 함수를 정의한다면 컴파일시에 에러가 발생하지만 universal reference에서는 허용된다.

이렇게 컴파일러는 참조에 대한 참조가 발생했을 때 reference collapse에 의해 아래와 같은 규칙으로 참조가 된다.

T& & t => T& t

T& && t => T& t

T&& & t => T& t

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

이러한 규칙으로 참조에 대한 참조를 해결하고 universal reference또한 이 규칙으로 좌우측값을 결정한다.

10.  Nullptr 키워드

nullptr이란?

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

Nullptr은 포인터만을 위한 null상수이다.

null매크로나 상수 ‘0’을 함수 인자로 넘기면 정수형으로 추론되는 경우로 인해 문제가 발생해서 nullptr 키워드가 등장했다.

#include<iostream>
#include<vector>
#include<memory>
#include<string>
Using namespace std;
int main() {
	int *ptr = nullptr;
	cout << typeid(nullptr).name();
	cout << sizeof(nullptr);
}

11.  Constexpr 키워드

constexpr이란?

Const 키워드는 변수 초기화시 런타임까지 변수의 초기화를 지연할 수 있지만(런타임 상수 초깃값을 런타임에서만 확인할 수 있는 상수) constexpr 키워드는 컴파일 타임에 변수 초기화가 이루어져야한다(컴파일 시간에 초깃값을 확인할 수 있는 상수).

컴파일 타임은 컴파일러가 바이너리 코드를 만들어내는 시기이고 런타임은 프로그램이 실제로 동작하는 시기이다.

Constexpr 키워드는 런타임에 수행할 작업을 컴파일 타임에 하므로 컴파일 시간은 늘어날 수 있지만 런타임 수행능력은 향상된다.

Constexpr 함수는 컴파일타임에 리턴값을 계산할 수 있으면 계산하고 없으면 일반적인 함수형태로 된다.->두가지의 경우를 하나의 함수로 구현 할 수 있는 편리함을 제공한다.

constexpr함수의 제약사항

1. 정의후에 사용이 가능

2. C++14이전에는 증감연산(++,--)을 사용할 수 없었고 이후에는 사용이 가능하다.

3. C++14이전에는 return구문은 single state(단하나),삼항연산자만 가능했지만 이후에는 다른 것들도 사용이 가능하다.

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

5. C++14이전에는 지역변수를 사용할 수 없었지만 이후에는 사용이 가능하다.

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

#include<iostream>
#include<vector>
#include<memory>
#include<string>
using namespace std;
constexpr int fuk(int n) {
	return n + n;
}
int main() {
	constexpr int a = 1;
	constexpr int b = { 2 };
	int n;
	cin >> n;
	cout << fuk(n) << endl;
	cout << fuk(5) << endl;
}