Chapter 18. Visiting with the New C++ Standard

지환·2022년 8월 23일
0

링크
(여긴 거의 다 정리된듯. 난 필요한 것만 좀 정리함)

>>C++11<<

Uniform Initialization
모든 곳에서 {}를 이용한 초기화가 가능하도록 확대

initializer_list
class ctor의 매개변수로 std::initializer_list가 설정돼있다면, 의미를 잘 해석해야한다.
vector<int> a1(10); : 길이 10짜리 vector
vector<int> a2{10}; : initializer_list있는 ctor사용. 원소가 10인 길이 1짜리 vector
vector<int> a3{1,2,3}; : 마찬가지로 initializer_list있는 ctor사용. 원소가 각각 1,2,3인 길이 3짜리 vector

vector class에 initializer_list가 있는 ctor이 없었다면,
`vector<int> a{10};` 이것도 첫번째랑 같은 의미가 된다.
언어 자체에 내장된 일반 braces 형식으로 인식됨.

auto
auto p1 = il.begin();
식으로 간단하게 type 명시없이 초기화 가능
(근데 이렇게 type 추측하게해서 시간 버리느니 명시해주는게 더 안낫나?)

using =
typedef로도 template의 alias를 만들 수 있지만,
ex.typedef std::vector<int>::iterator itType;,
using =을 써도 된다.
ex. using itType = std::vector<int>::iterator

차이는, alias를 자체를 template처럼, 특정 부분을 template처럼 만들 수 있다는 것이다.
ex.

template<typename T>
using arr12 = std::array<T, 12>;

nullptr
C에서의 NULL처럼 header에 정의된 상수가 아니라, 언어 자체에서 제공하는 keyword이다.
C/C++ 모두 source code상의 표현은 0이지만, 내부 표현은 다를 수가 있는데, 이건 알아서 변환된다.
근데 그렇다고 상수든 포인터든 모두 0으로 표현하면 오류 발생 위험이 있으므로 C에서도 굳이 NULL을 사용했던 것이다.
C++에선 언어 자체에서 C++11부터 nullptr이란 keyword를 제공해 null pointer을 표현하도록 한다.

Exception Specification
C++ community의 의견을 종합해보니 exception specification은 의도한대로 잘 작동하지 않았다.
따라서 C++11 표준에선 Exception Sepcification을 사용하지 말도록 권고한다.
하지만 standards committee에선 exception이 발생하지 않는 함수는 명시할 필요가 있다고 판단(최적화 가능+일반 Exception Sepcification에 비해 예측(의도)대로 잘 작동했겠지)했고, noexcept keyword를 추가하였다.

Scoped Enumerations

enum old1 {yes, no, maybe};
enum class new1 {never, sometimes, often, always};
enum class new2 {never, sometimes, often, always};

이렇게 아래 두놈처럼 class를 붙여주면 enumeration 상수 이름이 충돌을 안한다.
서로 다른 file에서 다른 enumeration인데 같은 이름의 enumeration 상수가 존재할 수 있다. 이때 합쳐지면 사고이기때문에 그런걸 막을때 주로 사용한다.
new1::never 식으로 사용.

Class changes
C++ 경험이 늘어나면 알겠지만, class object의 implicit type conversion은 예상치 못한 문제가 될 때가 있다.
그래서 기존엔 ctor에만 explicit keyword를 사용해 implicit type conversion을 막았지만, C++11 이후론 conversion functions에도 explicit keyword를 적용할 수 있도록 한다.

Range-based for Loop
built-in array와 begin(), end()로 범위확인 가능한 classes에 적용할 수 있다.

