Software Workshop

원문: The Biggest Changes in C++11 (and Why You Should Care)The Biggest Changes in C++11 (and Why You Should Care) by Danny Kalev | June 21, 2011

직역보다는 의역과 평역 위주로 정리해본다.

C++의 창시자 Bjarne Stroustrup은 최근 ”C++11이 새로운 언어처럼 느껴진다 - 더 잘 들어맞는 조각들”이라고 말했다. 사실 핵심 C++11은 크게 변화했다. 람다 표현식, 객체의 자동 타입 추론, 균일한 초기화 문법, 생성자 파견, deleted and defaulted function 선언, nullptr, rvalue reference.

C++11 표준 라이브러리는 또한 새로운 알고리즘과 컨테이너 클래스, 원자 연산, 타입 특성(type traits), 정규 표현식, 새로운 스마트 포인터, async() 함수, 멀티쓰레딩 라이브러리를 가지고 개편되었다.

C++11의 새로운 핵심 기능과 라이브러리 기능의 완전한 목록을 참고할 것.

1998년의 C++ 표준 승인 이후, 두명의 위원회 멤버들이 다음 C++ 표준은 '확실히' 내장된 가비지 컬렉터를 포함할 것이고, 아마도 이식가능한 쓰레딩 모델을 정의하는 데 관련된 기술적인 복잡성 때문에 멀티쓰레딩은 지원하지 않을 것이라고 예언했다. 13년 후, 새로운 C++ 표준인 C++11은 거의 완전하다. 뭐가 부족한지 맞춰봐라. 가비지 컬렉터가 부족하다. 하지만 최첨단의 쓰레딩 라이브러리를 포함하고 있다.

Lambda Expressions


int main()
{
    char s[] = "Hello World!";
    int Uppercase = 0; //modified by the lambda
    for_each(s, s+sizeof(s), [&Uppercase] (char c) {
        if (isupper(c))
            Uppercase++;
    });
    cout< < Uppercase< < " uppercase letters in: "< < s< < endl;
}

[&Uppercase] (char c) { if (isupper(c)) Uppercase++; });

바로 이 부분이 lambda expression이다. 여기 예제는 function을 lambda expression의 형태로 inline으로 적었지만, C++11 이전의 문법으로는 function object(functor)로 적을 수도 있다. 기존의 방식이라면 로직 외부에 function이나 functor를 구현해야 하는 번거로움이 있었으나 lambda expression이 도입되면서 inline으로 함수를 정의해서 사용할 수 있으므로 훨씬 편리하게 되었다.

Automatic Type Deduction and decltype

C++0x나 C++11에서는 auto라는 예약어의 의미가 기존과 달라졌다. 예전에는 block이나 function 내에서 local 변수로 지정할 때 사용했지만 이제부터는 rvalue, 즉 데이터의 값을 보고 변수의 타입을 지정하는 지시어로 사용된다.

auto x = 0; //x has type int because 0 is int
auto c = 'a'; //char
auto d = 0.5; //double
auto national_debt = 14400000000000LL;//long long 

위 예제를 보면 세 변수에 대입되는 데이터의 타입이 각각 int, char, double, long long이라는 것을 명확하게 알 수 있으므로 굳이 타입을 지정하는 대신에 auto를 사용하였음을 알 수 있다.

void func(const vector< int> &vi)
{
    vector< int>::const_iterator ci = vi.begin();
}

가장 유용한 용례는 iterator를 사용할 때가 아닐까 싶다. 일반적으로 STL에서 iterator를 사용할 때 container 변수의 타입(vector)을 지정하고 다시 iterator를 비슷한 방식(vector::const_iterator)으로 지정해줘야 하니 개발자 입장에서 상당히 번거로웠는데, C++11에서는 아래 예제처럼 auto라고 적어주면 간단히 iterator 변수 타입을 지정할 수 있다.

auto ci = vi.begin();

decltype이라는 연산자를 가지고 데이터로부터 타입을 알아낼 수 있다. 아래 예제에서처럼 decltype(vi.begi()) 이라고 지정하면 vector라는 타입임을 알 수 있는 것이다.

const vector< int> vi;
typedef decltype (vi.begin()) CIT;
CIT another_const_iterator;

Uniform Initialization Syntax

C++에는 다음의 4가지 초기화 방식을 사용할 수 있다.

d::string s("hello");
int m=int(); //default initialization
std::string s="hello";
int x=5;
int arr[4]={0,1,2,3};
struct tm today={0};
struct S {
 int x;
 S(): x(0) {} 
};

이렇게 다양한 초기화 방식 때문에 혼동하기 쉬우므로 C++11에서는 “균일한 중괄호 표기법(Uniform Brace Notation)“으로 통일한다.

class C
{
    int a;
    int b;
  public:
    C(int i, int j);
};
 
