오버로딩을 포기한다

오버로딩 대신 함수에 각자 다른 이름을 붙인다. 다만 생성자에는 통하지 않는다.

 

 

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

const한정자를 붙이면 보편 참조를 지원하지 않게 되지만 효율성을 조금 포기하더라도 예상치 못한 문제를 피한다는 점에서는 고려할 만하다.

 

 

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

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

복사될 것이 확실한 객체는 참조 전달 매개변수 대신 값 전달 매개변수를 사용한다.

구체적인 작동 방식과 효율성에 대한 논의는 추후 항목 41에서 다룬다.

 

 

꼬리표 배분을 사용한다

위에서 언급한 const 왼값 참조 전달이나 값 전달은 완벽 전달을 지원하지 않는다.

만약 보편 참조를 사용하려는 이유가 완벽 전달이라면 보편 참조 말고는 답이 없다.

 

보편 참조와 오버로딩을 둘 다 사용하되 보편 참조에 대한 오버로딩을 피하기 위해서는 SFINAE 기법과 동일한 꼬리표 배분을 사용하면 된다. 쉽게 이야기 하자면 오버로딩 대신 해당 구현부를 다른 함수들로 위임 시켜서 호출하는 것이다. 다른 함수를 호출하는 만큼 호출 깊이가 하나 증가하게 된다.

 

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

매개변수와 매개변수의 타입이 정수인지를 묻는 인수를 같이 전달한다.

 

template<typename T>
void logAndAddImpl(T&& name, std::false_type) { // 정수타입이 아닐 때
    ...
    names.emplace(std::forward<T>(name));
}

std::string nameFromIdx(int idx);

void logAndAddImpl(int idx, std::true_type) { // 정수타입일 때
    logAndAdd(nameFromIdx(idx));
}

전달된 인수의 타입이 정수타입이 아니면 첫 번째 함수가 호출될 것이고 정수타입이면 두 번째 함수가 호출될 것이다.

 

그런데 오버로딩 해소 과정은 컴파일 타임에 이루어져야 하지만 true, false는 런타임 값이다. 즉, true, false에 해당하는 어떤 타입이 컴파일 타임에 존재해야 하는 것이고 이를 표준 라이브러리가 std::true_type, std::false_type으로 제공해준다.

해당 타입 매개변수들은 런타임에 전혀 쓰이지 않기 때문에 이름조차 붙이지 않는다.

 

위와 같은 개념(SFINAE)은 템플릿 메타 프로그래밍의 표준 설계 요소이다.

 

 

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

꼬리표 배분의 필수 요소는 인터페이스 역할을 하는 단일(오버로딩 되지 않은) 함수이다.

꼬리표 배분을 통해 웬만한 문제는 해결되었을 거라 생각되지만 완벽 전달 생성자에 관련된 문제점은 아직 해결하지 못했다.

 

만약 생성자를 하나만 작성하고 그 안에서 꼬리표 배분을 사용하려고 했을 때, 컴파일러에 의해 자동으로 작성된 다른 생성자들에 의해 생성자가 오버로딩 되어 꼬리표 배분이 적용되지 않을 가능성이 존재한다.

 

전 항목에서 언급했듯이 파생 클래스의 복사, 이동 생성자의 멤버 초기화 리스트가 기반 클래스의 생성자를 호출하게 되면 각각 복사, 이동 생성자가 호출되기를 기대할 것이다. 하지만 기반 클래스에 완벽 전달 생성자가 정의되어 있는 경우라면 둘 다 완벽 전달 생성자가 호출된다.

 

꼬리표 배분 설계가 적합하지 않은 경우에는 std::enable_if를 사용하여 템플릿을 제어할 수 있다.

기본적으로 모든 템플릿은 활성화된 상태이지만 std::enable_if를 사용하는 템플릿은 오직 해당 std::enable_if가 만족될 때에만 활성화된다.

 

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

필요할 때만 완벽 전달 생성자가 호출되게 하려면 Person이 아닌 타입의 인수가 전달된 경우에만 Person의 완벽 전달 생성자가 활성화 되도록 해야한다. 이럴 때 유용한 type_traits으로 두 타입이 같은지를 판단하는 std::is_same이 있다.