double prices[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
for (double x : prices)   //`auto x`라고 해도 됨
	std::cout << x << std::endl;
    
//값을 변경하고 싶은 경우 아래처럼
std::vector<int> vi(6);
for (auto & x: vi)
	x = std::rand();

기타
valarray는 원래 STL과는 독립적으로 개발되었다 그래서 valarray object는 algorithms을 사용하지 못하는데, C++11에선 valarray class에 begin(), end() 함수를 추가하여 algorithms을 사용할 수 있도록 한다.

기존엔 class template과 그 내부 함수 정의를 다른 파일로 분리할 수 있도록 export를 사용할 수 있었지만,
비현실적이여서 C++11 이후론 export 역할이 끝났다.
나중에 다시 사용할 수 있도록 keyword는 남겨두었다.

rvalue와 rvalue reference

rvalue
lvalue & rvalue
: C++에서 rvalue란 expression이 끝난 이후 더이상 존재하지 않는 임시값.
return value도 기본적으로 rvalue이지만, return by reference는 lvalue이다.
간단하게 확인하려면, lvalue는 주소연산(&)이 가능하므로 직접 해보면 된다.
참고로, C-style string은 static storage duration이고 주소값도 가지는 lvalue이다.(assign 안된다고 lvalue가 아닌게 아님)

왼쪽에 존재하는 값, 오른쪽에 존재하는 값은 반만 맞는 말이다.
lvalue도 assignment expression에서 우측에 올 수 있고,
심지어 lvalue이더라도 `const` 변수라면 좌측에 올 수 없다.

rvalue reference
기존의 reference라고 말하던건 사실 lvalue reference로, 해당 identifier을 lvalue와 bind하는 것이다.
C++11 이후론 rvalue reference도 추가됐다.
&& 기호를 사용한다.
ex)

int x=3;
int y=2;

int && r1 = x+y;
int && r2 = 13;
int && r3 = std::sqrt(2.0);

rvalue reference는 계산되는 시점에 bind된다.
따라서 xy가 나중에 바뀌더라도, r1의 값은 바뀌지 않는다.

rvalue를 rvalue reference와 bind하면, 해당 값은 주소를 얻을 수 있는 특정 공간에 저장된다.
즉, r15의 값을 가지고, &r1 같은 연산도 가능하다.

이런 rvalue reference의 주목적은 move semantics의 구현이다.

링크
bind된다고해서 static처럼 된다는건 아니다. 얘도 변수 수명에따라 좌우된다.
그래서 lvalue return 할때처럼 잘 신경써야된다.

> 고찰
링크; rvalue 내부 구현??
정확히 말하자면 rvalue가 어디 저장되는진 implemented defined이긴하지만,
결국 register아니면 memory에 저장될 것이다.
기존 rvalue에 주소연산이 안되는 등 제한이 있었던건 register에 올라갈 가능성도 있었기 때문일 것이다.
rvalue reference란 개념을 도입하며 register에 올라가있으면 메모리에 옮겨뿌고 가리킬 수 있게한게 아닐까 생각한다.
기존에 memory에 올라가던 rvalue들은 계속 사용하도록 하기면 하면 되는거고.
결론: rvalue reference에 지정되면 새로 공간이 할당되고, 거기를 지시하는줄 알았는데, 그게 아니라 (register에 있는게 아니라면) 그냥 기존 공간을 계속 사용하는 것일 뿐인듯.

Q. rvalue reference를 쓰면 성능이 좋나?
A. rvalue가 register에 있는 경우라면 좀 효율이 떨어질 수도 있긴하지만
어쨌든 rvalue가 memory에 올라가는 경우도 있으니 전체적으론 rvalue reference가 효율을 올려주는게 맞지

Move Semantics

필요성

예시를 들어보자.

vector<string> vstr;
/*
here, build up a vector of 20,000 strings, each of 1000 characters
*/
vector<string> vstr_copy1(vstr); // make vstr_copy1 a copy of vstr

