본문 바로가기
프로그래밍/Effective C++

Chapter 5 구현

by Ohdumak 2018. 5. 23.

2018/04/17 - [프로그래밍/Effective C++] - Chapter 4 설계 및 선언

구현 시 발생할 수 있는 여러가지 문제를 어떻게 조심해야 할까?


항목 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자


생성자 혹은 소멸자를 끌고 다니는 타입으로 변수를 정의하면 반드시 물게 되는 비용이 두개 있다. 하나는 프로그램 제어 흐름이 변수의 정의에 닿을 때 생성자가 호출되는 비용이고, 또 하나는 변수가 유효범위를 벗어날 때 소멸자가 호출되는 비용이다. 

어떤 변수를 사용해야 할 때가 오기 전까지 그 변수의 정의를 늦추는 것은 기본이고, 초기화 인자를 손에 넣기 전까지 정의를 늦출 수 있는지도 둘러봐야 한다.

어떤 변수가 루프 안에서만 쓰이는 경우라면, 해당 변수를 루프 바깥에서 미리 정의해 놓고 루프 안에서 대입하는 방법이 좋을까, 아니면 루프 안에 변수를 정의하는 방법이 좋을까?

'대입이 생성자-소멸자 쌍보다 비용이 덜 들고, 전체 코드에서 수행 성능에 민감한 부분을 건드리는 중'이라고 생각하지 않으면, 루프 안에 변수를 정의하는 방법으로 가는 것이 좋다.


이것만은 잊지 말자!

- 변수 정의는 늦출 수 있을 때까지 늦춥시다. 프로그램이 더 깔끔해지며 효율도 좋아진다.



항목 27: 캐스팅은 절약, 또 절약! 잊지 말자


"어떤 일이 있어도 타입 에러가 생기지 않도록 보장한다." C++의 동작 규칙은 바로 이 철학을 바탕으로 설계되어 있다.

C++에서 캐스팅은 정말로 조심해서 써야 하는 기능이다.

캐스팅 문법 정리

C 스타일 캐스트

(T) 표현식                        // 표현식 부분을 T 타입으로 캐스팅한다.

함수 방식 캐스트

T(표현식)                         // 표현식 부분을 T 타입으로 캐스팅한다.

이 두 형태를 통틀어 '구형 스타일의 캐스트'라고 부른다.


C++는 네 가지로 이루어진 새로운 형태의 캐스트 연산자를 독자적으로 제공한다. (신형 스타일의 캐스트 혹은 C++ 스타일의 캐스트라고 한다.)

const_cast<T> (표현식)

dynamic_cast<T> (표현식)

reinterpret_cast<T> (표현식)

static_cast<T> (표현식)


- const_cast 객체의 상수성(constness)을 없애는 용도로 사용된다.

- dynamic_cast '안전한 다운캐스팅(safe downcasting)'을 할 때 사용하는 연산자이다. 즉, 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 아닌지를 결정하는 작업에 쓰인다.

- reinterpret_cast 포인터를 int로 바꾸는 등의 하부 수준 캐스팅을 위해 만들어진 연산자. 이런 캐스트는 하부 수준 코드 외에는 거의 없어야 한다.

- static_cast 암시적 변환[비상수 객체를 상수 객체로 바꾸거나, int를 double로 바꾸는 등의 변환]을 강제로 진행할 때 사용한다.


구형 스타일의 캐스트는 요즘도 여전히 적법하게 쓰일 수 있지만, 그보다는 C++ 스타일의 캐스트를 쓰는 것이 바람직하다. 


캐스팅은 그냥 어떤 타입을 다른 타입으로 처리하라고 컴파일러에게 알려 주는 것밖에 더 있느냐고 생각하는 프로그래머가 의외로 많다. 크나큰 오해이다.


캐스팅을 해야하는 코드를 내부 함수 속에 몰아 놓고, 그 안에서 일어나는 '천한' 일들은 이 함수를 호출하는 외부에서 알 수 없돌고 인터페이스로 막아두는 식으로 해결하면 됩니다.


이것만은 잊지 말자!

- 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast 는 몇 번이고 다시 생각하자. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해 보자.

- 캐스팅이 어떨 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해보자. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.

- 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세하게 드러난다.



항목 28: 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자


참조자, 포인터 및 반복자는 어쨌든 모두 핸들(handle, 다른 객체에 손을 댈 수 있게 하는 매개자)이고, 어떤 객체의 내부요소에 대한 핸들을 반환하게 만들면 언제든지 그 객체의 캡슐화를 무너뜨리는 위험이 있다.


그렇다고 해서 핸들을 반환하는 멤버 함수를 절대로 두지 말라는 이야기가 아니다.


이것만은 잊지 말자!