이 부분을 위 코드의 조건에 넣으면 될 것이다.

 

<!std::is_same<Person, T>::value>

 

Person p("Nancy");
auto cloneOfP(p); // 왼값 초기화

보편 참조 생성자의 타입 T는 Person&으로 추론되고 Person과 Person&는 같은 타입이 아니라고 판단한다.

 

 

!std::is_same<Person, Person&>::value // 둘의 타입이 달라서 false가 되고 not 연산자에 의해 true가 된다

Person의 템플릿 생성자가 T가 Person이 아닐 때에만 활성화 되어야 하는 조건을 좀 더 구체적으로 생각해보면 다음 두가지 사항을 무시해야 한다는 점을 알 수 있다.

 

◾ 참조 여부. 보편 참조 생성자의 활성화 여부를 판단할 때, Person, Person&, Person&&은 모두 같은 타입으로 간주되어야 한다.

◾ const성과 volatile성. 마찬가지로 const Person, volatile Person, const volatile Person 모두 Person과 같은 타입으로 간주되어야 한다.

 

이게 성립되려면 모든 참조 한정사와 const, volatile 한정사를 제거하는 수단이 필요한데 이 역시 type_traits의 std::decay가 제공해준다.

 

std::decay<T>::type은 T에서 모든 참조와 모든 cv한정사를 제거한 타입을 알려준다.

결론적으로 생성자의 활성화를 제어하는 조건은 다음과 같다.

 

!std::is_same<Person, typename std::decay<T>::type>::value

즉, Person이 모든 참조와 cv한정사를 제거한 T와 같지 않아야 한다.

 

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);
    ...
};

위의 내용을 정리해서 작성한 코드이다. 보기만 해도 어지러움을 느낀다.

그런데 아직도 모든 문제가 해결되지 않았다.

파생 클래스의 복사, 이동 생성자에서 기반 클래스의 생성자를 호출 시 전달된 인수는 당연히 기반 클래스와 파생 클래스의 타입이 다르기 때문에 템플릿이 활성화 되어 여전히 기반 클래스의 완벽 전달 생성자가 호출된다.

 

파생 클래스에서는 복사 생성자와 이동 생성자를 구현할 때 통상적인 규칙을 적용하여 구현한 것이기 때문에 기반 클래스에서 문제를 해결해주어야 한다. 고쳐야 할 부분은 보편 참조 생성자가 활성화되는 조건이다.

물론 그 조건은 type_traits가 std::is_base_of라는 것을 또 제공해준다. 이제는 놀랍지도 않다.

 

std::is_base_of<T1, T2>는 T2가 T1의 파생 클래스이면 참이다.

위의 is_same을 is_base_of로 바꿔주기만 하면 대장정이 거의 끝난다. 여기까지 했는데도 끝이 아니라 거의 끝이다.

 

class Person { // C++11
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);
    ...
};

class Person { // C++14
public:
    template<
        typename T,
        typename = std::enable_if_t<
                       !std::is_base_of<Person, std::decay_t<T>>::value
                   >
    >
    explicit Person(T&& n);
    ...
};

C++14 이상이라면 std::enable_if와 std::decay의 별칭을 이용해 뒤에 ::type을 없앰으로써 조금 더 간결하게 작성할 수 있다. 이미 이런 형태 자체가 간결한지는 잘 모르겠지만 말이다.

 

맨 처음 제기되었던 문제로 되돌아가서 정수 타입 인수와 비정수 타입 인수를 구분하는 방법까지 적용시켜야 정말 완성된다.

 

class Person { // C++14
public:
    template<
        typename T,
        typename = std::enable_if_t<
                    !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)) {}
    explicit Person(int idx) : name(nameFromIdx(idx)) {} // 정수 인수를 위한 생성자
    ... // 기타 복사, 이동 연산자들
private:
    std::string name;
};

드디어 생성자 오버로딩을 사용하면서 완벽 전달까지 사용되는 최대 효율의 코드가 완성되었다.