vstr_copy1이 만들어질때 어떤 일이 벌어질까.
vector는 dynamic memory allocation을 사용한다.
따라서 vector의 copy constructor는 new를 이용해 새로 공간을 할당 받을 것인데, 위 경우 20,000개의 string object 공간을 할당받을 것이다.
그런데 string 또한 dynamic memory allocation을 사용한다.
즉, 20,000개의 string object 공간이 할당되고, 각 string object가 vstr에서 vstr1로 복사(혹은 assign)될때 또 다시 공간 할당이 이루어진다,, 위 경우라면 각 1000개의 character 공간이 할당된다.
소모가 큰 작업이지만, 이 경우 하는게 맞다.
하지만 다음 경우를 보자.

vector<string> allcaps(const vector<String> & vs)
{
	vector<string> temp;
    //vs를 모두 대문자로 변환하여 temp에 저장
    return temp;
}

라는 함수가 있을때
vector<string> vstr_copy2(allcaps(vstr));
이라고 호출했을때, 이때(반환될때)도 꼭 이럴 필요가 있을까?
이때도 20,000,000개의 charcater 공간을 temp에 할당받고, return을 위해 temporary object가 생성된다.
그리고 그 temporary object는 vstr_copy2에 copy된 후 삭제된다.
굳이 temporary object를 삭제하지 않고, vstr_copy2가 그냥 그 object를 소유(?)하도록 하면 된다.
(실제 compiler에선 무식하게 다 복사안하고 거의 이렇게 작동함)
이것이 Move Semantic이다. (역설적이게도.. 실제 move는 지양하고 관리정보만 조정)

Move Constructor (assigment operator)

Move Semantics을 어떻게 구현할까? 언제 Move Semantics를 사용해야할까?
바로 initialize/assignment시에 rvalue가 왔을때이다.
Ch12에서 살펴봤듯이, lvalue가 initialize/assignment에서 쓰인다면 확실하게 하나하나 복사해줘야 문제가 안생긴다.
하지만 rvalue라면 어차피 버려질 값이므로 그때는 복사를 하지않고 소유권만 조정해주면 된다.

그래서 C++에선 기존 Copy Ctor과 다르게 rvalue reference가 parameter로 오는 Move Constructor을 추가한다.
(마찬가지로 Special member function이다.)
Move Constructor를 이용해, rvalue로 initialize를 하는 경우 하나하나 복사되는게 아니라 ownership만 조정할 수 있는 것이다.
ownership 조정하는 과정에서 argument로 오는 rvalue의 값을 수정할 수 있으므로 const로 선언하면 안된다.

※ Move Constructor ex)

Useless::Useless(Useless && f): n(f.n)
{
	++ct;
    pc = f.pc;   //주소 가로채기
    f.pc = nullptr;
    f.n = 0;
}

copy ctor과 다른 점은, 내부에서 copy하는 것이 아니라 소유권을 가로채는 것이라 argument가 수정될 수 있어서 (1)const로 선언하면 안되고,
내부적으로 deep copy를 하는 것이 아니라 값 복사하고 (2)heap으로 할당된 공간을 가로채와야 한다.
(3)가로채고 나서는 기존 pointer는 nullptr로 만들어서 delete시에 오류가 발생하지 않도록 한다.
(즉, ★소유권을 옮겨오고, 상대방 소유권을 아예 없애버려야함. 한 포인터만 데이터를 가리키는 것이 중요)
(이런 소유권 도둑질을 "pilfering"이라고 함.)

g++4.5.0으로 compile하면 아예 move ctor도 호출이 안된다.
즉 compiler 차원에서 곧바로 최적화시키는 것이다.(결과만 같으면 그럴 권한이 있음)
move ctor을 빼고 compile해도 같은 결과가 나온다.
물론 그런 compiler이더라도 내 의도를 정확히 기술해주는 것(move ctor 작성해주는 것)이 좋다.

move ctor이 없으면 rvalue는 어떻게 처리했지?
기본 copy ctor이 const class_name & argument를 가진다.
Ch8에서 봤듯이, constant reference는 rvalue라면 temporary object를 만든다.
마찬가지로 move ctor이 없으면 그렇게 처리한다.
(move들이 special member 함수이긴하지만, 밑에 보면 자동생성되지않는 경우도 있음)

