[c++11] 잘 쓰면 매우 편리한 C++11의 문법들

C/C++언어는 저수준까지 정밀하게 다룰 수 있는 반면, 프로그래머가 일일히 관리해줘야하는 부분이 많다는게 C/C++언어에 대한 일반적인 견해입니다. 그래서 저수준까지 일일히 신경쓰지 않아도 되는 쿨한 언어를 가지고 높은 생산성을 추구하는게 추세이기도 합니다.
.
.
.
만, C++의 골수빠로서 C++11을 통해 C++로도 충분히 쿨하고 생산성 높은 코-드를 작성할수 있다고 변호를 해보려고 합니다. Visual Studio 2011, 2012를 사용하는 사람이라면 C++11의 여러 신 문법을 맛볼수 있는데요, 다른 버전 컴파일러와의 호환성이 굳이 필요한 경우가 아니라면 즐거이 사용할 수 있을거에요.

1. RValue
우측값이라고 하죠. 솔직히 말해 코딩에 편리함을 가져다 주지는 않는 녀석이고 이해하기도 쉽지 않지만, STL알고리즘을 비롯하여 임시객체를 생성하고 파괴하는 일이 잦은 코드에서는 큰 성능 향상을 불러올 수 있는 개념입니다.

view plaincopy to clipboardprint?
void Ex1()
{
std::vector<SomeClass> vec;
vec.push_back(SomeClass(1, 2, “3”));
}
이런 코드가 있다고 치면, RValue 개념이 없던 시절에는 먼저 SomeClass 객체를 생성하고, 그 다음에 인자로 넘기기 위해서 복사생성자를 호출하겠죠. 그리고 push_back 함수 안으로 넘어가서 삽입될때 다시 한번 복사가 일어날 겁니다. 그리고 push_back 함수를 빠져나올때 인자가 파괴되고, push_back 함수를 빠져나온 다음에는 임시로 생성된 SomeClass 객체가 파괴됩니다. SomeClass 객체를 vector에 추가하기 위한건데, 쓸데 없이 객체를 두 번이나 복사하고 파괴했습니다. 객체안에 vector나 또 다른 컨테이너가 들어가 있었다고 생각하면 낭비가 막심하겠죠…

그런데 RValue개념이 도입됨으로 인해서 우측값(임시로 생성되는 값이라고 생각하면 쉽습니다)은 복사생성자가 아니라 이동(move)생성자를 호출하게 됩니다. 그래서 Visual Studio 2011, 2012에서는 이 코드는 먼저 인자로 사용하기 위해 SomeClass 객체를 생성하고, 이동생성자를 호출하여 인자로 옮겨주고 그 값을 vector안에 저장합니다. 불필요하게 복사생성된 객체 없으니 파괴할 필요도 없구요.

2. 컨테이너에 emplace 메소드 추가
위의 예시를 계속 이어서 보자면, vec은 어차피 SomeClass의 vector라는건 뻔한 사실입니다. 그런데 여기에 새로운 SomeClass 인스턴스를 추가하려고 하면 push_back(SomeClass(1, 2, “3”)) 처럼 귀찮게 SomeClass를 타이핑해야합니다. 그런데 C++11에서는 이를 간편하게 하는 emplace 메소드들이 추가되었습니다.

view plaincopy to clipboardprint?
void Ex2()
{
std::vector<SomeClass> vec;
vec.push_back(SomeClass(1, 2, “3”));
vec.emplace_back(1, 2, “3”);
}
emplace_back 함수를 보세요. 생성자로 넘길 인자를 그냥 emplace_back에 넘겨주면 됩니다. 이렇게 하면 컴파일러는 vec이 vector<SomeClass> 라는 걸 바탕으로 SomeClass의 생성자 중에서 1, 2, “3”을 인자로 받을수 있는 놈을 찾아서 내부에서 생성시켜줍니다. 가변인자 템플릿이라는 새로운 문법 때문에 가능해진 것이지요.

게다가 이 함수의 좋은 점은 push_back 보다 더 효율적이라는 것입니다. RValue을 이용한 push_back을 호출할 경우에는 생성자 호출 뒤에 이동생성자가 호출되지만, emplace_back을 이용할 경우에는 아예 이 함수 내부에서 인스턴스를 생성하므로 이동생성자가 아예 호출되지 않습니다.

3. auto
C++의 STL은 참 강력하지만… iterator를 쓰기위해 변수를 선언하는건 그동안 참으로 고된 일이었습니다. iterator의 타입 때문이지요.