보편 참조에 대한 오버로딩을 금지하는 대신 둘의 조합을 제어하므로 오버로딩을 피할 수 없는 상황에서도 사용할 수 있다.

 

 

절충점들

최초 세가지 기법들인 오버로딩 포기, const T& 전달, 값 전달은 호출되는 함수들의 각 매개변수에 대해 타입을 지정한다.

나머지 두 기법들인 꼬리표 배분과 템플릿 활성화 제한은 완벽 전달을 사용하므로 매개변수들의 타입을 지정하지 않는다.

 

완벽 전달은 임시 객체를 생성하는 비효율성이 없기 때문에 더 효율적이지만 완벽 전달이 불가능한 인수들이 존재한다. 이 내용은 항목 30에서 다루도록 하고 또 하나의 단점으로 유효하지 않은 인수들을 전달했을 때, 출력되는 에러 메시지가 난해하다는 점이다.

 

보편 참조가 전달되는 횟수가 많을수록 에러 메시지가 더 장황해진다. 그래서 보편 참조를 성능이 최우선적인 인터페이스에만 사용하는 것만으로 충분하다는 의견들도 있다.

 

◾ 보편 참조와 오버로딩의 조합에 대한 대안으로는 구별되는 함수 이름 사용, 매개변수를 const에 대한 왼값 참조로 전달, 매개변수를 값으로 전달, 꼬리표 배분 사용 등이 있다.

◾ std::enable_if를 이용해서 템플릿의 인스턴스화를 제한함으로써 보편 참조와 오버로딩을 함께 사용할 수 있다. std::enable_if는 컴파일러가 보편 참조 오버로딩을 사용하는 조건을 프로그래머가 직접 제어하는 용도로 쓰인다.

◾ 보편 참조 매개변수는 효율성 면에서 장점인 경우가 많지만, 대체로 사용성 면에서는 단점이 많다.

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

void logAndAdd(const std::string& name) {
    ...
    names.emplace(name);
}

std::string petName("Darla");
logAndAdd(petName); // 왼값
logAndAdd(std::string("Persephone")); // 오른값
logAndAdd("Patty Dog"); // 문자열 리터럴

인수가 왼값인 경우 emplace의 복사는 피할 수 없다.

 

인수가 오른값인 경우 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어진다. 하지만 인수가 오른값이기 때문에 원칙적으로는 복사 대신 이동을 적용할 여지가 있다.

 

인수가 리터럴 문자열인 경우 두번째 경우와 마찬가지로 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어지며 이동을 적용할 여지가 있다. 약간의 차이가 있다면 두번째 경우는 명시적인 임시 string 객체를 생성하고 매개변수가 거기에 묶였지만, 리터럴 문자열은 암묵적으로 생성된 임시 string 객체에 name이 묶인다.

만약 문자열 리터럴을 직접 emplace에 전달했다면 multiset 안에서 직접 string 객체를 생성했을 것이다. 애초에 복사나 이동 비용을 치를 필요가 없던 것이다.

 

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

template<typename T>
void logAndAdd(T&& name) {
    ...
    names.emplace(std::forward<T>(name));
}

std::string petName("Darla");
logAndAdd(petName); // 왼값 복사
logAndAdd(std::string("Persephone")); // 오른값 이동
logAndAdd("Patty Dog"); // 임시객체 복사 대신 multiset 안에 string 생성

보편 전달을 적용함으로써 최적화를 이뤄낼 수 있다.

 

이번에는 인수가 문자열이 아니라 정수타입 변수를 통해 인덱스를 전달하고 함수 내에서 해당 인덱스를 통해 이름을 가져오는 함수를 오버로딩 한다고 해보자.

 

std::string nameFromIdx(int idx); // idx에 해당하는 이름 반환

void logAndAdd(int idx) {
    ...
    names.emplace(nameFromIdx(idx));
}

logAndAdd(22);

잘 해소된것처럼 보이지만 short 타입을 전달하면 에러가 발생한다.

short타입을 캐스팅 등을 통해 int타입으로 변환하지 않는 이상 보편 참조를 받는 버전의 함수가 호출된다.