※ Move Assigment operator ex)

Useless & Useless::operator=(Useless && f)
{
	if (this == &if)
    	return *this;
    delete [] pc;
    n = f.n;
    pc = f.pc;
    f.n = 0;
    f.pc = nullptr;   //nullptr로 설정, rvalue 사라질때 복사된 값 삭제되면 안됨
    return *this;
}

같은 논리로 Move Assginment operator도 있다.(마찬가지로 special member function)
기존 assigment opeator와 다른 점은, 얘도 deep copy가 아닌 가로채오는 역할이란 것이다.
그래서 본인인지 check하는 것이나 기존 값 delete하는 것이나 reference 반환하는건 같지만, 값을 옮겨오는 과정이 좀 다르다.
move ctor에서 한 것처럼 우선 pointer 값을 가로채온 후, rvalue는 곧바로 삭제되기때문에 dtor에서 delete할때 오류가 생기지 않도록 포인터는 nullptr로 설정해줘야한다.

Forcing a Move

lvalue인데 move를 사용하고 싶다면?
(특정 객체를 lvalue로 만들긴했는데 특정 순간 이후로 안쓰는 경우도 있음)

1. static_cast<>를 이용해 rvalue reference로 변경할 수 있다.
ex. four = static_cast<Useless &&>(one);

2. utility header에 있는 std::move()
ex. four = std::move(one);
(그냥 four = one; 을하면 move assign이 아니라 일반 assign이 됨)

std::move() 함수가 move 기능을 해주는게 아니다. "그냥 rvalue로 바꿔주는 역할일 뿐"이다.
move assignment operator가 없다면,
std::move()를 써서 rvalue로 바꾸면 그냥 기존 operator= 쓰는 것일 뿐이다.
(기존 oeprator=도 rvalue 기본으로 받아들일 수 있음)

참고)
STL classes는 Copy Ctor, Move Ctor, Copy assignment op, Move assignment op 다 가짐.

move ctor/assignment 언제 호출되나?


New Class Features

Special Member Functions

1) Default ctor ("아무 ctor"도 없어야 만들어짐)
2) Dtor
3) Copy ctor
4) Copy assignment op
5) Move ctor : ex. Someclass::Someclass(Someclass &&);
6) Move assignment op : ex. Someclass& Someclass::oprator=(Someclass &&);

모두 기본으로 만들어지지만...
★예외★)
1. dtor/copy ctor/copy assignment op 중 하나를 제공해주면, move ctor나 move assignment op는 자동 생성되지 않는다.
2. move ctor/move assignment op 중 하나를 제공하면, copy ctor나 copy assignment op는 자동 생성되지 않는다.

간단하게 move진영의 애들이 선언되면 반대편은 안만들어지고, 역도 성립

기본 move ctor/move assignment op도 기본 copy ctor/copy assignment op랑 비슷하게 행동한다.
그냥 값들 copy됨. 그걸 원하지 않으면 지금까지 해왔듯이 따로 짜줘야되고.

default keyword

move ctor을 제공했다고 해보자.
default ctor, copy ctor, copy assignment op가 전부 자동으로 안만들어진다.
이런 경우 default keyword를 이용해 이런 함수의 기본 버전을 만들 수 있다.

class Someclass
{
public:
	Someclass(Someclass &&);
    Someclass() = default;
    Someclass(const Someclass &) = default;
    Someclass& oprator=(const Someclass &) = default;
};
당연히 special member functions에만 적용할 수 있다.

delete keyword

객체를 보호하기위해 copy를 막는다고 해보자.
1. Ch12에서 봤듯이, private에 copy 관련 함수들을 몰아버릴 수 있다.
2. delete keyword를 이용해 사용을 막을 수 있다.(자동생성돼도 사용안됨)