C c {0,0}; //C++11 only. Equivalent to: C c(0,0);
 
int* a = new int[3] { 1, 2, 0 }; /C++11 only
 
class X {
    int a[4];
  public:
    X() : a{1,2,3,4} {} //C++11, member array initializer
};

// C++11 container initializer
vector vs< string>={ "first", "second", "third"};
map singers = { {"Lady Gaga", "+1 (212) 555-7890"}, {"Beyonce Knowles", "+1 (212) 555-0987"}};

container에 대해서도 push_back()을 나열하던 기존 방식을 버리고 “균일한 중괄호 표기법”을 이용하면 된다.

class C
{
    int a=7; //C++11 only
  public:
    C();
};

클래스 멤버 변수에 대한 클래스 내부 초기화도 동일한 방식으로 가능하다.

Deleted and Defaulted Functions

기본 생성자(default constructor)를 작성하는 일은 번거로운 일이다. 특히, 복사 생성자(copy constructor)를 가지는 클래스의 기본 생성자의 경우에는 컴파일러가 대신 만들어 주지도 못하기 때문에 개발자의 손길이 꼭 필요하다. 그래서 이런 번거로움을 없애기 위해 C++11에서 “Defaulted Function”이라는 개념이 생겼다. Defaulted function은 컴파일러가 기본으로 생성하는 함수이며, 개발자가 생성자와 소멸자의 구현에 더 이상 신경쓰지 않아도 된다는 장점을 가져다준다. 그러나 defaulted function은 선언만 할 수 있을 뿐, 정의할 수는 없다.


struct A
{
    A() = default; //C++11
    virtual ~A() = default; //C++11
};
int func()=delete; 

위 구조체에서 constructor와 destructor는 defaulted function으로 선언되었다.