이 때, T&&는 short&로 추론되며 name에 묶이고 emplace 함수를 호출할 때, name을 string 생성자에 전달하지만 short타입을 받는 생성자는 존재하지 않기 때문에 에러가 발생하는 것이다.

 

보편 참조를 받는 함수 템플릿은 생각보다 많은 타입들을 빨아들이기 때문에 오버로딩과 결합하는 것은 웬만해서는 나쁜 선택이 된다.

 

이런 문제를 쉽게 피하는 방법 중 하나는 완벽 전달(Perfect Forwarding) 생성자를 작성하는 것이지만 더 심각한 문제를 야기할수도 있다.

 

class Person {
public:
    template<typename T>
    explicit Person(T&& n) : name(std::forward<T>(n)) {} // 완벽 전달 생성자
    explicit Person(int idx) : name(nameFromIdx(idx)) {}
    
    Person(const Person& rhs); // 복사 생성자
    Person(Person&& rhs); // 이동 생성자
private:
    std::string name;
};

Person p("Nancy");
auto cloneOfP(p); // 복사 생성자 호출?

완벽 전달 생성자, 복사 생성자, 이동 생성자 모두 정의되어있는 클래스이다.

cloneOfP는 복사 생성자를 호출함으로써 객체를 생성하려고 했지만 실제로 호출되는 것은 완벽 전달 생성자이다.

 

class Person {
public:
    explicit Person(Person& n) : name(std::forward<Person&>(n)) {}
    ...
};

cloneOfP의 생성자는 Person& 타입의 매개변수를 받는 형태로 인스턴스화 된다. p가 비const 객체이기 때문에 복사 생성자보다는 완벽 전달 생성자의 매개변수 타입에 더 부합하기 때문에 완벽 전달 생성자가 호출된다.

만약 p가 const 객체였다면 복사 생성자의 매개변수 타입에 부합하므로 복사 생성자가 호출될 것이다.

 

class Person {
public:
    explicit Person(const Person& n);
    Person(const Person& rhs);
    ...
};

그런데 p가 const객체로 전달되는 경우 생성자 템플릿의 매개변수가 복사 생성자와 똑같아진다. 이런 경우 템플릿 함수보다 비템플릿 함수(일반적인 함수)의 호출을 우선시한다는 규칙에 의해 자동으로 작성된 복사 생성자가 호출된다.

 

class SpecialPerson : public Person {
public:
    SpecialPerson(const SpecialPerson& rhs) : Person(rhs) {}
    SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) {}
    ...
};

상속이 관여하는 클래스에서는 완벽 전달 생성자와 컴파일러가 작성한 복사 및 이동 연산들 사이의 상호작용이 미치는 여파가 더욱 크다.

파생 클래스의 복사, 이동 생성자가 기반 클래스의 복사, 이동 생성자를 호출하는 것 처럼 보이지만 실제로는 둘 다 완벽 전달 생성자를 호출하고 있다.

 

보편 참조에 대한 함수 오버로딩에 대한 해결책은 다음 항목에서 다루도록 한다.

 

◾ 보편 참조에 대한 오버로딩은 거의 항상 보편 참조 오버로딩 버전이 예상보다 자주 호출되는 상황으로 이어진다.

◾ 완벽 전달 생성자들은 특히나 문제가 많다. 그런 생성자는 대체로 비const 왼값에 대한 복사 생성자보다 더 나은 부합이며, 기반 클래스 복사 및 이동 생성자들에 대한 파생 클래스의 호출들을 가로챌 수 있기 때문이다.

오른값 참조를 다른 함수에 넘겨줄때는 std::move를 사용하고 보편 참조를 다른 함수에 넘겨줄때는 std::forward를 사용해야 한다.

 

오른값 참조에 std::forward를 사용하는 것은 동작 자체는 문제가 없지만 실수의 여지가 있다. 보편 참조에 std::move를 사용하는것은 아예 좋지 않은 결과가 초래될 수 있다.

 