class Someclass
{
public:
	...
    Someclass(const Someclass &) = delete;
    Someclass& oprator=(const Someclass &) = delete;
    ...
}

private보다 눈에띄고 확실한 방법임.

default와 다르게 delete는 아무 member 함수에나 적용할 수 있다.
한 예시로, 특정 형변환을 막는데 사용할 수 있다.

class Someclass
{
public:
	void redo(double);
    void redo(int) = delete;
}

이제 sc.redo(5);라고 하게되면, int형 함수를 찾아가고 delete된 함수이기때문에 compile time error가 발생한다.
(double 버전만 있다면 변환돼서 잘 작동했을 것)

Delegating Constructors

Class 내에 constructor가 많다면, 전부 코드가 비슷비슷하다.
그래서 class내의 한 ctor이 다른 ctor을 이용할 수 있도록 한다.

Notes::Notes(int kk, double xx, std::string stt) : k(kk), x(xx), st(stt){ ~ }

Notes::Notes() : Notes(0, 0.01, "Oh"){ ~ }

Notes::Notes(int kk) : Notes(kk, 0.01, "Ah"){ ~ }

Notes::Notes(int kk, double xx) : Notes(kk, xx, "Uh"){ ~ }

아래 세 ctor이 처음 ctor을 사용한다.

Inheriting Constructors

Inheriting Constructors란.
using을 이용해 base class의 ctor들을 derived class에서도 사용하는 것이다.(원래 ctors은 상속이 안되니..)

근데 쓸일이 있을진 모르겠다.
base의 ctor 써버리면 derived의 멤버는 초기화가 안되는데,
좀 귀찮아도 derived class에 추가된 멤버도 다 초기화 해줘야되지않나..
나중에 쓸일이 생기면, 혹은 코드 어딘가에서 보면 그때 다시 자세히 보던가.. 하는걸로
일단 "using으로 base의 member 함수를 scope내로 가져왔듯이, ctor도 가져올 수 있다"정도만 알자..

> 번외

namespace A{
    void test1(void) {cout<<"A::test1()!\n"; }
}
namespace B{
    //using A::test1;    //활성화하면 충돌함
    void test1(void) {cout<<"B::test1()!\n"; }
}

기본적으로 namespace에선 당연히 위처럼 하면 이름이 충돌하지만,

class C1
{
public:
    void fn(double a) {cout<<"C1::fn(double)!\n";}
};
class C2 :public C1
{
public:
    using C1::fn;
    void fn(double a) {cout<<"C2::fn(double)!\n";}
};

이런식으로 상속관계인 class에선 using 이용해 base class에서 같은 이름의 함수를 가져와도 충돌하지 않고,
derived의 함수가 override하게 된다.

overridefinal

> override
Ch13에서 봤듯이 class inheritance시에 함수를 override(redefine) 할 수 있는데,
이때 함수 prototype을 맞춰주지 않으면 문제가 될 수 있다는걸 봤다.
그걸 강제하게 해주는게 override specifier이다.
override하는 함수라면 끝에 override를 붙여준다.
base version과 derived version의 prototype 형태가 일치하지 않는다면 compiler가 거부한다.
ex) virtual void f(char * ch) const override { ~ }
: base class의 함수 f와 prototype 일치하지 않으면 오류

> final
final specifier을 함수 끝에 붙이면 그 밑으로 더이상 override가 금지된다.
ex) virtual void f(char ch) const final { ~ }
: 이 함수를 포함하는 class를 derived한 class에선 함수 f를 redefine할 수 없다.

둘 다 keyword가 아니므로 변수 이름 등에 사용될 수 있다.

Lambda Functions

문제: 랜덤으로 만들어진 수열에서 3, 13 나누어 떨어지는 수의 개수를 구하라.

std::vector<int> numbers(50);
std::generate(numbers.begin(), numbers.end(), std::rand);