struct NoCopy
{
    NoCopy & operator = ( const NoCopy & ) = delete;
    NoCopy ( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); //compilation error, copy ctor is deleted 

deleted function은 defaulted function과 반대의 의미를 가진다. 객체가 복사되는 것을 방지하기 위해 대입( = ) 연산자와 복사 생성자를 지우는 방법을 택한다. 이렇게 연산자와 복사 생성자가 지워지면 컴파일 단계에서 에러가 발생한다.

nullptr

(int) 0과 (void *) 0을 구분하기 위해 nullptr이라는 예약어가 도입되었다. 아래 예제처럼 f(0)을 호출했을 때 0이라는 값의 모호성 때문에 충돌이 발생할 수 있는데, nullptr을 사용하면 C의 NULL처럼 null pointer로서 간주된다.


void f(int); //#1
void f(char *);//#2
  
//C++03
f(0); //which f is called?

//C++11
f(nullptr) //unambiguous, calls #2 

모든 포인터에 사용 가능하며, 함수 포인터나 멤버 변수를 가리키는 포인터로도 가능하다.


const char *pc=str.c_str(); //data pointers
if (pc!=nullptr)
    cout< < pc< < endl;
int (A::*pmf)()=nullptr; //pointer to member function
void (*pmf)()=nullptr; //pointer to function 

Delegating Constructors

C++에서는 생성자를 다른 생성자로 파견(delegate)할 수 없다.(Java에서는 가능하다.) 그러므로 비슷한 생성자를 parameter 형상에 따라 여러 벌 만들어야 하는 부담이 존재한다. 특히 default parameter를 지원하려면 같은 기능의 코드를 생성자마다 복사해 넣어야 하는 부작용도 발생하게 된다.


class M //C++11 delegating constructors
{
    int x, y;
    char *p;
 public:
    M(int v) : x(v), y(0), p(new char [MAX]) {} //#1 target constructor
    M(): M(0) {cout< < "delegating ctor "< < endl; } // # 2 constructor
}

위 예제에서는 M()를 호출하면 M(0)이 호출된다. x와 y를 각각 0으로 초기화하고 p에 메모리를 할당하고 비어있는 채로 초기화한다. 실행 순서는 M(0)가 먼저 실행되고 그 후에 M()의 body block이 실행된다.

Rvalue References

C++03까지 lvalue에만 제한되어 있던 reference를 rvalue까지 확대하였다. rvalue reference 도입의 주된 이유는 이동 시맨틱(move semantics)를 지원하기 위해서이다. 전통적인 복사와 달리 이동은 대상 객체가 원본 객체로부터 리소스를 조금씩 빼돌리는 것을 의미한다. 객체를 복사하면 비싸기도 할 뿐더러 불필요한 경우에, 이동 연산이 대신 필요하다.

다음 예제는 두 개의 변수의 값을 바꾸는 것인데 변수를 가리키는 포인터를 바꾸는 방법을 이용하면 좀 더 적은 비용으로 swap이 가능하다.


void naiveswap(string &a, string & b)
{
    string temp = a;
    a = b;
    b = temp;
}
void moveswapstr(string& empty, string & filled)
{
    //pseudo code, but you get the idea
    size_t sz=empty.size();
    const char *p= empty.data();
    //move filled's resources to empty
    empty.setsize(filled.size());
    empty.setdata(filled.data());
    //filled becomes empty
    filled.setsize(sz);
    filled.setdata(p);
}

다음 예제는 복사 생성자 대신 이동 생성자를 선언하고 있다. &&에 주목하자.

class Movable
{
    Movable (Movable&&); //move constructor
    Movable&& operator=(Movable&&); //move assignment operator
};

C++11 표준 라이브러리는 이동 시맨틱을 확장해서 사용하고 있다. 많은 알고리즘들과 container들이 이동 연산에 최적화되어 있다.

C++11 Standard Library

C++은 2003년에 Library Technical Report 1 (TR1)이라는 형태로 큰 외형 변화(facelift)를 겪었다. TR1은 새로운 container 클래스와 여러 새로운 라이브러리들을 포함시켰다. 컨테이너로 unordered_set, unordered_map, unordered_multiset, unordered_multimap, 그리고 라이브러리로 정규 표현식, 튜플(tuple), function object wrapper 등이 있다. C++11의 승인으로 TR1은 공식적으로 C++ 표준에 통합되었고 새로운 라이브러리들은 TR1부터 추가되었다.

다음 몇 가지들은 C++11의 표준 라이브러리 특징이다.

Threading Library

개발자 관점에서 C++11에 추가된 가장 큰 특징은 동시성(concurrency)이다. C++11은 execution thread, future, promise로 대표되는 thread class와 async() 함수 템플릿, 그리고 thread_local 저장 타입을 가지고 있다. thread class느s 동시 환경에서 동기화(synchronization)를 위해 사용되는 객체이다. async() 함수 템플릿은 동시 작업(concurrent task)을 시작하기 위해 필요하며, thread_local 스토리지 타입은 쓰레드에 유일한 데이터를 선언하기 위해 사용된다. C++11 쓰레딩 라이브러리를 빠르게 살펴보려면 Anthony Williams의 Simpler Multithreading in C++0x를 읽으면 된다.

New Smart Pointer Classes

C++98은 단 한 가지의 스마트 포인터 클래스 auto_ptr을 정의했다. 이건 사용하지 않는 게 좋다. C++11은 새로운 스마트 포인터 클래스로 shared_ptr((번역) shared_ptr")과 unique_ptr((번역) unique_ptr)을 추가했다. 두 가지 모두 표준 라이브러리 컴포넌트와 호환되므로 안전하게 표준 컨테이너에 저장할 수 있고 표준 알고리즘으로 조작할 수 있다.

New Algorithms

C++11 표준 라이브러리는 새로운 알고리즘으로 all_of(), any_of(), none_of()를 정의한다.

아래 예제에서 범위의 속성들을 검사하기 위해 all_of()와 any_of(), none_of()를 사용하여 first, first+n)범위(first부터 first+n전까지) ispositive()를 테스트해본다. (예를 들어, first를 -3으로 n을 5로 가정하면, -3, -2, -1, 0, 1의 값에 대한 테스트로 생각할 수 있다.)

#include < algorithm>
//C++11 code
//are all of the elements positive?
all_of(first, first+n, ispositive()); //false
//is there at least one positive element?
any_of(first, first+n, ispositive());//true
// are none of the elements positive?
none_of(first, first+n, ispositive()); //false 

copy_n() 알고리즘을 이용해서 5개의 요소로 구성된 배열을 다른 배열에 복사한다.

#include < algorithm>
int source[5]={0,12,34,50,80};
int target[5];
//copy 5 elements from source to target
copy_n(source,5,target);

알고리즘 iota()는 연속적으로 증가하는 값으로 구성된 범위를 만들어낸다. 다음 예제는 배열 a에 10부터 14까지 값을 증가시켜가면서 대입하는 것을 보여준다.

#include < numeric>
int a[5]={0};
char c[3]={0};
iota(a, a+5, 10); //changes a to {10,11,12,13,14}
iota(c, c+3, 'a'); //{'a','b','c'}  

맺음말

C++11은 아직까지 XML API, socket, GUI, reflection, garbage collector등의 몇 가지 유용한 라이브러리가 부족하다. 그러나 C++을 보다 안전하고 효율적이며 배우고 사용하기 쉽게 만드는 새로운 특징들을 많이 제공하고 있다.

C++11의 변화가 압도적인 것 같다면 놀라지 마시라. 이런 변화를 점진적으로 소화시키기 위해 시간이 필요하다. 이런 과정의 끝에서 아마도 Stroustrup의 말에 동의하게 될 것이다. 'C++11은 새로운 언어, 훨씬 더 나은 언어처럼 느껴진다.'