티스토리 뷰

항목 26에서 나온 문제에 대하여 해결 방법을 알아보자.

2019/12/01 - [C++/Effective Modern C++] - [Effective Modern C++] 항목 26. 보편 참조에 대한 중복적재를 피하라

불러오는 중입니다...

중복적재를 포기한다

 항목 26에서 사용한 logAndAdd 함수를 중복적재를 사용하다보니 문제가 발생하였다. 문제가 발생하는 함수에 대하여 함수 명을 logAnddAddNameIdx 와 같이 변경하면 문제를 해결할 수 있다. 그렇지만 생성자의 경우에는 함수명을 변경할 수 없기 때문에 해결할 수 없는 경우도 있다.

const T& 매개변수를 사용한다.

 보편 참조 매개변수 대신에 const에 대한 왼값 참조 매개변수를 사용할 수 있다. 이는 항목 26에서 사용한 방법으로 효율성의 문제가 생길 수 있지만 예상치 않은 문제를 피할 수 있다.

값 전달 방식의 매개변수를 사용한다

 참조 전달 매개변수 대신 값 전달 매개변수를 사용하는 것이다. 아래와 같이 사용할 수 있다.

class Person {
public:
    explicit Person(std::string n)
    : name(std::move(n)) {}
    
    explicit Person(int idx)
    : name(nameFromIdx(idx)) {}
    ...
    
private:
    std::string name;
};

꼬리표 배분을 사용한다

 const 왼값 참조 전달이나 값 전달은 완벽 전달을 지원하지 않는다. 완벽 전달을 위하여 보편 참조를 사용하려는 것이라면 보편 참조 외에는 대안이 존재하지 않는다. 이 문제를 해결하기 위해서는 꼬리표 배분이라는 방식을 사용할 수 있다. 먼저 문제 코드를 다시 보자.

std::multiset<std::string> names;

template<typename T>
void logAndAdd(T&& name)
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

 여기서 int 를 받는 중복적재를 추가하는 대신에 다른 두 함수로 위임하게 한다. logAndAdd 함수는 인터페이스 역할을 하고 logAndAddImpl 함수를 추가하여 실제 동작을 하도록 하는 방법이다.

template<typename T>
void logAndAdd(T&& name)
{
    logAnddAddImpl(std::forward<T>(name),
                   std::is_integral<T>());
}

 위 코드에서 사용한 std::is_integral 함수는 타입을 보고 정수 타입인지 확인하는 함수로 정수 타입의 경우에 true 를 리턴한다. 위 코드에서 문제는 int& 형태의 타입이 전달되면 이 타입이 정수 타입이라고 판단하지 않는다는 것이다. 이 문제를 없애기 위하여 아래와 같이 참조들을 제거해줘야 한다.

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(
        std::forward<T>(name),
        std::is_integral<typename std::remove_reference<T>::type>()
    );
}

 이제 logAndAddImpl 함수를 구현해보자. 먼저 정수혀태가 아닌 경우이다.

template<typename T>
void logAndAddImpl(T&& name, std::false_type)
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

 파라미터가 하나 추가된 것 외에는 차이가 없다. 위 std::false_type 은 컴파일러가 적절한 함수를 찾기위해 사용하는 것으로 인자가 false 에 해당하는 경우에만 위 함수가 호출되게 된다. true 인 경우에는 다음과 같다.

std::string nameFromIdx(int idx);

void logAndAddImpl(int idx, std::true_type)
{
    logAndAdd(nameFromIdx(idx));
}

 true 인 경우에는 위 함수가 호출되게 되고 idx 에 해당하는 string 을 찾아 다시 logAndAdd 함수를 호출하도록 하였다. 위와 같은 방식을 꼬리표 배분이라고 부르며 탬플릿 메타프로그래밍의 표준적인 구축 요소이다.

보편 참조를 받는 템플릿을 제한한다.

 std::enable_if 라는 것이 존재하는데 이는 컴파일러에게 특정 템플릿이 존재하지 않는 것처럼 행동하게 만들 수 있다. 

class Person {
public:
    template<typename T,
             typename = typename std::enable_if<조건>::type>
    explicit Person(T&& n);
    ...
};

 위 코드에서 조건에 들어가야하는 것은 Person 타입이 아닌 경우에만 인스턴스를 생성하도록 만들어야하는 것이다. 여기서 먼저 알아야할 것이 있다. 바로 참조 여부와 const, volatile 여부이다. 모두 같은 Person 타입이지만 이 같이 특별한 경우에는 같은 타입이라고 인지하지 못한다. 이와 같은 수식어들을 없애는 방법은 바로 std::ecay 함수를 이용하는 것이다. 아래와 같이 상요할 수 있다.

class Person {
public:
    template<
      typename T,
      typename = typename std::enable_if<
                     !std::is_same<Person,
                                   typename std::decay<T>::type
                                  >::value
                 >::type
    >
    explicit Person(T&& n);
    ...
};

 여기서 끝난것 같지만 그러허지 않다. 항목 26에서 이야기한 파생 클래스에서의 문제들에 대해서 아직 해결하지 못한다. 파생 클래스의 경우에도 인스턴스를 생성하지 못하도록 해야한다. 우리는 운이 좋게도 표준 라이브러리에서 이 함수를 찾을 수 있다. std::is_base_of 는 파생 형식인 경우에 true 를 리턴하도록 되어있다. 이제 간단하다 위 코드에서 is_same 대신에 is_base_of 를 사용하기만 하면 된다.

class Person {
public:
    template<
      typename T,
      typename = typename std::enable_if<
                     !std::is_base_of<Person,
                                      typename std::decay<T>::type
                                     >::value
                 >::type
    >
    explicit Person(T&& n);
    ...
};

 이제 거의 완벽하다. 정수 타입의 문제만 해결하면 된다. 앞서 사용한 is_integral 함수를 사용하면 쉽게 해결이 가능하다.

class Person {
public:
    template<
      typename T,
      typename = std::enable_if<
         !std::is_base_of<Person, std::decay_t<T>>::value
         &&
         !std::is_Integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)
    : name(std:;forward<T>(n))
    { ... }
    ...
private:
    std::string name;
};

절충점들

 지금까지 완벽전달의 장점들을 활요하기 위해 여러가지 해결 방법을 알아보았다. 그러나 완벽 전달에도 단점들이 있다. 하나는 완벽 전달이 불가능한 인수들이 존재한다는 것이고, 또 하나는 오류 메시지의 문제이다. 예를 들어 char 형이 아닌 char16_t 를 사용한다고 하자. 이를 Person 객체에 적용하면 난해한 오류 메시지들이 출력된다. 에러가 발생하는 이유는 사실 간단하다. string 형태로 변경할 수 없어 발생하는 문제이다. static_assert 를 이용하여 오류 메시지를 쉽게 확인할 수 있다.

class Person {
public:
    template<
      typename T,
      typename = std::enable_if<
         !std::is_base_of<Person, std::decay_t<T>>::value
         &&
         !std::is_Integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)
    : name(std:;forward<T>(n))
    {
        static_assert(
            std::is_constructible<std::string, T>::value,
            "Parameter n can't be used to construct a std::string"
        );
    }
    ...
private:
    std::string name;
};

 

참고 서적

스콧 마이어스, Effective Modern C++
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함