std::count_if를 사용해서 count할건데, 얘의 3번째 인자를 3가지 방식으로 넘겨줄 수 있다.

1) 일반 함수

bool f3(int x) {return x % 3 == 0;}
bool f13(int x) {return x % 13 == 0;}

int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
int count13 = std::count_if(numbers.begin(), numbers.end(), f13);

2) functor

class f_mod
{
private:
	int dv;
public:
	f_mod(int d = 1) : dv(d) {}
	bool operator()(int x) {return x % dv == 0;}
};

int count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));
int count13 = std::count_if(numbers.begin(), numbers.end(), f_mod(13));

3) Lambda
anonymous function을 만들도록 해준다.
이를 이용해 functor를 인자로 받는 곳에 넘겨줄 수 있다.
[](int x) {return x % 3 == 0;} (위의 f3 함수와 같다.)
return type과 함수이름이 []로 대체됐다.

return type은 return expression에 `decltype`이 유추하듯 유추해서 정해진다.
즉, return문 보고 알아서 추측한다.

return statement가 하나여야만 적용된다.
return statement가 두개이상이거나 다른 return type을 명시하고싶다면,
trailing return syntax를 사용해야한다.
ex)`[](double x)->double{int y = x; return x – y;}`

int count3 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;});
int count13 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x % 13 == 0;});

왜 lambda?

code의 읿부분을 재활용할때, 그 정의는 최대한 가까이 있는게 보기 편하고 효율적이다.
일반 함수는 애초에 다른 함수 내에 정의될 수 없으니 그런 측면에선 최악이고, functor는 그나마 낫다. lambda는 그런 면에선 최고인 것이다.

lambda는 비슷한(or 같은) 코드여도 여러번 적어야 한다는게 일반함수에비해 단점이 될 수 있는데,
lambda에 이름을 줘서 반복될때 똑같은 코드를 안써도 되게 할 수도 있다.
auto mod3 = [](int x){return x%3 == 0;}
(이름 지정도되고 다른 함수 내에서 정의돼서 접근성도 좋으니 간단하게 쓸땐 일반함수보다 좋지 ㅇㅇ)

또 함수는 inline이라고 명시하지 않으면 inline으로 만들지 않는다는 것도 효율성 측면에서 불리하다.
(나머지 둘은 자동으로 inline으로 되기도 한단 소린가? 쨌든 일반함수보단 효율적인건 맞겠지)

> 추가기능
[] 를 활용해서 같은 scope내의 변수들에 접근할 수 있다.
[] 안에 같은 scope의 다른 automatic variable의 identifier을 넣으면 된다.
예를들어 [z] 이렇게 넣으면 lambda 함수 내에서 z 값에 접근할 수 있다.
단, 변수명만 적으면 copy로 접근하는 것이고, [&z]라고 적어야 값 자체에 접근할 수 있는 것이다.
[a,&b] 처럼 여러개가 와도 된다.
[=]라고하면 value로 모든 automatic variable에 접근할 수 있고, [&]라고하면 ditto.
ex)

int count13;
std::for_each(numbers.begin(), numbers.end(),
		      [&count13]{count13 += x % 13 == 0;});
int count3, count13;
std::for_each(numbers.begin(), numbers.end(),
		      [&]{count3 += x % 3 == 0; count13 += x % 13 == 0;});

Wrapper

binder1st, binder2nd 같은 adapater(wrapper)을 통해 더 통일되고 적절한 interface를 제공할 수 있다.

function wrapper

`binder1st` 처럼 adapter(wrapper) 역할을 한다.

특정 기능을 하는 함수 template있다고 해보자.
이 함수는 functor을 하나의 인자로 받는다.(template parameter 이용)
이때 이 functor로 넘어갈 수 있는 종류는 꽤 많다.
같은 double (double) 같은 signature을 가지더라도, 일반함수,functor,lambda 같은 애들이 올 수 있다.
(여기선 signature을 argument list가 아니라 return type까지로 확장해서 말함)

