2012-10-11

C++11

C++에 손을 뗀지 어언 3년, 그동안 C++0x이니 C++11 같은 게 정말 좋아졌다고 하긴 하는데, 한눈에 알아볼 수 있는 한글 사이트가 없기에 한번 정리해봤다. 위키피디아 문서를 기준으로 요약, 번역한 것이니 틀린 점이 있으면 너그럽게 지적해주기 바란다.

아래 샘플 코드들을 Xcode 4.5에서 Apple LLVM 4.1--std=c++11, --stdlib=libc++ 옵션으로 테스트 했음을 밝힌다.

요약

C++ 11 의 언어 레벨에서의 개선 사항은 대충 다음과 같이 요약할 수 있다.

키워드 설명
&& rvalue 참조 연산자
auto 컴파일러가 유추해낼 수 있는 자동 타입. any 와는 다르다.
char16_t, char_32_t UTF-16 과 UTF-32 타입과 리터럴 선언
constexpr 이름 그대로 상수 + 식
decltype 변수 이름으로 타입을 알아내어서 선언 가능
noexcept 어떤 함수가 예외를 던지는지의 여부를 지정
nullptr NULL 포인터 상수
static_assert assert 의 정적 버전
thread_local 쓰레드 로컬 저장소를 사용하는 변수 지정자
using type aliasing
override 가상 함수의 상속 여부 지정
alignas, alignof 구조체나 클래스의 메모리 정렬 단위를 지정
final 가상 함수나 클래스의 상속 불가능 여부 지정

표준 라이브러리 역시 많이 개선되었다. 그 중에서 핵심적인 부분들을 살펴보면,

클래스/함수 설명
std::initializer_list 초기화 리스트
std::thread, future, promise, async 쓰레드 관련 클래스들
std::function 함수 객체
std::regex 정규식
std::unordered_map O(1) 짜리 해시맵
std::tuple 튜플
std::forward_list 단방향 리스트
std::array 배열
std::atomic lockless 연산을 위한 클래스

rvalue 참조 연산자 &&

class A {
    char* buf;
    int size;
    // move constructor
    A(A&& a): buf(a.buf), size(a.size) { a.buf=0; a.size=0; }
};

주로 임시 객체가 만들어질 때 불필요한 메모리 할당이나 버퍼 복사로 인한 성능 저하를 막기 위해, 파라미터의 내부값 전체를 이동시켜 버리는 용도다. 많은 STL 컨테이너들이 성능을 위해서 이런 이동 생성자(move constructor)와 이동 대입연산자(move assignment operator)을 구현했다는데, 평범한 개발자가 쓸 일은 드물 듯.

더 많은 내용은 MSDN 참조.

constexpr

constexpr int get_five() {return 5;}
int some_value[get_five() + 7];

constexpr을 붙이면 일반 변수나 함수도 컴파일 타임 상수로 쓸 수 있다. 단 함수일 경우 내부에 리턴문만 있어야 한다.

템플릿의 Extern 선언

extern template class std::vector<MyClass>;

이제 특수화된 템플릿 클래스의 extern 선언이 가능해졌다. 단 한 군데에서만 컴파일하게 되므로 컴파일 시간도 빨라졌다.

리스트 초기화

class SequenceClass {
public:
    SequenceClass(std::initializer_list<int> list);
};
SequenceClass some_var = {1, 4, 5, 6};

void function_name(std::initializer_list<float> list);
function_name({1.0f, -3.45f, -0.4f});

std::vector<std::string> v = { "xyzzy", "plugh", "abracadabra" };

struct AltStruct {
    AltStruct(int x, double y) : x_{x}, y_{y} {}

private:
    int x_;
    double y_;
};

AltStruct var2{2, 4.3};

새롭게 추가된 initializer_list를 이용하면, 리스트로 객체를 초기화하거나 파라미터로도 넘길 수 있고, 생성자를 대체할 수도 있다. 맨 마지막 문법은 좀 손가락이 꼬이는 느낌.

auto 와 decltype

auto some_type = boost::bind(&some_function, _2, _1, some_object);
decltype(some_type) other_type = some_type;

템플릿 메타 프로그래밍에서는 리턴값을 알기 힘든 경우가 많은데, 이때 auto로 변수를 선언하면 컴파일러가 자동으로 타입을 찾아준다. decltype은 변수만으로도 타입을 추론해낼 수 있다.

덕분에, iterator를 사용하는 for 루프는 정말 간단해질 수 있다.

