프로그래밍 이야기

A Tour of C++ : 5장 필수적인 연산

원생계 2019. 10. 20. 02:57

.

.

책 읽으면서 정리한 메모


5. 필수적인 연산

소개 : 필수적인 연산, 변환, 멤버 초기화

복사와 이동 : 컨테이너 복사, 컨테이너 이동

자원 관리

관례적인 연산 : 비교, 컨테이너 연산, 입력과 출력 연산, 사용자 정의 리터럴, swap(), hash<>

5.1 소개

== 와 << 등의 연산은 관례적인 의미가 있다.

5.1.1 필수적인 연산

class X {

public:

X(Sometype); // “일반적인 생성자” : 객체 생성

X(); // 기본 생성자

X(const &X); // 복사 생성자

X(X&&); // 이동 생성자

X& operator=(const X&); // 복사 대입 : 대상을 정리하고 복사

X& operator=(X&&); // 이동 대입 : 대상을 정리하고 이동

~X(); // 소멸자 : 정리 작업

}

객체가 복사되거나 이동되는 경우

- 대입 연산의 원본

- 객체 초깃값

- 함수 인자

- 함수 반환값

- 예외

클래스가 클래스 멤버를 포함한다면 복사, 이동 연산자를 명시적으로 정의하는 것이 좋다. delete 할 무언가를 포인터가 가리킨다면 기본 멤버별 복사는 적합하지 않기 때문. 반대로 delete 하면 안 되는 것을 포인터가 가리키는 경우에도 코드에 명시해야한다. 5.2.1 절에 예제.

일반적으로는 필수적인 함수를 모두 정의하거나, 아무것도 명시적으로 정의하지 않는 것이 좋음.(모두 default 함수를 사용하게)

class Y {

public:

Y(Sometype);

Y(const Y&) = default; // 기본 복사 생성자

Y(Y&&) = default; // 기본 이동 생성자

}

=default 를 보완하는 용도로 =delete 를 이용하면 해당 연산이 생성되지 않음. 멤버별 복사가 적합하지 않은 예로 클래스 계층 구조의 기반 클래스.

class Shape {

public:

Shape(const Shape&) =delete;

Shape& operator=(const Shape&) =delete;

}

void copy(Shape& s1, const Shape& s2)

{

s1 = s2;

)

컴파일 에러. 필수 멤버 함수가 아니어도 =delete 를 쓸 수 있다.

5.1.2 변환

생성자의 인지가 하나뿐인 때, 변환 연산을 정의한다.

complex z1 = 3.14; // double 인자 생성자를 제공해서 초기화됨.

vector v1 = 7;

이것도 가능하지만, 의도하지 않은 것. 표준 vector 도 허용하지 않음. 명시적인 변환만 허용하게끔 할 수 있다.

class Vector {

explicit Vector(int s );

}

Vector v1(7); // 이건 허용. 명시적 생성자 호출.

Vector v2 = 7; // 이건 에러

즉, 특별한 이유가 없는 한, 인자가 하나인 생성자는 explicit 을 선언한다.

5.2 멤버 초깃값

간단한 구체 타입 객체면 단순히 멤버별 복사가 이루어지고, 적합한 경우도 있지만, Vector 처럼 정교한 클래스는 멤버 복사가 적합하지 않음.

5.2.1 컨테이너 복사

자원 핸들 역할 클래스, 포인터로 접근되는 객체 책임 객체는 곤란. 복사받은 Vector 가 제거될 때 요소들의 소멸자도 불릴 수 있기 때문에.

5.2.2 컨테이너 이동

복사하는 게 아니라 이동해야 한다.

class Vector {

Vector(const Vector& a);

Vector& operator=(const Vector& a );

Vector(Vector&& a); // 이동 생성자

Vector& operator=(Vector&& a ); // 이동 대입

}

r = x+y+z

&&는 rvalue 참조’ 즉 rvalue가 될 수 있는 객체의 참조를 말한다. lvalue 는 대입의 왼쪽에 올 수 있는 무언가를 말한다. rvalue는 대입의 원본이 되는 것. 예, 함수가 반환한 정수. Vector operator+() 의 지역변수 res 도 rvalue의 한 예.

