티스토리 뷰

 오른값 참조는 이동할 수 있는 객체에만 묶인다. 다시 말하면 어떤 매개변수가 오른값 참조라면, 객체를 이동할 수 있다는 의미이다. 오른값 참조로 캐스팅해주는 객체가 바로 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);

...

setName 함수에 지역변수 n 을 전달해주었다. 작성자는 n 을 읽기 전용으로 사용할 것이라 생각하겠지만, 내부적으로 std::move 함수를 이용하여 무조건 오른값으로 캐스팅한다. 그 결과 setName 함수가 끝난 이후에는 n 은 미지정 값이 된다. 위와 같은 문제를 해결하기 위해 매개변수를 보편참조로 선언하지 않고 함수 오버로딩을 이용해서 해결하자는 생각을 할 수도 있다. 아래와 같이 말이다.

class Widget {
public:
    void setName(const std::string& newName)
    { name = snewName; }
    
    void setName(std::string&& newNmae)
    { name = std::move(newName); }
    
    ...
};

 위 코드가 해결책일 수는 있지만 3가지 단점이 생긴다. 첫째는 유지보수해야 할 소스 코드의 양이 늘어났다. 둘째는 효율성이 떨어질 수 있다. 예를들어 문자열 리터럴을 파라미터로 전달한다고 생각해보자.

class Widget {
public:
    ...  
    
    void setName(std::string&& newNmae)  // 임시 객체이므로 오른값
    { name = std::move(newName); }       // name으로 이동 후 임시 객체 소멸
    
    ...
};

...

w.setName("Adela Novak");  // 문자열 리터럴 전달 및 임시 객체 생성

 위에 주석에 나와있듯이 불필요한 객체 생성과 소멸이 일어난다. 만약 보편 참조를 사용했더라면 문자열 리터럴이 name 에 대한 배정 연산자의 인수로 쓰였을 것이다. 

 마지막으로 세번째 문제는 코드양이 기하급수적으로 증가한다는 것이다. 더 나쁜 소식은 왼값일 수도 있고 오른값일 수도 있는 매개변수들을 무제한으로 사용하는 함수들이 존재한다는 것이다. 대표적으로 std::make_sharedstd::make_unique 함수가 그렇다. 이런 함수들은 내부적으로 std::move 가 아닌 std::forward 를 사용해야만 한다. 

 보편 참조이든 오른값 참조이든 함수 내부적으로 여러번 사용하는 경우가 있을 것이다. 이러한 경우에는 반드시 마지막에만 std::move std::forward 사용해야만 한다.

template<typename T>
void setSignText(T&& text)
{
    sign.setText(text);
    
    auto now =
        std::chrono::system_clock::now();
        
    signHistory.add(now,
                    std::forward<T>(text));
}

 위 코드에서 주의해야할 점은 sign.setText 함수에서 text 의 값을 변경하지 않는 것이다. 오직 읽기만 해야한다.

반환에서의 이동

 어떤 함수가 값으로 반환한다고 하자. 그리고 그 값이 오른값 참조이거나 보편 참조에 묶인 객체라면 반환문에 std::movestd::forward 를 사용하는 것이 바람직하다. 두가지 같은 함수를 비교해보자.

Matrix
operator+(Matrix&& lhs, const Matrix& rhs)
{
    lhs += rhs;
    return std::move(lhs);
}

Matrix
operator+(Matrix&& lhs, const Matrix& rhs)
{
    lhs += rhs;
    return lhs;
}

위의 두가지 함수의 역할은 같다. 차이가 있다면 아래 함수에서는 std::move 를 사용하지 않았다는 것이다. 결과적으로 lhs 는 반환할 때 복사가 일어난다. 하지만 위의 함수에서는 복사가 일어나지 않고 이동이 될 뿐이다. 만약 Matrix 객체가 이동이 되지 않는다고 하더라도 문제는 없다. Matrix 는 아래 함수처럼 복사가 일어난다. 추후에 이동을 지원하게 되면 저절로 효율성이 향상될 것이다. 

 이 같은 접근방식을 다른곳에도 적용할 수 있다고 생각할지 모른다. 예시를 보자.

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

 

 언뜻 보기에는 복사를 이동으로 바꾸었으니 성능이 향상될 것이라고 생각이 될 것이다. 그러나 문제가 있다. 위 코드와 같이 지역 변수를 반환하는 함수를 위하여 반환값을 위해 마련한 메모리 안에 객체를 생성한다면 w 의 복사를 피할 수 있다는 것은 예전부터 알려져왔다. 이것을 RVO(return value optimization) 이라고 부른다. 이는 반환 타입과 같은 타입을 가지는 지역변수를 반환값으로 사용하는 경우에 컴파일러가 최적화한다.

 그런데 만약 std::move 를 사용하게 되면 반환하는 객체의 타입이 Widget 이 아닌 std::move(w) 의 결과가 되어버린다. 컴파일러는 RVO 의 조건을 만족하지 않게되어 최적화를 하지 않게 되어 버린다. 이는 성능 하락이 일어날 수 있기 때문에 주의해야한다.

 

참고 서적

스콧 마이어스, 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
글 보관함