view plaincopy to clipboardprint?
void Ex3()
{
std::vector<SomeClass> vec;
for(std::vector<SomeClass>::iterator it = vec.begin(); it != end(); ++it)
{
//something
}
}
iterator를 이용해 컨테이너를 순회하는 전형적인 예입니다. 근데 std::vector<SomeClass>::iterator 타이핑하는거 너무 귀찮습니다. IDE가 자동완성 기능 지원해준다고 해도 타이핑할건 타이핑해야되지요. 대안으로 typedef를 이용해 짧게 이름붙여서 쓰는 방법도 있지만, 이것도 컨테이너 타입별로 다 따로 만들어줘야하는 거라서 귀찮기는 마찬가지지요. 그래서 너무 짜증이 났는데…
C++11에서 auto가 추가되었습니다. 이는 변수 선언이 타입을 자동으로 추론하게 해주는 쌈박한 기능입니다. 어차피 변수 초기화 값을 보면 무슨 타입인지 알수 있기 때문에, 굳이 저런 복잡한 타입을 명시하지 않도록 해주는거죠. 이 코드를 이용하면 아래와 같이 간단하게 코드를 쓸 수 있습니다.
view plaincopy to clipboardprint?
void Ex4()
{
std::vector<SomeClass> vec;
for(auto it = vec.begin(); it != end(); ++it)
{
//something
}
}
쿨하지 않나요? 물론 이 기능은 초기값을 지정하는 변수 선언에만 사용이 가능합니다.

4. ranged for loop
하지만 위의 for문도 귀찮기는 마찬가지지요. 하지만 범위기반 for문이 출동하면 어떻게 될까요

view plaincopy to clipboardprint?
void Ex5()
{
std::vector<SomeClass> vec;
for(auto it : vec)
{
//something
}
}
매우 쿨하네요. 컨테이너 순회는 늘 begin()에서 시작해서 end()까지 ++it로 뻔하게 이루어지니깐, 그걸 아예 패턴화 시켜서 for(auto it : vec)처럼 쓸수 있게 해준겁니다. 단 여기서 it는 값으로 복사된 것으로 it값을 아무리 바꾼다고해도 vec안의 값은 못 바꿉니다. vec를 수정하고 싶으면? 레퍼런스를 사용해서 for(auto& it : vec)으로 쓰면 됩니다. (개인적으로 제일 마음에 드는 문법입니다. 너무 편해요. 컨테이너만 보면 그냥 막… 순회하고 싶어집니다)
* 아마 이녀석은 VS2012에서부터만 사용가능할 거에요.

5. lambda expression
정말 대박인 기능이지요. 이제 익명함수, 클로져를 c#이나 파이썬, 자바스크립트 마냥 쓱쓱 작성하고 쓸수 있습니다. 물론 예전에도 functor 기능은 제공되었습니다. STL의 알고리즘은 비교함수 등에 functor를 쓸수 있게 지원했었지요. 그래서 옛날에는 이런 코드를 썼어요.

view plaincopy to clipboardprint?
void Ex6()
{
class Compare
{
public:
bool operator()(const SomeClass& o1, const SomeClass& o2) const
{
// compare function
}
};
std::vector<SomeClass> vec;
std::sort(vec.begin(), vec.end(), Compare());
}
근데 비교함수를 만들기 위해서는 귀찮게도 새로 클래스를 정의하고 그 안에 연산자로 비교 함수를 정의한 다음, 그 클래스의 인스턴스를 넘기는 방식을 사용해야되었죠. 이게 너무 귀찮아서 정렬이 하기 싫었어요… 일일히 클래스 정의하는게 귀찮은 사람들을 위해서 <functional> 헤더에, bind, bind_1st, less 등등의 템플릿이 있어서 이를 조합해서 functor를 만드는게 가능하긴 했지만 직관적이지 못해서 써먹기가 너무 힘들었죠.

그런데 이제는 lambda expression이 사용가능하니 다음과 같이 쓸수 있습니다.

