Post List

2015년 3월 7일 토요일

item 26. Universal Reference를 함수 오버로드의 인자로 사용하지 말자.

Effective Modern C++ - Scott Meyers
Item 26: Avoid overloading on universal references.

item 26. Universal Refernece를 함수 오버로드의 인자로 사용하지 말자.

item을 인자로 받아서, 전역 Log History에 저장하는 함수를 구상해보자.

std::multiset<std::string> History;   // Global Log History

void Log(const std::string& item)
{
History.emplace(item);            // add item to Global Log History
}

잘못된 Code는 아니지만, 비효율적이다.
아래 3가지 함수 Call을 보자면,

std::string name("Luna");

Log(name);                 // 1. Pass L-Value std::string
Log(std::string("Star"));  // 2. Pass R-Value std::string
Log("Seokjoon");           // 3. Pass string literal

1. Log의 인자로 name이라는 변수를 사용했고, History.emplace로 전달된다.
   itemL-Value이기 때문에 HistoryCOPY된다.

2. 인자로 R-Value가 전달되었지만, item은 L-Value이기 때문에 결국 History로 COPY된다.
   MOVE 할 수 있는 방법도 있지만, 일단 여기서는 COPY 하였다. (즉, 개선의 여지가 있다.)

3. item이 다시 R-Value로 전달되었다.
   하지만 이번에는 literal부터 임시적으로 std::string이 만들어지는 경우이다.
   2번같이 item은 History로 COPY 된다.
   하지만 여기서는 literal이 그대로 History.emplace로 전달가능하기 때문에 임시로 std::string를 만들 필요가 없다.
   대신 emplace가 std::multiset 안에다가 직접 std::string를 생성할 수 있기 때문이다.
   하지만 여기서는 잘못사용해서 COPY를 하게 된다.

2,3번의 비효율성을 Universal Reference (item 24)std::forward (item 25)를 사용하여 해결 할 수 있다.

template<typename T>
void Log(T&& item)
{
    History.emplace(std::forward<T>(item));
}

std::string name("Luna");

Log(name)                   // 1. Pass L-Value std::string  (As before)
Log(std::string("Star"));   // 2. Move R-Value (instead of copying it)
Log("Seokjoon");            // 3. Create std::string in multiset
        (instead of copying a temporary std::string)

 만세 ! 효과적으로 최적화 되었다.
 모든게 다 해결된 듯하지만, 그렇지 않다.
 History를 직접 제어하지 않고 중간에 다른 것을 사용 할 수도 있다.
 예를 들어 index를 가지고 item이 저장된 table에서 찾아서 전달 할 경우를 생각해보자.
 그럴 경우를 대비하여 Log를 오버로딩하였다.

std::string GetItemFromTable(int idx);

void Log(int idx)
{
    History.emplace(GetItemFromTable(idx));
}

Log(22);
// Call int Overload

정상적으로 동작한다. 하지만 idx를 int로만 전달한다고는 생각하면 큰 착각이다.

short s = 22;
Log(s); // Compile Error : can’t convert argument ‘short’ to std::string ctor

함수 사용시 흔히 발생할 수 있는 경우이다. 그런데 Compile 오류가 난다.

Log에는 2개의 오버로드된 함수가 있다.
하나는 Universal Reference를 사용한 것으로 T를 short로 추론이 가능하다.
int를 인자로 받는 오버로드된 함수는 정확하게 타입이 일치했을 때만 호출이 된다.
그래서 오버로드 결정 규칙(Overload resolution rule)에 의해서 short는 int와는 정확하게 일치하지가 않고 T로 추론이 가능하므로 Universal Reference를 사용한 함수가 호출된다.

함수안에서 어떤 일이 발생하는지 한번 보자.
인자 item에 short가 전달된다. std::forward를 통해 History(std::multiset<std::string>)의 emplace 함수를 호출하게 되어 std::string의 생성자가 호출된다.
하지만 std::string에는 short를 받아서 처리하는 생성자가 없어서 error가 발생한다.
Universal Reference Overload가 int보다는 short와 더 우선순위에 있어서 생긴 결과이다.

Universal Reference를 받는 함수는 C++에서 가장 탐욕스러운 함수이다.
인자의 거의 모든 타입들에 대해서 정확히 일치시키는 함수로 인스턴스화 된다.
(그렇지 않은 인자에 대해서는 item 30에서 설명)
Universal Reference는 우리가 무엇을 상상하든 그 이상으로 인자 타입들을 호로록 한다.
그래서 Universal Reference를 인자로 사용하는 함수를 오버로드하는 것은 위험하다.

이걸 해결할 수 있는 가장 쉬운 방법은 Perfect Forwarding Constructor를 만드는 것이다.
예제에 조금만 고쳐도 해결된다.
위 함수와 똑같은 기능을 가진 Item이라는 class를 만들어보자.

class Item {
public:
    template<typename T>
    explicit Item(T&& data) // Perfect Forwarding ctor
       : value(std::forward<T>(data)) {}

    explicit Item(int idx) // int ctor
       : value(GetItemFromTable(idx)) {}

private:
    std::string value;
};

하지만 위 예제도 int를 제외한 모든 타입 (예를 들어 std::size_tshortlong)에 대해서 Universal Reference 생성자가 호출되어서,
결국 Compile Error가 난다.

Item의 생성자는 뭘 상상하든 그 이상으로 훨씬 더 많은 오버로딩을 만들어 낸다.
item 17에서 일정한 조건에서는 C++이 COPYMOVE 생성자를 만들어 준다고 배웠다.
template를 포함한 class도 마찬가지다.
Item의 COPYMOVE 생성자가 생성되면 다음과 같은 형태일 것이다.

