Effective Modern C++


Effective Modern C++ 도서를 읽고 정리한 효율적인 C++ 프로그래밍 방법

저자: 스콧 마이어스

역: 류광

출판사: 한빛미디어

발간일: 2015/09/18



C++14: C++ 14 표시가 없으면 C++11, C++14 모두 해당


Type Deduction

  • 01 template type deduction

    template<typedef T>
    void f(ParamType param);
    f(expr);
    
    1. PramType이 포인터 또는 참조 형식이지만 보편 참조(&&)는 아님
      • ParamType 의 형식이 T& 인경우 T는 참조성을 무시한다.
    2. PramType이 보편 참조임
      • expr 이 lvalue일 경우 1번과 다르게 취급한다.
    3. 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>
    
  • 02 auto의 type deduction

    1. 형식 지정자가 포인터나 참조 형식이지만 보편 참조는 아닌 경우

    2. 형식 지정자가 보편 참조인 경우

    3. 형식 지정자가 포인터도 아니고 참조도 아닌 경우

    4. 배열과 함수 이름이 포인터로 붕괴하는 경우

    5. 균일 초기화(uniform initialization)를 하는 경우

    6. 반환 형식에 또는 람다의 매개함수에 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++14

      template<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>::referencebool 타입으로의 형변환 기능을 포함한다.)

        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 0NULL 보다 nullptr 을 선호하자.

    • 0int 이기 때문에 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)

    • 함수 재정의 요건

      1. base class 가 가상함수 (virtual)
      2. base class 와 derived class 의 함수명이 동일
      3. base class 와 derived class 의 매개변수의 형식들이 동일
      4. base class 와 derived class 의 const 성이 동일
      5. base class 와 derived class 의 exception specification이 동일
      6. 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_ptrstd::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::movestd::forward
    • std::movestd::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 constconstexpr 자료 멤버, 템플릿 및 중복적재된 함수 이름, 비트필드이면 완벽 전달이 실패한다.

람다 표현식

  • lambda expression: 람다 표현식, 소스코드의 일부
  • closure: 람다에 의해 만들어진 실행 시점의 객체
  • closure class: closure를 만드는데 쓰인 클래스
  • 31 기본 갈무리(capture) 모드를 피하라
    • 기본 참조 갈무리는 참조가 대상을 잃을 위험이 있다.
    • 기본 값 갈무리는 포인터가 대상을 잃을 수 있으며, 람다가 자기 완결적이라는 오해를 부를 수 있다.
  • 32 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라.
    • C++14에서는 초기화 갈무리를 사용하라.
    • C++11 에서는 직접 작성한 classstd::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++ (스콧 마이어스 지음, 류광 옮김)도서를 참고

  • std::forward

이 시리즈의 게시물

댓글