view plaincopy to clipboardprint?
void Ex6()
{
std::vector<SomeClass> vec;
std::sort(vec.begin(), vec.end(), [](const SomeClass& o1, const SomeClass& o2)
{
// compare function
});
}
아아.. 작고 아름다워요. 람다 표현식에서는 굳이 리턴 타입을 밝히지 않아도 됩니다. 왜냐면 컴파일러가 return 문에 들어가는 값을 보고 알아서 추론해주니까요. 그런데도 굳이 타입을 밝히고 싶다고 하시면
[](const SomeClass& o1, const SomeClass& o2) -> bool {…} 이렇게 작성하시면 됩니다
여기서 -> 요 문법도 C++11에서 새로 추가된 거에요. 리턴 타입을 후방에서 선언할 수 있게 해줍니다. (전 아직은 유용하게 쓴 적이 별로 없어서 아직 애착이 가지는 않네요)
게다가 [] 안에 캡쳐할 변수를 나열함으로써, 람다 함수 내에서 외부 변수를 사용할 수 있습니다! =를 넣으면 모든 변수를 값 복사로 캡쳐하고, &를 넣으면 모든 변수를 레퍼런스로 캡쳐합니다. (단 레퍼런스로 캡쳐한다고 해서 외부의 지역변수가 클로져로 들어오고 그러지는 않아요. 즉, 지역변수가 파괴되면, 캡쳐한 레퍼런스는 시망하게 됩니다. 이를 막기 위해서는 shared_ptr를 쓰면 됩니다.)

6. std::function
가변인자 템플릿이 가능해지면서 이제 임의의 함수 타입을 담는 타입이 등장하게 되었습니다. 이로써 함수적 프로그래밍이 훨씬 쉬워졌죠.

view plaincopy to clipboardprint?
void Ex7()
{
std::function<int(int, int)> op[] =
{
[](int a, int b) {return a+b;},
[](int a, int b) {return a-b;},
[](int a, int b) {return a*b;},
[](int a, int b) {return a/b;},
[someVar](int a, int b) {return a+b+someVar;},
};
for(auto f : op)
{
op(1, 1);
}
}
int 두 개를 받아서 int를 리턴하는 함수들을 모아놓는 op 배열을 만들었습니다. 이렇게 함수를 저장해뒀다가, 필요할때 호출해줄 수 있지요. std::function<int(int, int)>가 쓰기에 너무 길다면 auto 쓰면됩니다. 게다가 캡쳐로 외부 변수를 저장하는 것도 자유로우니 그 기능은 막강하다고 할 수 있습니다. 얘네들은 특히 thread 와 함께 사용하면 더욱 편리해요.
(이 예제는 std::function<int(int, int)>를 auto로 바꾸면 현재 c++11 컴파일러에서 작동하지 않습니다. 왜인지는 모르겠지만, 초기화리스트가 타입추론하는데 방해가 되는듯 합니다.)

7. nullptr
뭐가 편해진거냐? 싶은 기능이긴 하지만, 포인터변수를 NULL이나 0이 아니라 nullptr로 초기화한다는 점에서 타입 검사가 조금 더 강력해졌다고 할수 있지요. 그리고 NULL이나 0을 쓰면 색깔이 검어서 보기 미운데, nullptr는 키워드다 보니 파랗게 보여서 그냥 보기에도 좋습니다. (…)

8. enum class
기존의 enum의 문제점이라고 하면 enum은 별도의 이름공간을 가지지 않는다는 것이라고 할 수 있습니다. 이게 왜 문제가 되냐하면, enum을 여러개 쓰다보면 한 쪽에서 쓰는 속성이름을 다른쪽에서도 쓰는 경우가 왕왕 발생한다는 거죠. 이렇게 되면 컴파일러는 식별자가 이 enum에 속한 것인지 저 enum에 속한것인지 알수가 없습니다. 그래서 이를 방지하기 위해서는 enum의 속성이름들을 겹치지 않게 길고 특별하게 짓거나 별도의 name space를 주는 거죠.
그런데 아예 enum class를 도입함으로써 애초에 enum 속성들이 별도의 이름공간을 가지게 했어요. 그래서 이제 enum 속성 이름들을 짧고 아름답게 지을수 있게 되었습니다.

9. 향상된 메모리 관리
C++ 프로그래머의 고충 중 하나가 메모리 관리하는 것인데요, 이제는 메모리 관리가 훨씬 수월해졌습니다. 예전까지는 boost 라이브러리로 제공되던 레퍼런스 카운팅을 지원하는 shared_ptr이라던지 shared_ptr에 기생해서 사는 weak_ptr, 한 놈만 소유권을 가질 수 있는 unique_ptr 이 표준으로 제공됩니다. <memory> 헤더에 들어가 있죠. 물론 스마트 포인터만 쓴다고 메모리 관리가 다 되는것은 아니지만, 매우 편리해지는 것은 사실이지요.