class Widget {
public:
    template<typename T>
    void setName(T&& newName) { name = std::move(newName); } // 보편 참조를 move로 이동
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

std::string getWidgetName(); // 팩토리 함수
Widget w;
auto n = getWidgetName();
w.setName(n); // 호출이 끝나면 n의 값은 알 수 없어진다

const string&을 받는 함수를 오버로딩 하면 되는거 아닌가? 싶겠지만 리터럴 문자열을 인수로 넘겨줬을때 기존에는 발생하지 않았던 비용이 발생하는 것을 생각하면 그다지 올바르지 않다.

그보다는 코드의 확장이 나쁘다는것이 더 큰 문제이다. 지금은 매개변수가 고작 하나라서 함수 오버로딩이 두개면 됐지만 여러개이고 심지어 왼값, 오른값 모두 존재한다면 오버로딩의 수가 기하급수적으로 늘어난다.

이런 문제를 피하기 위해서 보편 참조에 대해서는 대체로 std::forward를 사용해야 한다.

 

'항상'이 아니라 '대체로' 라고 했다. 그 이유는 오른값 참조나 보편 참조에 묶인 객체는 함수 내에서 여러번 사용될수도 있기 때문에 전부 다 사용하기 전에 이동되는 일은 피해야 하기 때문이다. 그래서 std::move나 std::forward를 적용하는 것은 해당 참조의 마지막 사용이어야 한다.

 

또한 std::move의 경우 경우에 따라 std::move_if_noexcept를 사용하는 게 바람직한 경우도 존재한다. (항목 14 참조)

만약 함수가 결과를 값으로 돌려주고 해당 값이 오른값 참조나 보편 참조에 묶인 객체라면 std::move 또는 std::forward를 적용시켜서 반환하는 것이 바람직하다.

 

그런데 함수 내에서 선언된 지역 변수를 반환하는 경우에도 std::move를 적용하면 복사 연산 대신 이동 연산이 이루어 질거라는 확대 해석을 해서는 안된다.

 

Widget makeWidget() {
    Widget w;
    ...
    return std::move(w);
}

이런 경우는 굳이 std::move를 사용하지 않고 return w만 하더라도 반환값 최적화(RVO)에 의해 복사가 제거되어 복사가 이뤄지지 않는다.

다만 RVO가 적용되려면 지역 객체의 타입이 함수의 반환 타입과 같아야 하고 그 지역 객체가 함수의 반환값이어 한다.

위의 경우는 둘 다 해당되기 때문에 RVO가 적용된다.

 

굳이 std::move를 적용해서 반환하면 타입이 달라져서 RVO의 조건이 충족되지 않기 때문에 오히려 최적화 여지를 제한하게 되는 결과를 낳게된다.

 

RVO가 적용되더라도 컴파일러가 복사 연산을 반드시 제거하지는 않는다. 하지만 이런 경우라면 반환되는 객체는 반드시 오른값으로 취급되어야 한다고 정해져 있으므로 굳이 우리가 명시적으로 std::move를 사용할 필요는 없다.

어차피 RVO가 적용되는경우 복사 연산이 제거되거나 그렇지 않더라도 암묵적으로 반환값에 std::move 연산을 수행해주기 때문이다.

 

◾ 오른값 참조나 보편 참조가 마지막으로 쓰이는 지점에서, 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 적용하라.

◾ 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄때에도 각각 std::move나 std::forward를 이용하라.

◾ 반환값 최적화(RVO)의 대상이 될 수 있는 지역 객체에는 절대로 std::move나 std::forward를 적용하지 말아야 한다.

T&&가 오른값 참조라고 가정하는것은 당연한 것 같지만 안타깝게도 그렇지는 않다.

 

void f(Widget&& param); // 오른값 참조
Widget&& var1 = Widget(); // 오른값 참조

auto&& var2 = var1; // 오른값 참조 아님

template<typename T>
void f(std::vector<T>& param); // 오른값 참조

template<typename T>
void f(T&& param); // 오른값 참조 아님

T&&에는 두 가지 의미가 있다. 하나는 오른값 참조이고 다른 하나는 오른값 참조 또는 왼값 참조 중 하나라는 것이다.

오른값 참조인 것처럼 보여도 상황에 따라서는 왼값 참조로 동작한다.

이런 이중적인 참조는 const, volatile 객체 또는 둘 다 해당되는 객체에 묶일수도 있다. 거의 모든것에 묶을 수 있는 유연한 참조이고 이를 보편 참조(universal reference)라고 부른다. 거의 항상 std::forward를 사용하기 때문에 전달 참조(forwarding reference)라고 부르기도 한다.

 

template<typename T>
void f(T&& param);

auto&& var2 = var1;

보편 참조는 보통 타입 추론이 일어나는 템플릿 매개변수나 auto 선언에서 나타난다.

 

template<typename T>
void f(T&& param);

Widget w;
f(w); // 왼값 전달. param의 타입은 Widget&
f(std::move(w)); // 오른값 전달. param의 타입은 Widget&&

하나의 참조가 보편 참조이려면 반드시 타입 추론이 관여해야 한다(필요조건). 그렇지만 타입 추론이 관여했다고 해서 반드시 보편 참조가 되는것은 아니고(충분조건X) 참조 선언의 형태도 정확해야 한다.

 

template<typename T>
void f(std::vector<T>&& param); // 오른값 참조. 보편 참조가 아님

template<typename T>
void f(const T&& param); // 오른값 참조. 보편 참조가 아님

param의 타입이 단순히 T&&가 아니라 std::vector<T>&&라면 보편 참조가 될 수 없고 오른값 참조로만 동작한다.

또는 const 한정사 하나만 붙여도 보편 참조가 되지 못한다.

 

 

그런데 또 T&&라고 다 보편참조는 아니다.

 

template<class T, class Allocator = allocator<T>>
class vector { // C++ 표준 벡터
public:
    void push_back(T&& x);
    ...
};

push_back의 매개변수는 보편 참조로 보이지만 push_back은 반드시 구체적으로 인스턴스화된 vector의 일부여야 하기 때문에 타입 추론이 전혀 일어나지 않는다.

 

std::vector<Widget> v; // Widget 타입으로 인스턴스화

class vector<Widget, allocator<Widget>> {
public:
    void push_back(Widget&& x); // 오른값 참조
    ...
};

Widget 타입으로 인스턴스화 되면서 push_back의 매개변수 T&&가 Widget&& 타입으로 결정되고 여기서 타입 추론은 전혀 관여하지 않는다.

 

template<class T, class Allocator = allocator<T>>
class vector {
public:
    template<class... Args>
    void emplace_back(Args&&... args);
    ...
};

이 경우는 T와 독립적이기 때문에 타입 추론이 적용되어서 Args는 보편 참조가 된다.

T가 Args로 바뀌었을 뿐 형태는 똑같다. 그래서 보편 참조의 형태가 T&&라고 할 뿐이다. 정확히는 타입&&이 보편 참조의 형태이다.

 

auto역시 보편 참조가 될 수 있다고 했는데 흔히 사용되지는 않고 C++14 이후의 람다식에서는 자주 사용되는 편이다.

 

auto timeFuncInvocation = [](auto&& func, auto&&... params) { // C++14
    std::forward<decltype(func)>(func)(
        std::forward<decltype(params)>(params)...
    );
};

C++14부터 람다식의 매개변수 타입에 auto를 사용할 수 있게 되면서 보편 참조의 사용이 가능해졌다.

 

◾ 함수 템플릿 매개변수의 타입의 형태가 T&&이고 T가 추론된다면, 또는 객체를 auto&&로 선언한다면, 그 매개변수나 객체는 보편 참조이다.

◾ 선언된 타입의 형태가 정확히 타입&&가 아니거나 타입 추론이 일어나지 않으면 타입&&는 오른값 참조를 뜻한다.

◾ 오른값으로 초기화되는 보편 참조는 오른값 참조에 해당된다. 왼값으로 초기화되는 보편 참조는 왼값 참조에 해당한다.

std::move는 모든 것을 이동하지는 않고 std::forward는 모든 것을 전달하지는 않는다.

둘 다 그저 캐스팅을 수행하는 함수 템플릿일 뿐이다.

 

template<typename T>
typename remove_reference<T>::type&& move(T&& param) {
    using ReturnType = typename remove_reference<T>::type&&;
    
    return static_cast<ReturnType>(param);
}

std::move는 T&&가 왼값 참조인 경우를 고려해서 참조자를 제거한뒤 오른값 참조를 붙여서 항상 오른값이 반환되도록 구현되어있다.

 

전달받은 인수를 그저 오른값으로 캐스팅 하는것뿐이라 move보다 rvalue_cast같은 이름이 더 낫다는 제안도 있었으나 결국은 move라는 이름을 가지게 되었다.

물론 오른값은 이동의 후보가 맞지만 아닌 경우도 존재한다.

 

class Annotation {
public:
    explicit Annotation(const std::string text) : value(std::move(text)) {}
private:
    std::string value;
};

그저 text의 내용을 읽기만 할 것이기 때문에 가급적 const를 붙이면 좋다는 점에 따라 const를 붙이고 move를 통해 복사 대신 오른값을 얻으려고 한다.

컴파일도 잘되고 링크도 잘되는데 의도한 동작이 전혀 이뤄지지 않는다.

move때문에 오른값으로 캐스팅 되는것은 확실하지만 text는 const std::string으로 선언되어있기 때문에 캐스팅 이후에는 오른값 const std::string가 될 것이다.

 

class string {
public:
    string(const string& rhs); // 복사 생성자
    string(string&& rhs); // 이동 생성자
};

그런데 이동 생성자는 const가 아닌 타입에 대한 오른값 참조를 받고, const에 대한 왼값 참조를 const 오른값에 묶는 것이 허용되기 때문에 복사 생성자가 호출된다.

 

생각해보면 const 객체의 이동은 지원해선 안된다. 이동이 된다는것은 기존 객체의 내용을 수정하겠다는 것인데 const 객체의 수정이 이루어지면 const 정확성이 유지되지 않는다. 그래서 언어 차원에서 방지하는 것은 당연한 것이다.

 

이를 통해 알 수 있는것은 이동을 지원할 객체는 const로 선언되어서는 안되고 std::move는 실제로 아무것도 이동시키지 않을 뿐만 아니라 캐스팅된 객체가 이동 자격을 갖추게 된다는 보장도 제공하지 않는다.

그저 확실한 것은 오른값을 확실하게 돌려준다는 것 뿐이다.

 

 

std::forward역시 비슷한 개념이 적용된다. 다만 move는 무조건 오른값으로 캐스팅하지만 forward는 특정 조건이 만족될 때에만 캐스팅한다는 차이가 있다.

 

void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param) {
    auto now = std::chrono::system_clock::now();
    
    makeLogEntry("Calling 'process'", now);
    process(param); // 왼값? 오른값?
}