class Item {
public:
    template<typename T>
    explicit Item(T&& data) : value(std::forward<T>(data)) {} // Perfect Forwarding ctor

    explicit Item(int idx) : value(GetItemFromTable(idx)) {} // int ctor
       
    Item(const Item& SRC); // Copy ctor (Compiler-generated)
    Item(Item&& SRC);      // Move ctor (Compiler-generated)

private:
    std::string value;
};

Item ME("Luna");
auto CloneofME(ME); // Create new Item from ME;

다른 Item으로 Item을 만들고자 한다. MEL-Value 이므로 COPY 생성자가 호출되리라 예상되지만,
COPY 생성자가 호출되지 않고, Perfect Forwarding 생성자를 호출한다.
Item Object (ME)로 부터 std::string을 생성하려 하지만, std::string는 Item을 인자로하는 생성자가 없으므로
컴파일러는 자신의 불만의 표현을 길고 이해할 수 없는 오류 메세지로 당신을 처벌할 것이다.

아 왜 ?
왜 다른 Item으로부터 Item을 생성하는데 COPY 생성자 대신 Perfect Forwarding 생성자가 호출될까 ?

우리와는 달리 Compiler는 C++의 Rule을 반드시 지키도록 맹세하였다.
오버로딩 된 함수를 호출했을 때 결정 규칙은 위와 같이 실행된다.

Compiler가 해석한 방법이다.
CloneofMEnon-const L-Value인 ME로 초기화되고 있다.
template생성자는 Item 타입의 non-const L-Value로 정확히 인스턴스화가 가능하다.
그래서 다음과 같이 Perfect Forwarding 생성자는 인스턴스화 될 것이다.

explicit Item(Item& data) // Instantiated from Perfect Forwarding template
: value(std::forward<Item&>(data)) {}


auto CloneofME(ME); 
에서 ME는 COPY 생성자 또는 인스턴스화 된 template 중 하나에 전달 될 수 있다.
COPY 생성자를 호출할려면 매개변수 타입으로 전달된 ME를 const를 붙여서 처리해야하지만, 인스턴스화 된 template는 그럴 필요가 없다.
즉, 인스턴스화 된 template가 오버로딩 된 함수들 중 가장 잘 매칭되는 경우가 된다.
Compiler 역시 가장 잘 매칭된 함수 (better-matching function)을 호출하도록 설계되어 있으므로,
Item 타입의 non-const L-Value의 전달은 COPY 생성자가 아닌 Perfect-forwarding 생성자로 전달된다.

전달되는 개체가 const가 되도록 수정한다면, 상황은 완전 다르게 된다.

const Item ME("Luna"); // Object is now const
auto CloneofME(ME);    // Call Copy ctor Exactly

이제 전달되는 개체가 const가 되었기 때문에, COPY 생성자로 정확하게 매칭된다.
물론 인스턴스화된 template생성자도 똑같은 형태로 가능하지만,
오버로딩 결정 규칙에 의하면 인스턴스화된 template와 template가 아닌 일반 함수가 똑같이 정의되어 있다면, 일반함수가 우선적으로 실행된다.
COPY 생성자(일반 함수)가 동일한 형태의 인스턴스화된 template에게 승리한다.

Perfect-forwarding 생성자와
컴파일러가 자동으로 생성해주는 COPYMOVE 연산자 들의
상호작용도 이미 완전히 복잡한데,
여기에다가 상속까지 짬뽕되버리면
그걸 이해하려고 고생하느라 얼굴에 주름살이 자글자글해질 것이다.

Item을 상속받은 파생 class의 COPYMOVE 작업에 대해서 살펴보자.

class ItemEx : public Item {
public:
    ItemEx(const ItemEx& item) // Copy ctor
    : Item(item) {}            // Calls base class forward ctor

    ItemEx(ItemEx&& item)      // Move ctor
    : Item(std::move(item)) {} // Calls base class forward ctor
};

ItemEx class의 COPYMOVE 생성자는 Item class의 COPYMOVE 생성자를 호출하지 않고, Perfect-forwarding 생성자를 호출한다 !
왜 그럴까 ?
파생 class의 함수는 ItemEx 타입의 인자를 기본 class로 전달한기 때문에
기본 class에서는 오버로딩 결정 규칙에 의해 Item class의 인스턴스화된 template에서 작업을 처리하게 된다.
std::string 생성자는 ItemEx를 처리할수 없기 때문에 저 Code Error가 발생한다.

가능하다면 Universal Reference Parameter를 오버로딩해서 쓰지 않길 바란다.
그래도 일부 인자 타입을 특별한 방식으로 처리해야할 필요가 있어서
대부분의 인자 티입을 Forwarding 하는게 필요한 경우
어떻게 해야 할까요 ?

계란으로 스크램블을 만드는 방법은 여러가지다.
사실 앞에서 이런 저런 안되는 얘기들을 많이 한 이유는 item 27 을 위해서 이다.
바로 다음장으로 넘어가자.

Things to Remember
*  Universal Reference를 오버로딩한 뭘 상상하든 그 이상으로 훨씬 더 자주 Universal Reference Overloading이 호출된다.
*  Perfect-forwarding 생성자는 특히 더 문제가 있다. 왜냐면 COPY 생성자보다 non-const L-Value와 더 잘 매칭되는데다가, 파생 클래스에서 기본 클래스의 COPYMOVE생성자를 호출하는 것도 가로채기 때문이다.