ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] auto와 std::vector, 그리고 Proxy pattern
    C++/Effective Modern C++ 2022. 2. 14. 16:10

    이전 글(auto의 사용을 고려해야 할 상황들) 에서 auto를 사용함으로써 이점이 명확한 예제들을 살펴보았습니다.

    그런데, auto는 형식 연역을 사용하기 때문에 초기치가 잘못되었을 경우, 의도하지 않은 형식으로 연역될 가능성이 존재합니다.

    이번 글에서는 한 예제를 살펴보며 원하지 않는 형식으로 연역되는 상황과, 그런 상황에 대한 해결법 한가지를 살펴보도록 하겠습니다.

     


     

    예제를 살펴보도록 하겠습니다.

    class Widget;
    
    std::vector<bool> features(const Widget& w);
    
    Widget w;
    bool high_priority = features(w)[5];
    
    process_widget(w, high_priority); //Do something

    위 코드의 각 함수, 클래스는 다음과 같습니다.

    Widget : 특정 기능을 하는 클래스

    features : 파라미터의 Widget이 특정 기능을 지원하는 여부를 담고있는 bool vector를 반환하는 함수

    high_priority : bool vector의 5번 비트는 Widget의 우선순위 여부를 나타냄

    process_widget : Widget과, 우선도에 따라 특정 작업을 수행

     

     

    1. std::vector<bool>

     

    더보기

    위 코드의 high_priority를 auto로 바꾸는 경우를 가정해보겠습니다.

    이 경우 high_priority는 더 이상 auto가 아니게 됩니다.

    template<typename T>
    void function(const T& param) {
        std::cout << "T : " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << "\n";
    }
    
    int main() {
        bool b1 = features(w)[5];
        auto b2 = features(w)[5];
        
        function(b1);
        function(b2);
    }
    T : bool
    T : class std::_Vb_reference<struct std::_Wrap_alloc<class std::allocator<unsigned int> > >

    function은 이전 글(형식 연역)에서 살펴본, 연역된 형식을 출력해주는 함수입니다.

    출력을 통해 auto는 bool로 연역되지 않음을 볼 수 있습니다.

    이것은 std::vector<bool>의 특수성 때문입니다.

    std::vector<bool>은 저장된 bool을 bool당 1비트의 압축된 형태로 표현하도록 명시되어 있습니다.

    그런데 C++에서 비트에 대한 참조가 금지되어 있기 때문에, std::vector<bool>의 operator[]는 bool&를 직접 반환할 수 없습니다.

     

    이것을 해결하기 위해 std::vector<bool>은 bool&처럼 작동하는 std::vector<bool>::reference라는 클래스를 가지고 있습니다. 위 예제의 두번째 출력이 그것입니다. 

    b1은 std::vector<bool>::referecne를 bool로 형변환 하지만, auto는 그렇지 않습니다.

    b2는 다음과 같은 과정을 거치게 됩니다.

    1. 호출된 feature()함수는 임시 std::vector<bool>객체를 반환하게 됩니다. 이 객체를 Temp라고 이름짓겠습니다.
    2. Temp에 대하여 호출된 operator[]는 std::vector<bool>::reference를 반환합니다.
    3. b2는 Temp의 reference로 연역되고, 이것은 임시 객체의 참조이기 때문에 문장의 끝에서 소멸합니다.

    따라서 b2는 Dangling pointer이 됩니다.

    std::vector<bool>::reference로 연역되더라도 이후에 형 변환이 이루어지면 문제가 발생하지 않을 수 있지만, 대상을 잃은 포인터가 되기 때문에 문제가 발생하는 것 입니다.

     

    2. Proxy pattern

     

    더보기

    다른 객체를 대체하면서, 기능 보강을 목적으로 하는 클래스를 대리자 클래스(Proxy class)라고 합니다.

    std::vector<bool>::reference도 이러한 Proxy class중의 하나이며, 이것의 목적은 bool&을 대체하는 것 입니다.

    Proxy class중에는 클라이언트에게 드러나는 종류와, 드러나지 않는 종류가 있습니다.

    std::vector<bool>::reference와 std::bitset::reference가 이러한 드러나지 않는 종류의 대리자입니다.

    드러나는 클래스의 대표적인 예시는 std::shared_ptr과 std::unique_ptr이 있겠습니다.

     

    STL 외에도 Proxy class는 수치 처리 코드의 효율성을 개선하기 위해서도 사용될 수 있습니다.

    Matrix sum = m1 + m2 + m3 + m4;

    위와 같은 클래스와 표현식이 있다고 가정했을 때 operator+연산이 Matrix를 반환하는 것이 아닌 연산 결과에 대한 대리자, 예를 들어 Sum<Matrix, Matrix>를 반환하고, operator=연산에 대하여 Sum<Matrix, Matrix>의 Matrix로의 변환을 지원하는 것 입니다.

    위와 같은 연산의 경우 초기화 표현식 전체를 부호화 합니다.

    따라서 결과는 Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>와 같은 형식이 될 것입니다.

     

    이러한 드러나지 않는 Proxy class는 객체의 수명이 짧다는 것을 가정하고 만들어졌을 확률이 높습니다.

    따라서 auto로 연역되었을 경우에는 미정의 행동을 유발할 수 있어 잘 맞지 않을 수 있습니다.

     

    3. auto의 형식을 지정하기

     

    더보기

    위와 같이 auto와 맞지 않는 초기치를 발견했을 경우의 해답은 auto를 사용하지 않는 것이 아닙니다.

    auto가 문제가 아니고, auto가 개발자의 의도와 다른 형식을 연역하는 것이 문제이기 때문입니다.

    이것의 해결법은 auto를 사용하지 않는 것이 아니고, auto가 다른 형식을 연역하도록 지정하는 것 입니다.

     

    예를 들어 위와 같은 std::vector<bool>::reference의 경우에는 아래와 같이 됩니다.

    template<typename T>
    void function(const T& param) {
        std::cout << "T : " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << "\n";
    }
    
    int main() {
        bool b1 = features(w)[5];
        auto b2 = static_cast<bool>(features(w)[5]);
        
        function(b1);
        function(b2);
    }
    T : bool
    T : bool

    static_cast<>를 통해 b2가 bool로 연역될수 있도록 한 것을 볼 수 있습니다.

    이 코드에서 두 번째 feature()[5]가 임시 객체에 대한 std::vector<bool>::reference를 반환하는 것은 동일하지만, 이후 bool로 변환되는 과정에서 features()의 std::vector<bool>에 대한 포인터가 역참조됩니다.

    이 시점에서 포인터는 유효하며 역참조된 std::vector<bool>에 대하여 [5]가 적용되고, b2가 5번 비트의 bool로 초기화 되는 것 입니다.

     

    그 다음 문단의 Proxy class를 가정한 Matrix 연산 또한 아래와 같이 적용이 가능합니다.

    Matrix sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

     

    위와 같이 auto의 초기치 외에도 표현식의 형식과 다른 형식의 변수를 생성할 때, 의도를 명확하게 나타내는 데에도 사용할 수 있습니다.

    예를들어 특정한 한계치를 계산하는 함수가 있다고 가정할 때

    //function
    double calc_epsilon();
    
    //Before
    float ep = calc_epsilon();
    
    //After
    float ep = static_cast<float>(calc_epsilon());

    메모리 관점에서 기존 함수의 double보다 float이 더 낫다고 판단될 경우 Before의 식처럼 float에 담을 수 있습니다. 이 경우 double이 묵시적으로 float으로 변환됩니다.

    하지만, 이 경우 코드만 가지고는 개발자의 의도가 나타나지 않을 수 있습니다. 그 때에 After의 형 변환 연산자를 통해 의도를 표현할 수 있습니다.

     


     

    위 예제의 Proxy class처럼 auto가 피해야 할 형식이 존재할 경우, 그 식은 대부분 라이브러리 문서 혹은 헤더 파일에서 함수의 서명을 통해 찾을 수 있는 경우가 대부분입니다.

    위 사례들과 해법을 통해 auto를 사용하는 데 도움이 되었으면 좋겠습니다.

     

    읽어주셔서 감사합니다.

    댓글

Designed by Tistory.