Widget w;
logAndProcess(w); // 왼값으로 호출
logAndProcess(std::move(w)); // 오른값으로 호출

어떤 함수로 전달된 매개변수의 타입이 오른값 참조라고 하더라도 그 매개변수 자체는 왼값이다.

그래서 위와 같은 경우 호출은 왼값과 오른값으로 구분되었지만 param 자체는 왼값이기 때문에 둘 다 왼값 참조 타입을 매개변수로 받는 process가 호출된다.

이럴 때 logAndProcess 함수에서 process 함수에 param을 인수로 넘겨줄 때 forward를 통해 조건부 캐스팅하여 넘겨주면 본래 가지고 있던 타입으로 전달된다.

 

    ...
    process(std::forward<T>(param));
    ...

 

그럼 forward는 어떻게 인수가 오른값인지를 알 수 있을까?

간단하게 설명하면 해당 정보가 logAndProcess의 템플릿 매개변수 T에 부호화 되어 저장되어 있다.

std::forward로 전달될 때 해당 정보를 복원하여 캐스팅을 수행한다.

 

 

결과적으로 move, forward 둘 다 캐스팅을 수행하는 함수라는 점이다.

다만 move는 항상 캐스팅하지만 forward는 조건부 캐스팅이라는 차이가 있다.

 

move대신 forward만 사용해도 문제가 없지만 여러가지 이유로 권장하지 않는다.

 

◾ std::move는 오른값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.

◾ std::forward는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.

◾ std::move와 std::forward 둘 다, 실행시점에서는 아무 일도 하지 않는다.

+ Recent posts