- 어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하자. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화할 수 있다.



항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!


예외 안전성(exception safety)을 확보하려면 두 가지의 요구사항을 맞춰야 한다.

- 자원이 새도록 만들지 않는다.

- 자료구조가 더럽혀지는 것을 허용하지 않는다.


Lock 등의 자원관리 전담 클래스를 쓰면 가장 좋은 점 중 하나는 함수의 코드 길이가 짧아진다. 


예외 안정성을 갖춘 함수는 아래의 세 가지 보장(guarantee) 중 하나를 제공한다.

- 기본적인 보장(basic guarantee) 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장

- 강력한 보장(strong guarantee) 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장.

- 예외불가 보장(nothrow guarantee) 예외를 절대로 던지지 않겠다는 보장.


예외에 속수무책인 함수를 탈부꿈시켜 강력한 예외 안전성 보장을 제공하는 함수로 거듭나게 만드는 일반적인 설계 전략을 하나 알아보자. 이 전략은 '복사-후-맞바꾸기(copy-and-swap)'라는 이름으로 알려져 있다. 원리는 어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정하는 것이다.


'복사-후-맞바꾸기' 전략은 객체의 상태를 '전부 바꾸거나 혹은 안 바꾸거나(all-or-nothing)'방식으로 유지하려는 경우 좋다.


새로운 함수를 만들거나 기존의 코드를 고칠 때 '어떻게 하면 예외에 안전한 코드를 만들까'를 진지하게 고민하는 버릇을 들여야한다.


이것만은 잊지 말자!

- 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내벼러 두지 않는다. 이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.

- 강력한 예외 안전성 보장은 '복사-후-맞바꾸기' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.



항목 30: 인라인 함수는 미주알고주알 따져서 이해해 두자


인라인 함수는 함수처럼 보이고 함수처럼 동작하는데다가, 매크로보다 훨씬 안전하고 쓰기 좋다.


인라인 함수는 대체적으로 헤더 파일에 들어 있어야 하는 게 맞다. 왜냐하면 대부분의 빌드 환경에서 인라인을 컴파일 도중에 수행하기 때문이다.


인라인 함수가 실제로 인라인되느냐 안 되느냐의 여부는 전적으로 개발자가 사용하는 빌드 환경에 달렸다.


대부분의 디버거가 무척이나 곤란해 하는 비호감 대상이 바로 인라인 함수다.

우선, 아무것도 인라인하지 말아라. 아니면 꼭 인라인해야 하는 함수 혹은 정말 단순한 함수에 한해서만 인라인 함수로 선언하는 것으로 시작하자.


이것만은 잊지 말자!
- 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 묶어두자. 이렇게하면 디버깅 및 라이브러리의 바이너리 업그레이드가 용이해지고, 자칫 생길 수 있는 코드 부풀림 혐상이 최소화되며, 프로그램의 속력이 더 빨라질 수 있는 여지가 최고로 많아진다.

- 함수 템플릿이 대게 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안 된다.



항목 31: 파일 사이의 컴파일 의존성을 최대로 줄이자


인터페이스와 구현을 둘로 나우는 열쇠는 '정의부에 대한 의존성(dependencies on definitions)'을 '선언부에 대한 의존성(dependencies on declarations)'으로 바꾸어 놓는 데 있다. 이게 바로 커파일 의존성을 최소화하는 핵심 원리이다.

- 객체 참조자 및 포인터로 충분한 경에는 객체를 직접 쓰지 않는다.

- 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다.

- 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다.


파생 클래스의 생성자 역할을 대신하는 어떤 함수를 만들어 놓고 이것을 호출함으로써 해결한다. 이런 함수를 가리켜 팩토리 함수 혹은 가상 생성자(virtual constructor)라고 한다.


인터페이스 클래스를 구현하는 용도로 가장 많이 쓰이는 메커니즘이 두 가지 있는데, 인터페이스 클래스(Person)로부터 인터페이스 명세를 물려받게 만든 후에, 그 인터페이스에 들어 있는 함수(가상 함수)를 구현하는 것이라고 말할 수 있다. 한편 인터페이스 클래스를 구현하는 두 번째 방법은 다중 상속을 사용하는 것이다.


이것만은 잊지 말자!

- 컴파일 의존성을 최소화하는 작업의 배경이 되는 가장 기본적인 아이디어는 '정의' 대신에 '선언'에 읜존하게 만들자는 것이다. 이 아이디어를 기반한 두 가지 접근 방법은 핸들 클래스와 인터페이스 클래스이다.

- 라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며 선언부만 갖고 있는 형태여야 한다. 이 규칙은 템플릿이 쓰이거나 쓰이지 않거나 동일하게 적용하자.

728x90

댓글