여기서 문제가 같은 signature이라도 종류가 다르면 template이 다른 type으로 인식하고 instantiation이 다르게 만들어 진다는 것이다.
signature이 같아서 한 signature만 있어도 되는데, 여러 instantiation을 만들면 코드 양도 늘고 시간도 늘고 비효율 적이다.

이럴때 function wrapper을 사용해서 다른 종류의 functor을 하나의 유형으로 탈바꿈 시키는 것이다.
(자세한 문제상황예시/설명/ function사용법은 p.1191부터.. 금방읽음ㅇㅇ)

쉽게말해서 function template에 functor 인자가 있을때,
functor 종류에따라 instantiation이 계속 만들어질 수 있는데,
그럴 필요가 없는 경우 하나의 양식으로 통일해줘서 instantiation을 하나만 만들도록 해준다.

Variadic Templates

쓸일 많을라나 모르겠네 일단 건너뜀 ㅋㅋ 나중에 필요해지면 다시 보자
지금 고생해서 정리해도 안쓰면 까먹어서그럼, 일단 어떤 내용인지 읽어보기만함

또 다른 C++11 기능...

Concurrent Programming

요즘은 프로세서 속도를 높이는 것 보단 프로세서를 추가하는게 더 쉽다. 컴퓨터 코어가 증가하며 multiple threads가 가능해졌다.
이를 위한 각종 라이브러리나 메모리 모델을 C++11에서 지원한다.

멀티 쓰레드가 무조건 도움이 되는건 아니다. single linked list에서 특정 값을 찾는다고 해보자.
처음부터 끝까지 순차적으로 훑어야한다. 나머지 쓰레드들이 도울만한 것이 딱히 없다.

Library Additions

random, chrono(시간측정), tuple, ratio(compile-time rational arithmetic)이 추가됐다.
그리고 regex header file도 추가돼 정규표현식을 지원한다.
특정 패턴을 나타내려면 \d 같은 표현을 써야하는데, \를 작성하려면 \\라고 해야하니 간단하게 쓸 방법이 필요했는데 그게 raw string(ch4)이 추가된 이유 중 하나이다.

Low-Level Programming

여기서 low level은 프로그램의 질이 아닌 추상화 레벨이다. low level 접근성은 임베디드 프로그래밍 등 효율성 증가를 위해 중요하다.

우선 POD(Plain Old Data)의 제한을 어느 정도
해제했다.
POD는 byte 단위를 copy하는데 안전한 type이어야한다는 아이디어는 변함없지만, POD가 될 수 있는 type을 좀 늘려서 POD가 필요한 low level operations을 더 지원한다.

union도 ctor과 dtor을 가질 수 있도록 하였다.

alignas specifier과 alignof() operator을 이용해 memory alignment를 다룰 수 있도록 하였다.

constexpr을 통해 compiler가 compile time에 상수값 계산하는 능력을 확장시켰다.
low level 측면에서 보면 해당 값을 read-only memory에 저장할 수 있도록 한다.(embeded 프로그래밍시 유용)


언어 발전

표준 위원회도 있지만, C++ 커뮤니티의 영향력도 무시할 순 없다.
Library project 같은게 표준에 영향을 미치기도 하고, 검증역할도 한다.
실제로 표준과 호환은 되지만 포함되진 않는 것들을 Technical Report 1 (TR1) 이라는 하위 프로젝트에 넣어두는데, 이는 다음 표준의 후보이다. 이런 것들이 다음 표준이 나오기 전까지 사용되며 검증되고 발전하는 것이다.

Boost project

1998년에 시작된 C++ library 개발 프로젝트이다. www.boost.org에서 문서와 파일을 다운 받을 수 있다.
다양한 기능 지원함. 현재 100개이상의 라이브러리(글쓰는 시점에서 100개이상이니 지금은 더...?)