for (std::vector<int>::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

auto 를 리턴하는 함수의 경우, -> 를 이용해서 타입을 컴파일러에게 친절하게 알려줄 수 있다.

struct SomeStruct  {
    auto func_name(int x, int y) -> int;
};

auto SomeStruct::func_name(int x, int y) -> int {
    return x + y;
}

범위 기반 for 루프

int my_array[5] = {1, 2, 3, 4, 5};
for (int &x : my_array) {
    x *= 2;
}

배열, 초기화 리스트, begin/end가 정의된 컨테이너 등 컴파일러가 범위를 알 수 있을 듯한 컬렉션들에 한해서만 가능하다. in 연산자를 지원해주면 참 좋겠지만...

람다식과 람다 함수

[](int x, int y) { return x + y; }

함수명에 []을 쓰면, 람다 함수로 사용할 수 있다. 리턴값은 자동적으로 추론된다고 한다.

생성자 관련 개선

class SomeType  {
    int number;
    int value = 5;
public:
    SomeType(int new_number) : number(new_number) {}
    SomeType() : SomeType(42) {}
};

생성자에서 다른 생성자를 호출할 수 있게 되었고, 데이터 멤버도 클래스 선언시에 초기값을 가질 수 있다.

override & final

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed because it doesn't override a base class method
};

struct Base1 final { };
struct Base2 {
    virtual void f() final;
};

c# 이나 자바 같은 언어에서 오래전부터 지원하던 override 와 final 지정자가 드디어 표준이 되었다. final 은 클래스에 대해서도 사용 가능하다.

널 포인터 상수 nullptr

char *pc = nullptr;     // OK
int  *pi = nullptr;     // OK
bool   b = nullptr;     // OK. b is false.
int    i = nullptr;     // error

foo(nullptr);           // calls foo(char *), not foo(int);

NULL 이 0 과 혼용되면서 생긴 문제 때문에, 포인터에 대한 전용 상수인 nullptr이 추가되었다.

열거자의 타입 지정

enum class Enum2 : unsigned int {Val1, Val2};

예전 열거자는 컴파일러 구현에 따라서 int 도 되고, unsigned int 도 되었다. 새 열거자 선언 방식에서는 기본적으로 int이고, 별도로 타입을 지정할 수도 있지만, 정수로 암묵적으로 변환되지는 않는다.

using

template <typename First, typename Second, int Third>
class SomeType;

template <typename Second>
using TypedefName = SomeType<OtherType, Second, 5>;

typedef void (*Type)(double);       // Old style
using OtherType = void (*)(double);     // New introduced syntax

매크로나 typdef 대신 using 을 이용해서 긴 템플릿 이름을 짧게 줄일 수 있다.

가변 갯수 템플릿 인자

template<typename... Arguments>
void SampleFunction(Arguments... parameters);

SampleFunction<int, int>(16, 24);

세상에나, 템플릿 인자도 가변적으로 받을 수 있구나. 여기 참조.

union 개선

#include <new> // Required for placement 'new'.
//
struct Point {
    Point() {}
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};
//
union U {
    int z;
    double w;
    Point p; // Illegal in C++03; legal in C++11.
    //
    // Due to the Point member, a constructor definition is now required.
    //
    U() {new(&p) Point();}
};

union 에 non-trivial 생성자를 가진 클래스를 담을 수 없던 제약이 풀렸다. 근데 이런 거 쓰는 사람이 있나.

유니코드 문자열 선언

const char* a = u8"I'm a UTF-8 string.";
const char16_t* b = u"This is a UTF-16 string.";
const char32_t* c = U"This is a UTF-32 string.";

이제, utf-8 문자열을 하드코딩할 수 있다.

const char* d = R"(The String Data \ Stuff " )";
const char* e = u8R"XXX(I'm a "raw UTF-8" string.)XXX";
const char16_t* f = uR"*(This is a "raw UTF-16" string.)*";
const char32_t* g = UR"(This is a "raw UTF-32" string.)";

정규식을 위한 raw 문자열도 추가되었다. 단, R 만 붙이는게 아니라 따옴표와 괄호까지가 필수적이라서, 첫번째 괄호와 마지막 괄호 내부의 문자열 escape 되지 않게 된다.

멤버 함수 삭제

struct NonCopyable {
    NonCopyable & operator=(const NonCopyable&) = delete;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable() = default;
};

컴파일러가 자동으로 만들어주던 기본 생성자와 복사 생성자, 기본 소멸자 및 대입 연산자에 대해서 명시적으로 default 또는 deleted 를 지정할 수 있게 되었다. 물론 일반 멤버 함수에 대해서도 가능하다.

컴파일 시간 assertions

static_assert((GREEKPI > 3.14) && (GREEKPI < 3.15), "GREEKPI is inaccurate!");

