배고픈 개발자 이야기

[Effective Modern C++] 0.소개 본문

언어/Effective Mordern C++

[Effective Modern C++] 0.소개

이융희 2019. 10. 7. 18:16
728x90

<소개>

 

C++11을 처음 접하고, 뭐 좀 바뀌긴 했지만 그래도 C++이겠지 라고 생각했을 것이나,

배울수록 변화의 폭에 놀랐을 것이다.

auto선언, 범위 기반 for 루프, 람다 표현식, 오른값 참조는 C++의 겉모습을 바꾸었다.

새로운 동시성 기능들 역시 마찬가지이다. 게다가 관용구적인 변화도 있다. 0typedef이 물러나고, nullptr과 별칭 선언이 등장했다. 이제는 열거형에 범위가 적용되며, 내장 포인터보다 똑똑한 포인터가 선호된다. 대체로 객체를 이동하는 것이 복사하는 것 보다 낫다.

 

C++14는 말할 것도 없이, C++11만으로도 새 기능을 효과적으로 사용하려면 배울 것이 많다.

정확하고, 효율적이며 유지보수하기 좋은, 그리고 이식성 있는 소프트웨어를 만들기 위해 기능을 어떻게 사용해야 하는지에 관한 지침에 대해 기술한다.

 

이 책은 기능 자체가 아니라 기능들의 효과적인 적용방법에 대해 서술한다.

다양한 형태의 형식 유추에 정통하고 싶은가?

auto 선언을 사용해야 할 때와 사용하지 말아야 할 때를 알고 싶은가?

const 멤버 함수가 스레드에 안전한 이유나

std::unique_ptr을 이용해서 Pimpl 관용구를 구현하는 방법,

람다 표현식의 기본 갈무리 모드를 피해야 하는 이유,

std::atomic과 volatile의 차이를 알고 싶은가?

에 대한 모든 답이 이 책에 있으며, 그 답들은 모두 플랫폼 독립적이고 표준을 준수한다.

이 책은 이식성 있는(portable) C++을 다루기 때문이다.

 

이 책에는 예외가 포함되어 있기 때문에 규칙이 아니라 지침이며, 각 항목에서 가장 중요한 부분은 해당 항목이 제공하는 조언이 아니라 그 조언에 깔린 논리적 근거이다.

이 책은 독자에게 일일이 해야 할 일과 하지 말아야 할 일을 일러주는 것이 아니라 깊게 이해하는데 목표를 가진다.

 

<용어와 관례>

C++에서 가장 널리 쓰이는 기능은 move semantics일 것이며, rvalue/lvalue에 해당하는 표현식이 구분된다는 점에 근거한다.

이는, rvalue는 이동 연산이 가능한 객체를 가리키지만 lvalue는 일반적으로 그렇지 않기 때문이다.

개념적으로 rvalue는 함수가 돌려준 임시 객체에 해당하고, lvalue는 이름이나 포인터, 왼값 참조를 통해서 지칭할 수 있는 객체에 해당한다.

주어진 한 표현식이 왼값인지의 여부를 결정하는 데 유용한 발견법(heuristic) 하나는 그 표현식의 주소를 취할 수 있는지 보는 것이다.

일반적으로 주소를 취할 수 있다면 lvalue이고 없다면 rvalue이다.

이 발견법의 멋진 특징은, 표현식이 lvalue인지 rvalue인지의 여부가 그 표현식의 형식과는 무관하다는 점을 기억하는 데 도움이 된다는 점이다. 이 점을 기억하는 것은 rvalue 참조 형식의 매개변수를 다룰 때 특히나 중요하다. 매개변수 자체는 lvalue이기 때문이다.

 

class Widget {
    public:
        Widget(Widget&& rhs); // rhs의 형식은 오른값 참조이지만,
                              // rhs 자체는 왼값이다.
};

 