다음 공부 방향

이런 것도 알려주네 ㅋㅋ

부록 H에 읽으면 좋은 책들이 정리돼있다.

OOP aproach에서 꼭 해봐야하는건 내가 모델링한 상황을 표현하는 classes를 만들어보는 것이다.
실제 상황은 복잡해서 뜻대로 안될때가 많다.
그럼 계속 반복하며 다시 분석하고 디자인하고 해야한다.(일반 코드 쓰고쓰고하는 것보단 분석/디자인 많이 해보라고하네)
analysis하는데 좋은 방법은 (1)use-case analysis와 (2)CRC card를 이용하는 것이다.

큰 규모에서는 체계적인 방법이 필요한데, 최근(요즘도그른가?)엔 Unified Modeling Language (UML)을 많이 쓴다.
프로그래밍 언어라기보단 프로젝트를 분석하고 디자인하는 것을 표현하는 언어이다.

UML은 3개의 modeling languages인 the Booch Method, OMT (Object Modeling Technique),
and OOSE (Object-Oriented Software Engineering)를 개발한
Grady Booch, Jim Rumbaugh,and Ivar Jacobson가 만든 언어이다.
UML은 저 모델링 언어들의 후계이다.

Chapter Review

vector<int> a1(10);vector<int> a1{10};는 다르다.
vector<> class template엔 initializer_list가 정의돼있기 때문이다.
이렇게 initializer_list로 정의한 ctor이 있다면 기본으로 있는 brace initializer 형태가 아니므로 주의해야한다.

`int a = 10;`는 `int a{10};`이나 `int a={10};`이나 `int a(10);`이나 같다.
이때는 기존의 brace 이용한 초기화로 인식되는 것이다.

rvalue reference는 lvalue를 인자로 받을 수 없다.
("rvalue"니까 당연하다고 보자. lvalue 일반 변수는 rvalue를 받는 경우도 있으니 좀 달라서 헷갈려서 적어둠.)

함수가 overload돼있으면 더 적합한 곳을 찾아간다.
예를들어 rvalue reference parameter와 const lvalue reference parameter가 둘 다 있다면,
그냥 3+5 같은 rvalue는 둘 다 들어갈 수 있지만 전자를 선호한다.

move semantics는 pointer로 힙에 메모리를 할당받는 멤버가 있어야 말이 된다.
ownership을 옮기는 것인데, 일반 array나 built-in type들은 소유권을 옮기는 방법이 애초에 없다.
그러니 적어도 멤버중에 포인터가 있어야 move ctor이 의미가 있다.


<후기>
C++을 시작하기에 좋은 책인 것 같다. C로부터 어떻게 발전했는지, 어떤 철학으로 쓰인 언어인지, 많은 예시와 자세한 설명으로 이해하기 수월했다.(대신 그만큼 두껍다.. 근데 C++ 내용 대부분 cover하는 기초책 중에선 오히려 1000쪽 안넘는게 찾기 힘들더라)
처음에 두께에 놀랐지만, 그만큼 C에서 추가된 기능이 많았고, 익숙해지면 충분히 편리한 기능들이었다.
처음에 원서로 보다가 Chapter16부터 슬슬 지루해져서 학교 도서관에 번역된 책이 있길래 빌려서 같이 봤다.
그런데.. 음 역자께서 경력이 화려하시긴하지만 전공자가 아니여서 그런지 번역하면 안될 것 같은 단어도 전부 한글화(function wrapper가 "함수 래퍼"로 번역돼있다던가)가 돼있고,, 번역하며 생긴 오타도 있고, 이상한 부분이 좀 있었다. 되도록 원서로 보거나, 아니면 한글판으로 빠르게 맥락 파악하고 세부사항은 원서로 보는걸 추천한다.
이제 혼자 STL 공부 좀 해보고, effective series를 볼까 한다. 가자~~

0개의 댓글