Effective Modern C++
Effective Modern C++ 도서를 읽고 정리한 효율적인 C++ 프로그래밍 방법
저자: 스콧 마이어스
역: 류광
출판사: 한빛미디어
발간일: 2015/09/18
C++14: C++ 14 표시가 없으면 C++11, C++14 모두 해당
Type Deduction
-
01
template
type deductiontemplate<typedef T> void f(ParamType param); f(expr);
- PramType이 포인터 또는 참조 형식이지만 보편 참조(&&)는 아님
- ParamType 의 형식이 T& 인경우 T는 참조성을 무시한다.
- PramType이 보편 참조임
- expr 이 lvalue일 경우 1번과 다르게 취급한다.
- PramType이 포인터도 아니고 참조도 아님 (값 전달 방식)
- expr 의 const, volatile 속성은 무시된다.
// 1. 참조(&) 형식의 param template<typename T> void f1(T& param); int x = 27; // int const int cx = x; // const int const int& rx = x; // const int 인 x에 대한 참조 f1(x); // T: int, param 의 형식: int& f1(cx); // T: const int, param의 형식: const int& f1(rx); // T: const int, param의 형식: const int& // 2. const 형식의 param template<typename T> void f2(const T& param); f2(x); // T: int, param 의 형식: const int& f2(cx); // T: int, param의 형식: const int& f2(rx); // T: int, param의 형식: const int& // 3. param 이 보편 참조 template<typename T> void f3(T&& param); f3(x); // x는 lvalue // T: int&, param 의 형식: int& f3(cx); // cx는 lvalue // T: const int&, param의 형식: const int& f3(rx); // rx는 rvalue // T: int, param의 형식: int&& // 4. param 이 포인터도 아니고 참조도 아님 (pass-by-value) template<typename T> void f4(T param); // 인수의 참조(&), const, volatile 형태는 무시된다. f4(x); // T: int, param 의 형식: int f4(cx); // T: int, param의 형식: int f4(rx); // T: int, param의 형식: int const char* const ptr = "fun with pointers"; // 여기서 왼쪽의 const는 문자열(char*) 이 변경 불가함을 의미한다. // 오른쪽의 const는 ptr의 포인터가 다른 곳을 가르키도록 변경이 불가함을 의미한다. // (예를 들어 null로 변경이 불가) f4(ptr); // param 은 const 문자열(char*)을 가르키는 수정 가능한 포인터 // ptr 포인터의 변경이 불가능한 속성(오른쪽 const)이 사라짐
- expr 의 형식이 배열인 경우
- 포인터로 붕괴된다.
- 참조로 받을 경우 붕괴되지 않는다.
const char*
는 배열(const char[]
) 이 아니다!const char[]
말고std::array<cahr>
를 쓰자!
template<typedef T> void f1(T param); template<typedef T> void f2(T& param); const char name[] = "J. P. Bridggs"; // name 의 형식은 const char[13] f1(name); // param 의 형식은 const char* (포인터로 붕괴) f2(name); // param 의 형식은 const char[13] // 위의 특성을 이용하여 배열의 크기를 알아내는 함수 // constexpr 는 이 함수의 호출 결과를 컴파일 도중에 사용 가능하게 한다. template<typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept { return N; }
- uniform initialization 을 이용하는 경우
template<typename T> f1(T param); f1({1, 2, 3}); // compile error! template<typename T> f2(std::initializer_list<T> param); f2({1, 2, 3}); // T는 int 로 연역(deduction) // param 의 형식은 std::initializer_list<int>
- PramType이 포인터 또는 참조 형식이지만 보편 참조(&&)는 아님
-
02
auto
의 type deduction-
형식 지정자가 포인터나 참조 형식이지만 보편 참조는 아닌 경우
-
형식 지정자가 보편 참조인 경우
-
형식 지정자가 포인터도 아니고 참조도 아닌 경우
-
배열과 함수 이름이 포인터로 붕괴하는 경우
-
균일 초기화(uniform initialization)를 하는 경우
-
반환 형식에 또는 람다의 매개함수에 auto 형식 연역을 하는 경우 C++14
template type deduction 과 동일하게 작동
// 1. 3. auto x = 27; // 3. x 는 포인터도 아니고 참조도 아님 const auto cx = x; // 3. cx 는 포인터도 아니고 참조도 아님 const auto& rx = x; // 1. rx 는 참조 형식 // 2. auto&& uref1 = x; // 2. x는 int 이자 lvalue // uref1 은 int& auto&& uref2 = cx; // 2. cx는 const int 이자 lvalue // uref2 의 형식은 const int& auto&& uref3 = 27; // 2. 27은 int 이자 rvalue // uref3의 형식은 int&& // 4. const char name[] = "R. N. Briggs"; // name 의 형식은 const char[13] auto arr1 = name; // arr1 의 형식은 const char* auto arr2 = name; // arr2 의 형식은 const char (&)[13] void someFunc(int, double); auto func1 = someFunc; // func1 의 형식은 void (*)(int, double) auto& func2 = someFunc; // func2 의 형식은 void (&)(int, double) // 5. auto x1 = 27; // x1 의 형식은 int, 값은 27 auto x2(27); // x2 의 형식은 int, 값은 27 auto x3 = { 27 }; // x3 의 형식은 std::initializer_list<int>, 값은 {27} auto x4{27}; // x4 의 형식은 std::initializer_list<int>, 값은 {27} auto x5 = {1, 2, 3.0}; // compile error! // std::initalizer_list<T> 의 T를 연역 할 수 없음 // 6. auto createInitList() { return { 1, 2, 3 }; // compile error } auto resetV = [&v](const auto& newValue) { v = newValue; } resetV({1, 2, 3}); // compile error
-
-
03
decltype
의 작동 방식-
decltype
은 항상 변수나 표현식의 형식을 아무 수정 없이 보고한다. -
decltype
은 형식이T
이고 이름이 아닌 lvalue 표현식에 대해서는 항상T&
형식을 보고한다. -
C++14 에서는
decltype(auto)
를 지원한다. -
일반적인 동작 예제
const int i = 0; // decltype(i): const int bool f(const Widget& w); // decltype(w): const Widget& // decltype(f): bool(const Widget&) struct Point { int x, y; // decltype(Point::x): int } // decltype(Point::y): int Widget w; // decltype(w): Widget if (f(w)) ... // decltype(f(w)): bool template<typename T> class vector{ public: ... T& operator[](std::size_t index); ... }; vector<int> v; // decltype(v): vector<int> if (v[0] == 0) ... // decltype(v[0]): int&
-
후행 반환 형식 (trailing return type) 에 사용
template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) -> decltype(c[i]) { authenticateUser(); return c[i]; }
-
반환 형식을 auto 로 사용할 때의 문제,
decltype(auto)
C++14template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) { authenticateUser(); return c[i]; } std::deque<int> d; authAndAccess(d, 5) = 5; // Compile error! // 여기서 c[i]의 auto 연역에서 참조성이 무시된다. // 반환형식은 int& 가 아닌 int이므로 rvalue에 // 값을 쓰려 하기 때문에 컴파일 오류가 발생한다.
위의 문제를 해결하기 위해
auto
대신 아래와 같이decltype(auto)
를 사용해야 한다.template<typename Container, typename Index> decltype(auto) authAndAccess(Container& c, Index i) { authenticateUser(); return c[i]; // decltype 연역에 의해 T& 를 반환 } // 비슷한 방식으로 decltype 은 참조성을 유지하기 위해 // auto 대신 아래와 같이 사용 가능하다. Widget w; const Widget& cw = w; auto myWidget1 = cw; // myWidget1 의 형식: Widget decltype(auto) myWidget2 = cw; // myWidget2 의 형식: const Widget&
위의 경우는 반환값을 lvalue로 반환하지만 rvalue로도 반환하게 하기 위해서는 아래와 같이 보편 참조를 이용하여 작성해야한다.
template<typename Container, typename Index> decltype(auto) authAndAccess(Container&& c, Index i) { authenticateUser(); return std::forward<Container>(c[i]); } // for C++14 template<typename Container, typename Index> auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) { authenticateUser(); return std::forward<Container>(c[i]); }
int x;
일 때 C++ 에서는(x)
는 왼값이기 때문에decltype((x))
는int&
가 된다.decltype(auto) f1() { int x = 0; return x; // f1() 은 int 를 반환 } decltype(auto) f2() { int x = 0; return (x); // f2()는 int&를 반환 }
-
-
04 type 을 debugging 하는 방법
// STL auto x = getX(); std::cout << typeid(x).name() << '\n'; // Boost Library #include <boost/type_index.hpp> std::cout << type_id_with_cvr<T>().pretty_name() << '\n';
auto
- 05 명시적 형식 선언보다는
auto
를 선호하자.-
초기화를 빼먹지 않게 된다.
-
람다 표현식에서 사용 가능 C++14
-
compiler 만 알던 형식을 지정할 수 있다.
auto derefUPLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; };
auto
로 선언한 함수 포인터가std::function
로 선언한 함수포인터가 메모리를 덜 사용할 수 있다.std::function
을 사용할 경우, 클로저는std::function template
의 인스턴스화 된다. 이때 클로저의 크기가 제한된 메모리를 넘어갈 경우 힙 메모리를 할당하여 클로저를 저장하여 더 많은 메모리 사용을 야기한다.→
std::function
: 메모리 사용 증가std::function은 인라인화가 제한된다. 간접 함수 호출 방식의 구현상 느리다.
→
std::function:
느리다. -
이식성, 효율성 문제를 유발할 수 있는 형식 불일치의 방생이 거의 없다.
std::unordered_map
의 key 부분이const
이다.auto
를 이용해 신경쓰지 않고 구현 가능하다.- 의도하지 않게 발생하는 형변환이 방지된다.
-
코드가 짧아진다.
-
원치 않는 형식 연역이 발생할 수 있다.
-
- 06
auto
가 원치 않은 형식으로 연역될 때엔는 명시적 형식의 초기치를 사용하자.std::vector<bool>::reference
와 같은 대리자 클래스(proxy class)의 객체 사용시auto
를 사용할 경우 의도하지 않은 형태로 연역 될 수 있다.-
std::vector<bool>
의operator[]
가 돌려주는 건은 컨테이너 요소의 참조가 아니라std::vector<bool>::reference
형식의 객체이다. (std::vector<bool>::reference
은bool
타입으로의 형변환 기능을 포함한다.)std::vector<bool> vecBool = {...}; auto elemBool1 = vecBool[2]; // elemBool1 은 bool 타입이 아니다. bool elemBool2 = vecBool[2]; // elemBool2 은 bool 타입이다. // 명시적 형변환을 이용 auto elemBool3 = static_cast<bool>(vecBool[2]); // elemBool3 는 bool 타입
-
Modern C++
-
07 객체 생성 시 괄호
()
와 중괄호{}
-
일반적으로 중괄호를 사용하는 것이 명확하고 잘 동작할 수 있다.
-
Norrow conversion 이 방지된다.
-
std::vector
와 같이 괄호와 중괄호에 따라 다른 생성자가 호출될 수 있다. -
생성자에
std::initializer_list
를 인수로 받는 함수가 있을 경우 이 생성자의 호출이 강제되어 컴파일 오류가 생길 수 있다class Widget{ public: void Widget(int i, double d); void Widget(bool b, double d); void Widget(std::initializer_list<bool> bl); }; Widget w1{1, 2}; // 세번째 생성자 호출로 인한 compile error // (narrow conversion 이 필요하기 때문)
-
-
08
0
과NULL
보다nullptr
을 선호하자.0
은int
이기 때문에 pointer 를 인수로 받는 함수의 overloading 시 문제가 생길 수 있음을 주의해야 한다.
-
09
typedef
보다using
을 선호하자.using
은 템플릿화를 지원한다.
-
10
enum
(unscoped enum)보다enum class
(scoped enum) 을 선호하자.- scoped enum 은 반드시 명시적 형변환이 필요하다.
- scoped enum 의 defualt 바탕형식은 int 이고 unscoped enum 은 defualt 바탕형식이 없다.
- 바탕형식이 없는 unscoped enum 은 전방선언이 불가하다.
-
11 함수 삭제 (
delete
)-
class
의 원치 않는 인스턴스를 방지원치 않는 복사를 방지할 수 있다.
class Widget{ public: Widget() = default; Widget(const Widget&) = delete; // 복사 생성자 삭제 Widget& operator=(const Widget&) = delete; // 복사 대입 연산자 삭제 ...
-
template
의 원치 않는 인스턴스를 방지포인터를 인자로 받는
template
의 원치 않는 인스턴스 방지 (일반 포인터가 아닌void*
와char*
를 인스턴스화 하지 않기 위해 사용 가능)template<typename T> void processPointer(T* ptr); template<> void processPointer<void>(void*) = delete; void processPointer<char*>(char*) = delete;
-
public 함수의 정의를 방지
private 으로 정의된 함수는 클라이언트 코드에서 debugging이 힘들어 진다.
-
-
12 함수 재정의,
override
사용, 참조 한정사 (reference qualifier)-
함수 재정의 요건
- base class 가 가상함수 (virtual)
- base class 와 derived class 의 함수명이 동일
- base class 와 derived class 의 매개변수의 형식들이 동일
- base class 와 derived class 의 const 성이 동일
- base class 와 derived class 의 exception specification이 동일
- base class 와 derived class 의 reference qualifier 가 동일
-
override 사용시 정의되지 않은 함수(또는 잘못 재정의된 함수)에 대한 에러가 발생 → 컴파일 단계에서 문제 확인이 가능
class Base{ public: virtual void f1() const; virtual void f2(int x); virtual void f3() &; // lvalue reference qualifier // *this 가 lvalue 일때만 적용된다. virtual void f4() const; }; // compile 되는 잘못된 코드 (함수 재정의가 되지 않는다.) class Derived1 : public Base{ public: virtual void f1(); virtual void f2(unsigned int x); virtual void f3() &&; // rvalue reference qualifier // *this 가 rvalue 일때만 적용된다. virtual void f4() const; }; // compile 되지 않는 잘못된 코드 class Derived2 : public Base{ public: virtual void f1() override; virtual void f2(unsigned int x) override; virtual void f3() && override; virtual void f4() const override; }; // compile 되는 올바른 코드 class Derived3 : public Base{ public: virtual void f1() const override; virtual void f2(int x) override; virtual void f3() & override; virtual void f4() const override; };
-
-
13
iterator
보다는const_iterator
를 선호하자.begin, end, rbegin
보다는cbegin, cend, crbegin
을 선호하자.std::vector<int> values; ... auto it = std::find(values.cbegin(), values.cend(), 1983); values.insert(it, 1998); // C++14 template<typename C, typename V> void findAndInsert(C& container, const V& target_value, const V& insert_value) { using std::cbegin; // C++14 using std::cend; // C++14 auto it = std::find(cbegin(container), cend(container), target_value); container.insert(it, insert_value); }
-
14 예외 명세
noexcept
- 함수의 인터페이스로 함수가 예외를 방출하지 않을 경우
noexcept
로 선언할 수 있다. noexcept
로 호출된 함수는 컴파일과정에서 최적화 될 여지가 많다.noexcept
는 이동 연산들과 swap, 메모리 해제 함수들, 소멸자들에 특히나 유용하다.- 하지만 대부분의 함수는
noexcept
가 아닌 예외에 중립적이다.- 내가 쓴 함수가 예외를 방출하지 않는다면
noexcept
로 선언 - 함수내 사용하는 third party library가 있을 경우, 확인하여 적용
- 내가 쓴 함수가 예외를 방출하지 않는다면
- 함수의 인터페이스로 함수가 예외를 방출하지 않을 경우
-
15 가능하면 항상
constexpr
을 선호하자.- 컴파일 시점에서 함수, 변수들을 사용할 수 있게 해준다.
- 함수의 경우 인자들이 컴파일 시점에서 알 수 있는 경우에는 컴파일 시점에서 동작하고 그렇지 않을 경우에는 실행 시점에서 동작한다. → 사용할 수 있는 문맥이 넓다.
- C++11은 constexpr 함수의 제약이 있다. (하지만 C++14에서는 폭넓게 사용 가능하다. )
- 리터럴 형식(void가 아닌 함수)이어야 한다.
- 멤버 변수 수정이 불가하다.
- 실행가능 문장이 많아야 하나이다.
-
16 const 멤버 함수를 스레드에 안전하게 작성하자.
std::mutex
,std::atomic
을 이용하여 스레드에 안전하게 작성하자.std::atomic
: 하나의 변수 또는 메모리 장소를 다룰 때std::mutex
: 둘 이상의 변수 또는 메모리 장소를 다룰 때std::mutex
,std::atomic
을 멤버 변수로 정의하면 클래스의 이동, 복사가 방지된다.
-
17 특수 멤버 함수 (special member function)
- 컴파일러가 스스로 작성하는 함수
- 기본 생성자
- 소멸자
- 복사 생성자와 복사 배정 연산자
- 복사 연산이 명시적으로 선언되지 않은 경우 자동으로 작성된다.
- 이동 연산이 하나라도 선언되면 삭제된다.
- 이동(move) 생성자와 이동 배정(move assignment) 연산자
- 이동 연산, 복사 연산, 소멸자가 명시되지 않은 경우 자동으로 작성된다.
- Rule of Three
- 복사 생성자, 복사 배정 연산자, 소멸자 중 하나라도 선언하면 나머지 둘도 선언해야 한다.
- Rule of Five
- Rule of Three 에서 이동, 이동 배정 연산 추가
- .
- .
- .
- 컴파일러가 스스로 작성하는 함수
Smart Pointer
-
18 소유권 독점 자원의 관리에는
std::unique_ptr
를 사용하자.- 독점 소유권 의미론을 가진다. (move only)
- custom deleter 를 지정할 수 있다.
- 상태가 없는 람다 함수를 이용하지 않고, 함수나 상태가 있는 삭제자를 사용하면 std::unique_ptr 의 크기가 커진다.
- std::unique_ptr 에서 std::shared_ptr 로 손쉽게 변환 가능하다.
- 사용 예제 - 팩터리 함수, pimpl 관용구
- 계층구조에서 팩터리 함수의 반환형식으로 사용
- Pimpl 관용구에 사용
-
19 소유권 공유 자원의 관리에는
std::shared_ptr
를 사용하자.-
std::shared_ptr는 임의의 공유자원의 수명을 편리하게 관리할 수 있게 한다.
-
대체로 std::shared_ptr 객체는 그 크기가 std::unique_ptr객체의 두 배이며, 제어 블록에 관련된 추가 부담을 유발하며 원자적 참조 횟수 조작을 요구한다.
-
자원은 기본적으로 delete로 파괴되나 커스텀 삭제자도 지원된다.
- std::unique_ptr 과는 다르게 커스텀 삭제자의 형식에 아무런 영향을 받지 않는다
-
raw pointer 형식의 변수로부터 std::shared_ptr를 생성하는 일은 피하자.
auto pw = new Widget; // create raw pointer std::shared_ptr<Widget> sp1(pw); // Bad code! std::shared_ptr<Widget> sp2(new Widget); // Better code!
-
-
20
std::shared_ptr
처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면std::weak_ptr
를 사용하자.- 캐싱, 관찰자 목록,
std::shared_ptr
순환 고리 방지에 사용 가능
- 캐싱, 관찰자 목록,
-
21
new
를 직접 사용하는 것보다std::make_unique
,std::make_shared
를 선호하자.new
를 직접 사용할때보다 소스코드의 중복 여지가 없어지고 예외 안전성이 향상되고 더 작고 빠른 프로그램이 산출된다.- 중괄호로 초기치를 전달할 수 없으며 custom deleter 를 지정할 수 없다.
- 예외적으로
new
를 선호해야 하는 상황- 메모리가 넉넉하지 않은 상황에서 큰 객체를 다루는 경우
- 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우
std::weak_ptr
가std::shared_ptr
보다 오래 살아남는 경우
-
22 Pimpl 관용구(Pointter to implementation idiom)를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하자.
-
Pimpl 관용구는 클래스 구현과 클래스 클라이언트 사이의 컴파일 의존성을 줄임으로써 빌드 시간을 감소시킨다.
-
std::unique_ptr형식의 pImpl 포인터를 사용할 때에는
- 특수 멤버 함수들을 클래스 헤더에 선언하고 구현 파일에서 구현해야 한다.
- 컴파일러가 기본으로 작성하는 함수 구현들을 그대로 사용할 경우에도 구현 파일에서 구현해야 한다.
// widget.h class Widget{ public: Widget(); ~Widget(); Widget(const Widget& rhs); // 복사 생성자; 선언만 한다. Widget& operator=(const Widget& rhs); // 복사 배정 생성자; 선언만 한다. private: struct Impl; std::unique_ptr<Impl> pImpl; }; // widget.cpp 구현파일 #include "widget.h" ... struct Widget::Impl { //Widget 객체에 필요한 자료 멤버들 std::string name; std::vector<double> data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique<Impl>(){} // 생성자 Widget::Widget(const Widget& rhs) // pImpl 의 deep copy를 위한 : pImpl(nullptr) // 복사 생성자 {if (rhs.pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); } Widget::~Widget() = default; // 기본 구현 소멸자 Widget& Widget::operator=(const Widget& rhs) // 복사 배정 연산자 { if (!rhs.pImpl) pImpl.reset(); // rhs 가 nullptr 이면 pImpl reset else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); // pImpl 가 nullptr 이면 깊은 복사 생성 else *pImpl = *rhs.pImpl; return *this; }
-
Rvalue Reference, Move Semantics, Perfect Forwarding
- move semantics
- 중요한 이유
- 복사 등이 일어났을때 비용이 얼마나 드는지 명확하게 알 수 있다. (이동 생성을 통해 비용을 줄일 수 있다.)
- 중요한 이유
- 오른값 (rvalue)
- 이동 연산이 가능
- 함수가 돌려준 임시 객체
- 주소를 취할 수 없다.
- 왼값 (lvalue)
- 이동 연산이 불가능
- 이름이나 포인터, 왼값 참조를 통해서 지칭할 수 있는 객체
- 주소를 취할 수 있다.
- 23
std::move
와std::forward
std::move
와std::forward
는 둘 다 실행시점에서 아무 일도 하지 않고 (조건부)캐스팅만 한다.std::move
는 무조건 rvalue로 캐스팅을 수행한다.std::forward
는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.
- 24 보편 참조(universal reference)
- 보편 참조는 오른값으로 초기화 되면 오른값 참조, 왼값으로 초기화 되면 왼값 참조가 된다.
- 함수 템플릿 매개변수의 형식이
T&&
형태이고T
가 연역된다면, 또는 객체를auto&&
로 선언한다면, 그 매개변수나 객체는 보편참조이다. - 형식 선언의 형태가 정확히
형식&&
가 아니면 또는형식
의 연역이 일어나지 않으면형식&&
은 오른값 참조이다.
- 25 오른값 참조에는
std::move
를, 보편 참조에는std::forward
를 사용하자.- 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄 때에도 각각 std::move나 std::forward를 적용하자.
- 컴파일러가 반환값 최적화를 수행할 수 있는 지역 객체에 대해서는 절대 std::move 나 std::forward를 적용하지 말아야 한다.
- 26 보편 참조에 대한 overloading 을 피하자.
- 의도치 않게 보편 참조의 overloading 함수가 호출되는 경향이 크다.
- 27 보편 참조에 대한 overloading 대신 사용할 수 있는 방법
- 함수의 이름을 다르게 하기
- 매개변수를 const 에 대한 lvalue reference 로 전달
- 꼬리표 배분
std::enable_if
를 이용해서 템플릿의 인스턴스화를 제한할 수 있다. (컴파일러가 보편 참조overloading
을 사용하는 조건을 제어할 수 있다.)
- 28 참조 축약을 숙지하라
- 참조 축약은 템플릿 인스턴스화,
auto
형식 연역,typedef
와 별칭 선언의 지정 및 사용,decltype
의 지정 및 사용시 발생한다.
- 참조 축약은 템플릿 인스턴스화,
- 29 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라.
- 30 완벽 전달이 실패하는 경우들을 잘 알아두라
- 완벽 전달(perfect forwarding)은 템플릿 형식 연역이 실패하거나 틀린 형식을 연역했을 때 실패한다.
- 인수가 중괄호 초기치이거나
0
또는NULL
로 표현된 널 포인터, 선언만 된 정수static const
및constexpr
자료 멤버, 템플릿 및 중복적재된 함수 이름, 비트필드이면 완벽 전달이 실패한다.
람다 표현식
- lambda expression: 람다 표현식, 소스코드의 일부
- closure: 람다에 의해 만들어진 실행 시점의 객체
- closure class: closure를 만드는데 쓰인 클래스
- 31 기본 갈무리(capture) 모드를 피하라
- 기본 참조 갈무리는 참조가 대상을 잃을 위험이 있다.
- 기본 값 갈무리는 포인터가 대상을 잃을 수 있으며, 람다가 자기 완결적이라는 오해를 부를 수 있다.
- 32 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라.
- C++14에서는 초기화 갈무리를 사용하라.
- C++11 에서는 직접 작성한
class
나std::bind
로 초기화 갈무리를 흉내 낼 수 있다.
- 33
std::forward
를 통해서 전달할auto&&
매개변수에는decltype
을 사용하라. - 34
std::bind
보다 람다를 선호하라.- 람다가 읽기 쉽고 표현력이 좋다.
- 람다는 함수를 인라인화 하여 더 효율적일 수 있다.
- 객체를 템플릿화된 함수 호출 연산자에 묶으려 할 때
std::bind
가 유용할 수 있다.
동시성(concurrency) API
- 35 스레드(thread) 기반 프로그래밍보다 과제(task) 기반 프로그래밍을 선호하라.
std::thread
API에서는 비동기적으로 실행된 함수의 반환값을 직접 얻을 수 없으며, 만일 그런 함수가 예외를 던지면 프로그램이 종료한다.- 스레드 기반 프로그래밍에서는 스레드 고갈, 과다구독, 부하 균형화, 새 플랫폼으로의 적응을 개발자가 직접 처리해야 한다.
std::async
와 기본 시동 방침을 이용한 과제 기반 프로그래밍은 그런 대부분의 문제를 알아서 처리해준다.
- 36 비동기성이 필수일 때에는
std::launch::async
를 지정하라.std::async
의 기본 동작은std::launch::async || std::launch::deferred
이다.std::launch::async
: 함수를 반드시 비동기적으로 다른 스레드에서 실행한다.std::launch::deferred
:std::async
가 돌려준 미래 객체(std::future
)에 대해get
이나wait
가 호출될 때까지 지연(deferred)된다.
- 37
std::thread
들은 모든 경로에서 합류 불가능하게 만들어라.- 모든 경로에서 std::thread를 합류 불가능으로 만들어라.
- 소멸 시 join 방식은 디버깅하기 어려운 성능 이상으로 이어질 수 있다.
- 소멸 시 detach 방식은 디버깅하기 어려운 미정의 행동으로 이어질 수 있다.
- 자료 멤버 목록에서 std::thread객체를 마지막에 선언하라.
- 38 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라.
- 미래 객체의 소멸자는 그냥 미래 객체의 자료 멤버들을 파괴할 뿐이다.
std::async
를 통해 시동된 비지연 과제에 대한 공유 상태를 참조하는 마지막 미래 객체의 소멸자는 그 과제가 완료될 때까지 차단된다.
- 39 단발성 사건 통신에는 void 미래 객체를 고려하라.
- 뮤텍스, 조건변수, 플래그 등을 고려할 수 있다.
std::promise
와 미래 객체를 사용할 수 있다.
- 40 동시성에는
std::atomic
을 사용하고volatile
은 특별한 메모리에 사용하라.std::atomic
은 뮤텍스 보호 없이 여러 스레드가 접근하는 자료를 위한 것volatile
은 읽기와 기록을 최적화로 제거하지 말아야 하는 메모리를 위한 것으로 memory mapped IO 등에 사용할 수 있다.
다듬기
- 41 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라.
- 42 삽입 대신 생성 삽입(emplace)을 고려하라.
Reference
-
기본적으로 Effective Modern C++ (스콧 마이어스 지음, 류광 옮김)도서를 참고