이 글은 Herb Sutter의 Elements of Modern C++ Style 을 번역하면서 약간의 변경을 가해본 것이다. 원문은  http://herbsutter.com/elements-of-modern-c-style/ 을 참고하기 바란다.



c++11 을 제대로 쓰려면, 아예 새로운 언어를 배워서 쓴다고 생각하고 시작하는게 좋다. 기존의 포인터, 컨테이너, 알고리즘에 대해 생각하던 것과 달리 접근할 필요가 있다.

언어를 배우는데 중요한 것은 개별적인 문법을 배우는 것이 아니라, 그 문법들이 조합되서 사용되는 패턴, 즉 idiom들을 배우는 것이다. 그런 관점에서 예전 c++의 디자인 문제로 인해 잘 사용될 수 없거나 비실용적이었던 부분들을 이제 다시 새로운 눈으로 볼 때가 되었다.

1. 컨테이너를 함수의 리턴값으로 전달받기
==================================

   전통적인 c에서 많은 양의 메모리를 프로그램의 한 부분에서 다른 부분으로 전달할 때 사용되었던 방법은 포인터이다. 포인터는 사실상 어셈블리어를 다르게 표현한 것이나 마찬가지이며, 효율적이면서도 남용되기 쉽다. c++에서 많은양의 메모리란 것은 결국 오브젝트들을 담고 있는 컨테이너로 표현하게 된다. 그런데, 막상 c++의 문법과 STL의 구현방식은 효율적인 조합이 되지 않았다.
   가령, std::vector<Widget>를 리턴값으로 받게 될 때에는 불필요하게 추가적인 복사가 일어난다.  그래서 API에서 함수의 리턴값으로 STL 컨테이너를 받게 만드는 코드는 최적화의 기본을 모르는 초보의 코드에서나 볼 수 있었다. 그 대신 쓸 수 있는 방법은 두가지가 있다.

1) 포인터를 사용한 방식 : api가 컨테이너를 새로 할당해서 리턴한다. 이 경우의 단점은 내가 delete를 반드시 해야 한다는 점이다. 까먹기 쉽다 

// C++98: alternatives to avoid copying
vector<int>* make_big_vector(); // option 1: return by pointer: no copy, but don't forget to delete
:::
vector<int>* result = make_big_vector();

2) 레퍼런스를 이용한 방식. 내가 컨테이너를 미리 만들어놓고, 그 컨테이너의 레퍼런스를 함수에 전달해서 거기다 채워넣게 하는 식의 코드를 써야 했다. 그렇게 짜면 효율은 떨어지지 않지만 코드는 보기가 좋지 않다.

void make_big_vector( vector<int>& out ); // option 2: pass out by reference: no copy, but caller needs a named object
:::
vector<int> result;
make_big_vector( result );

c++11에서는 위와 같이 그냥 리턴값으로 받으면 된다. 불필요한 복사가 일어나지도 않고, 코드도 깔끔하며, 동적할당을 신경쓸 필요도 없다.
 
// C++11: move
vector<int> make_big_vector(); // usually sufficient for 'callee-allocated out' situations
:::
vector<int> result = make_big_vector();

2. auto
=======

auto 는 가장 중요한 변화다. 알다시피 타이핑을 매우 줄여주는 역할을 한다. 

가령, container 의 iterator를 선언할 때, 예전에는 이렇게 했다.

// C++98
map<int,string>::iterator i = m.begin();
 
c++11 에서는 훨씬 쉽다.

// C++11
auto i = begin(m);

auto는 단순히 타이핑을 줄여주는 것이 용도가 아니다. 다음과 같은 예를 생각해보자

// C++11
auto x = [](int i) { return i > 42; };

위의 경우 x 의 정확한 타입은 무엇일까? 예전 c++코드와 비교해보자

// C++98
binder2nd< greater<int> > x = bind2nd( greater<int>(), 42 );

위와 같은 식이다보니, 아예 저런 기능은 잘 안 쓰거나 배우지 않게 된다. 이제는 저런 코드를 피할 필요가 없다. 정확한 타입을 모르고도 필요한 일은 모두 할 수 있을 것이다.

3. Lambda 함수
=============