이동 생성자가 const 인자를 받지 않는 건, 결국 인자의 값을 제거하기 때문. 이동 대입도 마찬가지. rvalue 참조를 초깃값으로 사용하거나 대입 연산의 오른쪽에 사용했을 때 이동 연산이 수행.

객체 초기화 시 컴파일러가 대부분의 복사를 방지. 생각만큼 이동 호출자가 자주 호출될 일은 없다.

5.3 자원관리

생성자, 복사 연산, 이동 연산, 소멸자를 정의함으로써 프로그래머는 객체의 자원 생애 주기를 완벽히 제어.

Vector, thread 같이 자원이 많으면 내장 포인터 쓰는 것보다 낫다. unique_ptr 을 비롯, ‘스마트 포인터’는 그 자체로 자원 핸들.

강력한 자원 안정성.자원 누수 방지. 가비지 컬렉터를 활용하지만, 명확하고 일반적, 지역화될 수 있는 자원 관리 방법이 더 이상 없을 때 고려하자.

메모리 이외의 자원이 비메모리 자원. 훌륜한 자원 관리 시스템은 모든 종류 자원을 관리할 수 있어야.

가비지 컬렉션을 고려하기 전에 자원 핸들을 체계적으로 사용해, 자원 소유자가 스코프 안에 존재하고, 스코프 끝날 때 해제하자. C++ 에선 이를 RAII(Resource acquisition is initialization). 이동 연산, 스마트 포인터로 스코프 사이에서 자원을 이동시킬 수 있다.

C++ 표준 라이브러리는 RAII 내장. 코드에서 잘 드러나지 않으면서도 자원 소유 시간을 줄일 수 있는 암죽적 자원 관리 가능.

5.4 관례적인 연산

관례적 연산자들은 관례를 따르게 해야 한다.

- 비교 : ==, !=, <, <=, >, >=

- 컨테이너 연산 : size(), begin(), end()

- 입력과 출력 연산 : >>, <<

- 사용자 정의 리터럴

- swap()

- 해시 함수 : hash<>

== 를 정의할 때는 != 도 정의. a != b 는 !(a==b) 를 의미해야.

마찬가지로 <를 정의할 때는 <=, >, >= 정의해야

5.4.2 컨테이너 연산

컨테이너 설계는 표준 라이브러리 스타일 따라야. 필수 연산을 포함한 자원 핸들로 구현하여 자원을 안전하게 관리할 수 있음.

탐색은 인덱스 대신 표준 알고리즘의 반복자(iterator)로 정의되는 시퀀스 개념 사용.

c.begin(), c.end() // end() 는 마지막 요소 다음을 가리킴.

for (auto& x : c) // 암묵적으로 .begin(), .end() 를 사용.

x = 0;

5.4.3 입력과 출력 연산

정수 쌍에 대해서는 <<, >> 가 시프트. iosteams 에선 출력/입력 의미.

5.4.4 사용자 정의 리터럴

User-Defined Literal

Literal Operators 를 이용해서 정의 가능.

표준 라이브러리의 리터럴 접미사

chrono_literals h,min,s,ms,us,ns

string_literals s

string_literals sv

complex_literals i,il,if

“Surprise!”s -> std::string

123s -> second

12.7i -> imaginary. 12.7i+47 = complex. (47,12.7)

5.4.5 swap()

두 객체의 값을 뒤바꾸는 swap() 함수. 굉장히 빠르고 예외를 던지지 않는다고 가정. tmp=a, a=b, b=tmp 로 정의.

5.4.6 hash<>

표준 라이브러리 unordered_map<K,V> 해시테이블.

5.6 조언

[3] 필수 연산자를 정의할 거면 모두 하거나, 아무것도 하지 않거나.

[5] 포인터 멤버 포함하는 클래스는 사용자 정의 혹은 제거된 소멸자와 복사, 이동이 필요.

[7] 인자가 하나인 생성자는 기본적으로 explicit으로 선언.

[9] 기본 복사가 적합하지 않은 클래스는 재정의하거나 막아라. (=delete)

[11] 큰 연산 항에는 const 참조를 인자 타입으로 사용

[14] 연산자를 오버로드해 관례적인 사용법을 흉내 내자.

.

.

.

728x90
반응형