본문 바로가기

3.구현/C or C++

[c++] std::move와 std::forward 사용하기

들어가기

c++11에서 std::move와 std::forward는 왜 사용하는지 이해하기 힘들었다. 어쩌면 오른쪽 참조를 많이 사용하지 않아서? 혹시 보편참조가 무서워서 사용하지 않아서인가? std::move와 std::forward을 왜 사용하는지 이해하고 어떻게 사용할지 알아보자.

작성자: ospace114@empal.com, http://ospace.tistory.com/

std::move

std::move의 역활은 단순하다. 오른값으로 캐스팅하는 함수이다. 아래 처럼 일반적인 왼값도 오른값으로 캐스팅할 수 있다.

void f(Box&& box) {}

Box box;
f(std::move(box)); // 오른값 참조 호출

std::move의 동작방식을 이해하면 좀더 쉽게 이해할 수 있다. C++11에서 구현된 std::move을 보자.

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

std::move 함수는 보편 참조로 인자를 받고 반환 형식은 오른값 참조이다. 인자가 보편 참조이므로 입력 값이 왼값이면 왼값 참조가되고 오른값이면 오름값 참조가 된다. std::remove_reference에 의해서 참조 특질은 제거된 상태에서 다시 “&&”참조가 적용되고 오른값 참조가 반환이 된다. 정리하면 입력된 값에 참조 특질은 모두 제거된 상태에서 다시 오른값 참조로 캐스팅되서 반환된다.

여기서 move라는 의미가 이동 연산 작업을 위해 캐스팅하는 의미로 해석할 수 있지만, 궁극적으로 하는 작업으로 오른값 캐스팅이 된다. 그래서 move보다 rvalue_cast가 더 적합하다는 말도 있다.

std::move 사용할 때에 주의할 점은 const형 객체는 std::move을 사용해도 제대로 적용되지 않는다. std::move로 캐스팅한다고 해도 실제 이동작업이 아니라 오른값 참조로만 변경되었다.

Box 클래스가 있고 string을 받아서 생성하는 예를 보자.

class Box {
public:
    Box(const std::string& value) : value(value) {}
    Box(std::string&& value) : value(value) {}
private:
    std::string value;
};

문자열을 받아서 value에 저장한다. std::string이 오른값과 왼값에 따라 초기화하도록 구현했다고 하자. C++에 어느정도 익숙해있다면 고정된 값은 const로 사용하면 된다고 알고 있다.

const std::string msg = "Hello World.";
Box box1(msg);            // (1)
Box box2(std::move(msg)); // (2)

(1)인 경우 상수형 문자열이기에 당연히 Box에서 왼값 초기화에 해당하는 Box(const std::string&) 생성자를 호출하게 된다.

(2)는 move로 오른값으로 변경되었기에 Box(Box&&) 생성자를 호출할거라 기대하고 있다. 실제로는 왼값으로 Box(const std::string&) 생성자를 호출한다.

이처럼 기대와는 다른게 동작할 수 있다. 즉 std::move가 오른값 참조를 호출한다고 생각하면 안된다.

std::move은 오른값 참조가 사용되는 경우에 명시적으로 사용하는게 좋다. 호출시점에 넘겨지는 인자가 오른값 참조를 받는 함수를 호출하고 싶다면 std::move을 사용한다.

std::forward

std::forward는 주어진 인수가 오른값이면 오른값으로 캐스팅하고 아니면 그대로 반환한다. 즉 std::forward는 오른값 조건에서만 오른값 캐스팅을 하는 조건부 캐스팅 함수이다. 이는 호출 시점에 그때마다 달라지는 경우가 발생한다. 대표적인 예가 보편 참조이다. 보편 참조에서 활용되는 전형적인 사용법을 살펴보자.

void pack(const Box& box);
void pack(Box&& box);

template<typename T>
void packing(T&& param) {
    // 중략
    pack(param);
}

실제 사용하는 예를 보자.

Box box;
packing(box);            // (1)
packing(std::move(box)); // (2)

(1)은 왼값 호출하고 (2)은 오른값 호출을 한다. 실제 호출되는 pack()은 왼값이 오버로딩된 pack() 함수이다. 이는 보편 참조를 인수로 받는 packing()에서 인수를 받게 되면 타입 T는 왼값 또는 오른값으로 형식 연역되지만 param은 단순 객체일 뿐이다. pack() 호출 시점에는 왼값으로만 호출된다.

이렇게 제대로된 형식으로 호출하려고할 때에 std::forward으로 조건부 캐스팅에 의해서 T가 오른값이면 오른값 캐스팅을 수행하면 된다. 수정된 packing()을 보자.

template<typename T>
void packing(T&& param) {
  // 중략
  pack(std::forward<T>(param));
}

또는 decltype을 사용해서도 가능하다.

template<typename T>
void packing(T&& param) {
  // 중략
  pack(std::forward<decltype(param)>(param));
}

std::forward로 좀더 명확하게 의미를 컴파일러에게 전달할 수 있게 된다.

반환값 최적화

컴파일러 최적화 기법 중에서 반환값 최적화(return value optimization, RVO)가 있다. 이는 함수 결과와 반환되는 값 형식이 같다면 메모리 안에 생성해서 복사를 피하는 최적화 기법이다. 조건이 함수 결과 형식과 지역 객체 형식이 같고 지역 객체가 반환되는 경우이어야 한다.

Box createBox() {
    Box box;
    //...
    return box;
}

반환값 최적화로 복사가 아닌 이동을 수행한다고 볼 수 있다. 먄약 std::move을 사용해서 오른값 참조로 반환하는 경우가 있을 수 있다.

Box createBox() {
    Box box;
    //...
    return std::move(box);
}

보기에는 반환값 최적화한다고 생각할 수 있지만, std::move에 의해 리턴되는 객체는 지역 객체가 아니기 참조이기 때문에 최적화를 수행하지 않는다.

반환값 최적화는 컴파일러가 반드시 수행하는 최적화는 아니다. 컴파일러마다 달라질 수 있다. 그럼 최적화를 하지 않고 std::move을 사용해 복사 없이 사용할 수 있지 않을까?

그러나 지역객체에 std::move을 사용하는 작업은 위험하다. 지역 객체는 소멸되기 때문에 참조나 포인터로 리턴할 수 없는 이유와 동일하다. 표준에서는 반환값 최적화 조건이 맞다면 반환객체에 대해 복사 제거가 되어 반환되거나 암묵적으로 std::move 적용된 것 처럼 오른값으로 반환된다. 이는 함수 매개변수를 반환하는 경우도 동일하다.

Box createBox(Box box) {
    //...
    return box;
}

위의 코드는 아래 코드와 동일하다고 볼 수 있다.

Box createBox(Box box) {
    //...
    return std::move(box);
}

마무리

std::move와 std::forward가 없어도 잘 작동하는 경우도 있다. 즉, 모든 곳에 반드시 사용해야하는 필수는 아니다. 앞에서 보이듯이 이동 연산을 호출하는게 아니라 복사 연산을 호출하는 차이라고 볼 수 있다. 의도와는 다르게 두 참초 간에 전혀 다른 기능을 구현하지 않았다면 동작 상에는 큰 문제가 없을 수도 있다. 단지 성능 상에 기대와 달라질 수 있다. 그러나 정확히 개발자가 의도한대로 동작하려고한다면 정확히 알고 적용하는게 좋다.

부족한 글이지만 여러분에게 도움이 되었으면 하네요. 즐거운 코딩생활 되세요. ospace.

참조

[1] 스콧 마이어스 저, 류광 역, Effective Modern C++, 인사이트

반응형