view plaincopy to clipboardprint?
void Ex8()
{
std::shared_ptr<SomeClass> ptr1 = std::shared_ptr<SomeClass>(new SomeClass(1, 2, “3”)); // 1
auto ptr2 = std::shared_ptr<SomeClass>(new SomeClass(1, 2, “3”)); // 2
auto ptr3 = std::make_shared<SomeClass>(1, 2, “3”);
}
근데 shared_ptr 쓸때 귀찮던 점은 생성하는 코드가 참… 길다는 거였죠. 1번 주석이 일반적으로 shared_ptr 를 생성할 때 쓰는 코드입니다. auto를 이용해서 줄인다고 해도 2번 주석 정도가 최대죠. 하지만 C++11에서 가변인자 템플릿이 지원되면 make_shared가 생겨났습니다. 이제 make_shared 안에 생성자로 넣어주던 인자를 넣음으로써 쉽게 shared_ptr를 생성할 수 있죠. (사랑하는 기능입니다ㅎㅎ)

근데 shared_ptr를 사용할 때 문제점은 그냥 포인터를 사용할때는 다른 포인터 타입으로의 캐스팅이 용이했는데, shared_ptr은 그렇지 않다는 것이죠. 이를 위해서 ((AnotherClass*)(ptr.get())) 와 같이 강제로 포인터를 얻어서 캐스팅을 해줄 수 있지만 전혀 C++답지 못한 코드이죠. 그래서 static_cast, dynamic_cast, const_cast에 해당하는 static_pointer_cast, dynamic_pointer_cast, const_pointer_cast가 추가되었습니다. 이로써 dynamic_pointer_cast<AnotherClass>(ptr)을 호출함으로써 안전하고도 편한 스마트 포인터 캐스팅이 가능해졌습니다. (어째 코드 길이는 좀 늘어난거 같다…?ㅋ ㅠㅠ)

10. 정규표현식 라이브러리
꺄악! 이제 C++표준에 정규표현식이 들어갔습니다. std::regex, std::wregex를 이용해서 쉽게 정규표현식을 생성할 수 있구요, 이를 이용해 match, search, replace를 쉽게 할 수 있습니다. <regex> 헤더에 들어가 있어요.

11. Thread Support Library
기존의 C/C++의 스레드 생성은 다분히 OS 종속적이었습니다. 운영체제가 제공하는 함수를 호출해 스레드를 생성하고 조작하는 방법이었기에 이식하기가 좀 많이 귀찮았고, API가 c기반이다보니, c++코드를 쓰기에는 영 궁합이 맞지 않았습니다. (콜백함수 어쩔거야)
하지만 이제 C++11 표준에 스레드 라이브러리가 들어감으로 인해, OS 독립적이며 C++과도 궁합이 맞는 코드를 쉽게 생성해내게 되었습니다. 스레드 프로그래밍이 쉬워진거죠.
스레드를 생성, 관리하는 std::thread 클래스, 상호배제를 실현하는 std::mutex 클래스 등이 추가되었습니다. <thread>헤더, <mutex>헤더에 들어있습니다.

view plaincopy to clipboardprint?
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;

void save_page(const std::string &url)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string result = “fake content”;

g_pages_mutex.lock();
g_pages[url] = result;
g_pages_mutex.unlock();
}

int main()
{
std::thread t1([](){save_page(“http://foo”)});
std::thread t2(save_page, “http://bar”);
t1.join();
t2.join();

g_pages_mutex.lock();
for (const auto &pair : g_pages) {
std::cout << pair.first << ” => ” << pair.second << ‘n’;
}
g_pages_mutex.unlock();
}
스레드 클래스는 t1처럼 람다함수를 넘겨줘도 좋고, t2처럼 함수와 인자를 넘겨줘도 좋습니다. 스레드 인스턴스는 생성되자마자 별도의 스레드에서 작업을 시작하여 주 스레드에서는 join 함수로 자식 스레드가 종료되길 기다릴 수 있죠. 자원 공유시 발생할 수 있는 문제는 mutex의 lock과 unlock으로 제어가능합니다.

덕분에 이제는 멀티스레드 프로그래밍이 한결 간편해졌습니다.

이정도만 해도 C++11 써보고 싶어지지 않나요? 근데 이건 아직 새발의 피라는게ㄷㄷ 그리고 며칠전에는 C++14에 대한 소식도 들었어요. C++도 시대에 뒤쳐지지 않게 부단히 발전해나가는 모습이 보기 좋아요