template<class T>
struct Check  {
    static_assert(sizeof(int) <= sizeof(T), "T is not big enough!");
};

된다!

sizeof 개선

struct SomeType { OtherType member; };

sizeof(SomeType::member); //Does not work with C++03. Okay with C++11

데이터 멤버에 대한 sizeof 가 가능해졌다.

동적 메모리 관리(Garbage Collection)

컴파일러에 따라 동적 메모리를 자동으로 관리할 수 있게 되었다. 물론 수동으로 관리하는 방법도 제공한다.

쓰레드 관련

std::thread 클래스가 지원되고 다음과 같은 쓰레드간 동기화 프리미티브들이 포함되었다.

  • std::mutex
  • std::recursive_mutex
  • std::condition_variable
  • std::condition_variable_any
  • std::lock_guard
  • std::unique_lock
std::atomic<bool> ready(false);
std::atomic<int> data(0);

// Thread 0:
data.store(1, std::memory_order_release);
ready.store(true, std::memory_order_release);

// Thread 1:
if (ready.load(std::memory_order_acquire))
    assert(data.load(std::memory_order_acquire) == 1);

보다 가벼운 원자적 연산(atomic operation)도 드디어 표준으로 채택되었다. 윈도우의 InterlockedIncrement() 류라고 보면 된다. 메모리 정렬은 좀 헷갈리니 여기를 참조.

std::packaged_task<int()> task([](){ return 7; }); // wrap the function
std::future<int> f1 = task.get_future();  // get a future
std::thread(std::move(task)).detach(); // launch on a thread
f1.wait();
std::cout << f1.get() << std::endl;

std::packaged_task를 이용하면, 비동기로 함수를 실행하고, 그 결과가 끝날 때까지 대기할 수 있다.

std::future<int> f2 = std::async(std::launch::async, [](){ return 8; });
f2.wait();
std::cout << f2.get() << std::endl;

std::async 역시 future 를 리턴하지만, 자동으로 쓰레드를 만들고 함수를 실행해준다.

튜플

typedef std::tuple <int, double, long &, const char *> test_tuple;
long lengthy = 12;
test_tuple proof (18, 6.5, lengthy, "Ciao!");

lengthy = std::get<0>(proof);  // Assign to 'lengthy' the value 18.
std::get<3>(proof) = " Beautiful!";  // Modify the tuple’s fourth element.

가변 인자 템플릿 덕분에 튜플의 인자 갯수 제한이 풀렸다.

unordered 해시 테이블

기존의 map 과 set 은 정렬된 트리를 사용하므로 검색에 O(log N)이 걸리지만, unordered 는 메모리를 좀 더 쓰는 대신 O(1)로 요소를 찾을 수 있다.

Type of hash table Associated values Equivalent keys
std::unordered_set no no
std::unordered_multiset no yes
std::unordered_map yes no
std::unordered_multimap yes yes

정규식

std::regex rgx(R"([ ,.\t\n;:])");
std::cmatch match;
if( std::regex_search( "Unseen University - Ankh-Morpork", match, rgx ) ) {
    for( size_t a = 0; a < match.size(); a++ ) {
        std::string str( match[a].first, match[a].second );
        std::cout << str << "\n";
    }
}

C++ 에서 정규식이 된다니 감개가 무량하다...

스마트 포인터

std::auto_ptr은 사라지고, std::unique_ptr, std::shared_ptr 그리고 and std::weak_ptr이 표준에 합류했다.

ref

void func (int &r)  { r++; }
template<class F, class P> void g (F f, P t)  { f(t); }

int i = 0 ;
g (func, i) ;
std::cout << i << std::endl;  // Output -> 0

g (func, std::ref(i));
std::cout << i << std::endl;  // Output -> 1

임의의 객체의 레퍼런스를 얻어내는 refcref가 추가되었다.

std::function

struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};

void print_num(int i)
{
    std::cout << i << '\n';
}

std::function<void(int)> f1 = print_num;
std::function<void()> f2 = []() { print_num(42); };
std::function<void()> f3 = std::bind(print_num, 31337);
std::function<void(const Foo&, int)> f4 = &Foo::print_add;
Foo foo(314159);
f4(foo, 1);

함수 객체는 일반적인 함수 포인터 뿐만 아니라, 람다 함수, 멤버 함수, bind식까지 담을 수 있다.

기타

  • 템플릿 선언할 때 일부러 > > 띄워쓰지 않아도 컴파일러가 잘 처리해준다.
  • 변환 연산자(conversion operator)에도 explicit 를 붙일 수 있다.
  • thread_local 지시를 붙이면 쓰레드 로컬 변수를 선언할 수 있다.

comments powered by Disqus