Widget의 이동생성자 안에서 곤의 주소를 취하는 것은 완벽하게 유효하다. 따라서, 비록 형식은 오른값 참조이지만, 곤 자체는 하나의 왼값이다. (마찬가지의 논리로, rhs뿐만 아니라 다른 모든 매개변수도 왼값이다.)

위의 예제는 책 전반의 몇 가지 관례로

- Widget은 임의의 사용자 정의 형식의 이름으로 쓰인다.

- rhs“right-hand side(우변)”를 줄인 것이다. 이 이름은 이동 연산들(, 이동 생성자와 이동 배정 연산자)과 복사 연산들(, 복사 생성자와 복사 배정 연산자)의 매개변수에 주로 쓰이며, 이항 연산자의 우변 매개변수 이름으로도 쓰인다.

 

C++에서 하나의 표현식은 두 가지 속성으로 규정되는데, 하나는 형식이고 또 하나는 소위 값 범주(value category)이다. 이 문장의 왼값인지 오른값인지의 여부가 값 범주에 해당한다.

 

객체를 같은 형식의 다른 객체를 이용해서 초기화할 때, 새 객체를 초기화에 쓰인 객체의 복사본이라고 부른다.

새 객체가 이동 생성자를 통해서 생성되는 경우에도 그냥 복사본이라고 부른다.

 

void someFunc(Widget w);      // someFunc의 매개변수 w는
                              // 값으로 전달된다.

Widget wid;                   // wid는 Widget의 한 객체이다.

someFunc(wid);                // 이 someFunc 호출에서 w는
                              // 복사 생성된 wid의
                              // 한 복사본이다.
                
someFunc(std::move(wid));     // 이 someFunc 호출에서 w는
                              // 이동 생성된 wid의
                              // 한 복사본이다.

대체로 오른값의 복사본은 이동 생성되고 왼값의 복사본은 복사 생성된다.

 

위의 코드 예제에서 매개변수 w를 만드는 데 든 비용이 어느 정도인지 파악하려면 someFunc에 전달된 것이 왼값인지 오른값인지를 알아야 한다. 또한 Widget의 복사와 이동 비용도 알아야 한다.

 

함수 호출의 맥락에서, 호출 지점에서 함수에 전달한 표현식을 인수라고 부른다. 인수는 함수의 매개변수를 초기화하는 데 쓰인다.

인수와 매개변수의 구분이 중요한 한가지 이유는, 매개변수의 값은 왼값이지만 매개변수의 초기화에 쓰이는 인수는 왼값일 수도 있고 오른값일 수도 있다는 점이다. 이 점은 한 함수에 전달된 인수를 그것의 왼값 또는 오른값 여부를 보존해서 또 다른 함수에 전달해야 하는 완벽 전달(perfect forwarding)과정에서 특히나 중요하다.

잘 설계된 함수는 예외에 안전하다. 다른 말로 하면 그 함수는 적어도 기본 예외 안전성을 보장한다. 이를 줄여서 기본 보장이라고 한다.

기본 보장 함수를 호출할 때에는 함수 실행 도중 예외가 발생해도 프로그램 불변식들이 위반되지 않으며(, 자료구조가 깨지는 일이 없다) 자원이 새지 않을 것을 확신할 수 있다.

더 나아가서 강한 예외 안정성을 보장하는 함수, 즉 강한 보장 함수를 호출할 때에는 예외가 발생해도 프로그램의 상태가 호출 이전 상태와 동일하게 유지될 것임을 확신할 수 있다.

 

일반적으로 이 책에서 함수 객체(function object)라는 용어는 operator() 멤버 함수를 지원하는 형식의 객체를 뜻한다. 다른 말로 하면, 함수 객체는 함수처럼 행동하는 객체이다.

그러나 때에 따라서는 비멤버(non-member) 함수의 호출 구문, , “함수이름(인수들)”형태를 이용해서 실행할 수 있는 모든 것을 뜻하는 좀 더 넓은 의미로도 쓰인다. 이러한 좀 더 넓은 의미의 함수 객체에는 operator()를 지원하는 객체뿐만 아니라 보통의 함수와 C 스타일의 함수 포인터도 포함된다. (좁은 의미는 C++98에서 비롯된 것이고 넓은 의미는 C++11에서 비롯된 것이다.)