람다함수야말로 코드를 빠르고 짧고 유연하게 만들어주는 결전병기다. 람다함수를 가장 써먹기 좋은 부분은 STL 알고리즘들이다. 

예를 들어 x 보다 크고 y보다 작은 첫번째 원소를 찾는다고 하자. 예전방식이라면 

// C++98: write a naked loop (using std::find_if is impractically difficult)
vector<int>::iterator i = v.begin(); // because we need to use i later
for( ; i != v.end(); ++i ) {
    if( *i > x && *i < y ) break;
}

위와 같이 장황한 코드를 써야 한다. 람다와 STL 알고리즘, auto가 결합되면 훨씬 짧고 보기도 편해진다

auto i = find_if( begin(v), end(v), [=](int i) { return i > x && i < y; } );

STL 알고리즘에는 유용한 것들이 많았지만 막상 그걸 쓰려면 functor들을 일일이 만들어줘야 했기 때문에 차라리 그냥 루프를 짜는 것이 더 편했다. 람다가 생기고나서야 드디어 STL의 알고리즘이 제대로 쓸만한 것이 되었다.

4. smart 포인터: 이제 delete는 쓸 필요가 없다
=======================================

c++을 쓰면서 new와 delete때문에 실수와 고생을 안해본 사람은 없을 것이다. 그렇다고 java처럼 강제로 GC가 돌아가는 환경을 택하는 것도 곤란하긴 마찬가지다. 처음부터 스마트 포인터를 제대로 써가면서 코딩을 하면 모든 문제는 해결된다. 기존의 포인터는 정말 속도가 중요한 루프 안에서, 캡슐화된 코드의 구현 내부에서만 쓴다고 마음을 먹자.

// C++98
widget* pw = new widget();
:::
delete pw;
 
// C++11
auto pw = make_shared<widget>();

다른 오브젝트를 소유하는 것이 아니라, 단지 참조만 한다면 weak_ptr를 이용해야 한다. 이렇게 하지 않으면 순환참조가 생기게 된다.

// C++11
class gadget;
 
class widget {
private:
    shared_ptr<gadget> g; // if shared ownership
};
 
class gadget {
private:
    weak_ptr<widget> w;
};

소유와 공유를 구분해주는 것은 디자인적으로도 중요한 일이다. 오브젝트의 소유자가 단 하나일 경우에는 unique_ptr 를 사용하면 된다. 가령 클래스의 구현을 헤더파일에서 빼내고 싶을때 pimpl idiom을 쓰는데 이 포인터는 unique_ptr로 간단히 구현된다

// C++11 Pimpl Idiom
class widget {
    :::
    ~widget();
private:
    class impl;
    unique_ptr<impl> pimpl;
};
 
// in .cpp file
class impl {
    :::
};
 
widget::widget()
    : pimpl( new impl() )
{
}
 
widget::~widget() = default;

몇가지 규칙만 지켜주면 delete문제에서 완전히 벗어날 수 있다. 

5. 그 외
=======

nullptr는 기존의 NULL이나 0 대신 꼭 써주도록 하자.
// C++98
int* p = 0;
 
// C++11
int* p = nullptr;

Range for

for를 아주 깔끔하게 만들어줄 문법이다.

// C++98
for( vector<double>::iterator i = v.begin(); i != v.end(); ++i ) {
    total += *i;
}
 
// C++11
for( auto d : v ) {
    total += d;
}

타이핑도 엄청나게 줄어들었다. 마치 ruby 나 python 문장을 보는 것 같기도 하다

nonmember begin 과 end

vector<int> v;

이런게 있다고 하면 v.begin() 과 begin(v)는 같은 역할을 한다. 전자가 member begin이고 후자가 nonmember begin이다.

nonmember begin과 end는 member보다 훨씬 좋으므로 항상 이쪽을 쓰도록 하자. 이렇게 하면 훨씬 코드가 통용성이 좋아지고, STL container말고 일반 C array에도 적용될 수 있다.

vector<int> v;
int a[100];
 
// C++98
sort( v.begin(), v.end() );
sort( &a[0], &a[0] + sizeof(a)/sizeof(a[0]) );
 
// C++11
sort( begin(v), end(v) );
sort( begin(a), end(a) );

imcgames 의 김학규입니다