현재 작업중인 프로젝트에서 얼마 전부터 vs2010을 사용하고 있다.
 
처음에는 그냥 컴파일러 버전만 좀 올라가고… 코딩에는 고작해야 auto 키워드를 쓰는 것 정도가 다였는데
이제 슬슬 vs2010에서 코딩 하는 양이 많아지면서 C++0x의 새로운 문법들을 자연스럽게 활용해가고 있다.
 
그 중에 요즘 조금씩 느끼고 있는 것 중에 한가지가, r-value reference를 이용한 이동 생성자(move constructor)를 사용하게 되면서, 내가 작성하는 모듈 인터페이스의 설계가 바뀌고 있다는 사실이다.
그 동안 C++에서 객체의 불필요한 복사를 피하기 위해 다년간 체득했던 방식들이 조금씩 변화하는 중.
 
코드로 예를 들어보면 아래와 같은 경우다.
 
// GetInfo 함수는 DWORD로 표현된 ip 주소와 port 정보를 받아
// 문자열로 된 x.x.x.x:xx 방식의 ipv4 주소를 반환합니다.
std::string GetInfo( DWORD dwIP, unsigned short usPort )
{
    SOCKADDR_IN addr;
    addr.sin_addr.s_addr = dwIP;
 
    char szBuff[16] = {0};
 
    // RVO 최적화를 피하기 위해 아래와 같이 코딩했습니다. 
    // 물론 더 효율적인 코딩도 가능합니다. 아래는 예시를 위한 샘플 입니다.
    std::string strBuff;
    strBuff = inet_ntoa( addr.sin_addr );
    strBuff += ":";
    strBuff += itoa( usPort, szBuff, 10 );
 
    return strBuff;
}
 
void main()
{
    std::cout << GetInfo( 0x164e8b, 20 ) << std::endl;
}
 
GetInfo() 함수는 내부에서 뭔가 의미심장한 일을 한 후 그로 인한 string 타입 결과값을 함수의 리턴 값으로 반환한다.
main 함수에서는 이 함수를 스트림 형식으로 출력하는 std::cout의 << 연산자에 그대로 호출하고 있다.
호출하는 곳에선 이렇게 하는 게 훨씬 깔끔하다.
하지만 문제는 GetInfo가 std::string을 반환할 때 복사 생성자가 호출되고, 이때 문자열 데이터가 메모리상에서 불필요하게 복사된다는 점이다.
 
그래서 대부분 어느 정도 숙련된 C++ 개발자라면 오버헤드를 없애기 위해 GetInfo의 인터페이스를 아래와 같이 수정할 것이다.
 
// 반환할 스트링 변수를 레퍼런스로 받아 이곳에 값을 채우도록 변경.
void GetInfo2( std::string& strOut, DWORD dwIP, unsigned short usPort )
{
    SOCKADDR_IN addr;
    addr.sin_addr.s_addr = dwIP;
 
    char szBuff[16] = {0};
 
    strOut = inet_ntoa( addr.sin_addr );
    strOut += ":";
    strOut += itoa( usPort, szBuff, 10 );
 
}
 
void main()
{
    std::string strInfo;
    GetInfo2( strInfo, 0x164e8b, 20 );
    std::cout << strInfo << std::endl;
}
 
그렇죠? 아마 이 글을 찬찬히 보고 있으신 C++ 프로그래머라면 머릿속에 이런 코드를 생각하고 있으셨을 겁니다.
 
그런데 visual studio 2010 부터는, 처음의 인터페이스인 std::string GetInfo(…) 스타일로 작성하고 RVO나 NRVO같은 컴파일 최적화의 도움을 받지 않아도 객체복사가 일어나지 않는다. 그 이유는 std::string 객체의 이동생성자 때문이다.
 
불필요한 메모리 복사의 문제만 아니라면 첫 번째 인터페이스가 사용하기에도 편하고 작성하기도 좀 더 편하다.
컴파일러 처음 갈아타고 나서는 이동생성자를 명시적으로 호출해주기 위해서 반환시점에 return std::move( strBuff ); 와 같은 식으로 처리했는데, 나중에 테스트코드를 돌려보니 std::move()를 사용하지 않아도 이동 생성자가 불린다.
 
근데 아직 함께 일하는 다른 프로그래머들도 새로운 문법에 적응하기 위한 시간을 거치는 중일 테니, 기능상으로는 쓸모 없더라도 문서화의 목적으로 return std::move( 리턴할 객체 ); 식으로 코딩해주는 것도 하나의 센스일 듯.
이 부분은 수정이 조금 필요하다. 실제로 나는 팀에서 이동 생성자를 의도한 코드에서는 std::move(...)를 문서화의 의미로 좀 남발하였는데, 이것은 std::move의 올바른 사용방식이 아니며, r-value의 동작 방식을 제대로 알고 있는 동료들에게는 오히려 혼동을 줄 수도 있겠다는 피드백을 받았다. 
  • 함수의 반환값은 무조건 r-value로 처리된다. 이것으로 인해 리턴값에 std::move를 쓰는 것은 이미 어색한 코드가 된다. 
  • std::move는 l-value를 r-value로 넘기기 위한 경우에만 쓰면 된다. 
  • std::move를 안 쓴다고 해서 move가 가능한 곳이 copy 되어버리는 케이스는 없다. 무슨 말이냐 하면, 객체를 이동 생성 하느냐, 복사 생성 하느냐 하는 이슈는 std::move를 적거나 빼먹었거나 하는 것에 달려있지 않다는 말이다.
  • 따라서, std::move는 처음에 썼던 것처럼 여기저기 막 붙이는 것이 아니라, 사용하지 않고는 컴파일 에러를 해결할 수 없는 케이스에만 사용하면 된다. 
 
어쨌거나 요즘은, 객체 복사의 불안함 때문에 반환값 객체를 레퍼런스로 받도록 하는 함수 인터페이스를 점점 안 쓰게 됐다. 적다가 보니 r-value reference에 대한 공부도 좀 더 깊이 있게 해둬야겠다 싶구나.


ps. r-value의 간단한 개념은 흥배형님의 핸드북에 친절히 설명되어 있다.


Posted by leafbird 트랙백 0 : 댓글 2

댓글을 달아 주세요

  1. addr | edit/del | reply Favicon of https://devnote.tistory.com BlogIcon leafbird 2013.10.03 00:06 신고

    2013.10.13 - 본문 내용 중 std::move(...)의 사용에 대한 부분을 수정.

  2. addr | edit/del | reply Favicon of http://www.npteam.net BlogIcon TTF 2013.10.03 13:42

    좋은 정보 감사합니다.