멤버 함수 포인터를 추가해서 이를 더욱 일반화하면 소위 호출 가능 객체(callable object)가 된다.

일반적으로는 이러한 세세한 구분을 무시하고, 함수 객체와 호출 가능 객체라는 것이 그냥 C++에서 일정한 함수 호출 구문을 이용해서 실행할 수 있는 무엇이라고만 생각해도 무방하다.

 

람다 표현식을 통해 만들어진 함수 객체를 클로저(closure)라고 부른다. 람다 표현식과 그로부터 생성된 클로저를 굳이 구분해야 하는 경우는 드물다. 그래서 이 책에서는 둘을 그냥 람다라고 칭한다.

 

C++에서는 선언과 정의가 모두 가능한 것들이 많이 있다. 선언은 이름과 형식을 현재 범위에 도입하기만 하고 그 세부사항(저장 장소나 구현 방식 등)은 지정하지 않는 것을 말한다.

 

extern int x;                 // 객체 선언
class Widget;                 // 클래스 선언
bool func(const Widget& w);   // 함수 선언
enum class Color;             // 범위 있는 열거형 선언

정의는 저장 장소나 구현 세부사항을 지정한다.

int x;                        // 객체 정의
class Widget {                // 클래스 정의
    ...
};

bool func(const Widget& w)
{ return w.size() < 10; }     // 함수 정의

enum class Color
{ Yellow, Red, Blue };        // 범위 있는 열거형 정의

 

이 책은 함수의 서명(signature)이 함수의 선언 중 매개변수 형식들과 반환 형식을 지정한 부분이라고 정의한다. 함수 이름과 매개변수 이름은 서명에 포함되지 않는다. 위의 예에서 func의 서명은 bool(const Widget&)이다. 함수의 선언 중 매개변수 형식과 반환 형식이 아닌 부분은(예를 들어 noexceptconstexpr이 있다면 그것들도) 서명에 포함되지 않는다.

 

대체로 새로운 C++ 표준들은 기존의 표준들에 맞게 작성된 코드의 유효성을 유지하지만, 가끔은 표준위원회가 특정 기능들을 사용하지 말라고 당부하는 경우도 있다. 이런 기능들은 이후의 이식 과정에서 골칫거리가 될 뿐만 아니라, 대체로 해당 기능을 대체하는 새로운 기능에 비해 열등한 경우가 많다. 예를 들어 C++11에서 std::auto_ptr은 비권장 기능인데, 이는 std::unique_ptr이 같은 일을 더 잘 해내기 때문이다.

 

표준은 종종 어떤 연산의 결과가 미정의 행동이라고 명시한다. 이는 그 연산의 실행시점(runtime) 행동을 예측할 수 없다는 뜻이다.

미정의 행동을 가진 연산의 예로는 std::vector의 한 원소를 대괄호(“[]”)로 참조할 때 벡터의 범위를 벗어나는 색인을 지정하는 것, 초기화되지 않은 반복자를 역 참조하는 것, 그리고 자료 경쟁(data race; , 적어도 하나의 쓰기 스레드를 포함하는 둘 이상의 스레드들이 같은 메모리 장소에 동시에 접근하는 상황)에 관여하는 것이 있다. = critical section?

 

이 책에서는 new가 돌려주는 포인터 같은 내장(built-in) 포인터를 생 포인터(raw pointer)라고 부른다. 생 포인터의 반대는 smart pointer이다. 대체로 smart pointer는 포인터 역참조 연산자들(operator->operator*)overloading하지만, std::weak_ptr은 예외이다.

 소스 코드의 식별자 이름에서 종종 보이는 dtor‘destructor(소멸자)’를 줄인 것이다.

 

Comments