본문 바로가기

진리는어디에/C++

[C++11] 주요 변경/추가 사항

람다 표현식(Lambda Expressions)

람다 표현식은 함수를 선언한 곳에서 바로 호출 할 수 있도록 한다. 람다 표현식은 아래와 같은 형태를 가진다.

[capture] (parameters) -> return-type {
    body
}

for_each() 알고리즘을 사용하여 문자열을 순회하며 대문자의 개수를 센다고 가정하자. 대문자인지 판단하기 위한 함수 객체를 넘겨 주는 대신 람다 표현식을 사용하면 보다 편리하게 코드 작성이 가능하다.

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++;
        }
    });
    
    std::cout<< Uppercase << " uppercase letters in: " << s << std::endl;          
    return 0; 
}

캡쳐 구문([&Uppercase])에 있는 '&'기호는 람다 표현식으로 변수의 참조를 넘겨준다는 의미다. 그래서 람다 내부에서 그 참조를 이용하여 값을 변경 할 수 있는 것이다. 만일 '&'기호 없이 캡쳐리스트를 통해 넘긴다면 해당 변수의 값이 복사 되어 넘어 간다.

자동 타입 추론(Automatic Type Deduction and decltype)

auto

auto 키워드를 이용해 명확한 데이터 타입의 선언이 없더라도 컴파일러에서 초기화 시 할당 되는 값으로 부터 데이터 타입을 추론하여 변수의 타입을 결정한다.

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

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

decltype

새로운 오퍼레이터 decltype은 표현식으로 부터 타입을 리턴 한다. 이것을 이용해 표현식을 이용한 typedef를 정의 할 수 있다.

const vector<int> vi;
typedef decltype(vi.begin()) CIT; // vi.begin() 표현식에서 타입을 추론하여 CIT 타입으로 정의

CIT another_const_iterator;

초기화 구문 단일화(Unitform Initialization Syntax)

C++ 최소한 네 개의 초기화 방법을 가지고 있다.

괄호로 감싸진 초기화  :

std::string s("hello"); 
int m=int(); //default initialization 

'=' 연산자를 사용해 초기화 :

std::string s="hello"; 
int x=5; 

POD(Plain Old Data structure) 집합체에서는 아래와 같이 대괄호를 사용해 초기화가 가능하다 :

int arr[4]={0,1,2,3}; 
struct tm today={0};

생성자의 멤버 변수 초기화를 이용 할 수 있다 :

struct S {
	int x;
	S(): x(0) {}
}; 

이런 다양한 방법은 편리함도 제공하지만 사용법의 혼란도 야기한다. 가장 최악은 C++03에서는 new[]로 할당된 POD 배열 멤버와 POD 배열을 초기화 할수 없다는 것이다. C++11에서는 이런 복잡함을 대괄호 구문으로 깔끔하게 만들었다.

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 
};

컨테이너 클래스의 초기화 시 지겹게 반복되는 push_back() 호출들도 간단하게 통합 할 수 있다.

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

비슷하게 C++11은 클래스 멤버 초기화도 지원한다.

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

기본 설정 및 삭제된 함수(Deleted and Defaulted Functions)

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

xxx() = default; 형태를 가진 함수는 컴파일러에게 함수의 기본 구현을 생성하라고 지시한다. 기본 설정된 함수는 두가지 장점을 가진다. 하나는 직접 구현한 함수보다 성능적으로 효과적이라는 것이고, 두번째는 프로그래머의 귀찮음을 덜어 준다는 것이다.

그 반대로 삭제된 함수가 있다. 

int func()=delete;

삭제된 함수는 copy 생성자와 할당 연산자를 막아 객체의 복사를 방지할 때 유용하다.

struct NoCopy
{
	NoCopy & operator =( const NoCopy & ) = delete;
	NoCopy ( const NoCopy & ) = delete;
};

NoCopy a;

NoCopy b(a); //compilation error, copy ctor is deleted

nullptr

기존에 단순히 정수 '0'을 NULL이라고 정의한 매크로에서 nullptr이라는 null 포인터임을 명시적으로 표현하는 타입이 추가 되었다. 정수 0과 명확히 구분되어 사용되므로 모호성 오류를 피할 수 있다.

void f(int); //#1
void f(char *);//#2 

//C++03 
f(0); //which f is called? 

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

nullptr은 일반 객체 포인터 뿐만 아니라 함수 포인터,  멤버 함수 포인터등 모든 포인터 타입에 적용 가능하다. 

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++11에서 생성자는 같은 클래스의 다른 생성자를 호출 할 수 있도록 변경 되었다..

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
	M(): M(0) {cout<<"delegating ctor"<<endl;} //#2 delegating 
}; 

위의 예제에서 생성자 2 M()은 생성자 1 M(int v)를 호출한다.

r-value References

C++03에서 참조 타입은 l-value만 가능했다. C++11에서는 r-value 참조라는 새로운 종류의 참조 타입이 도입되었다(r-value 참조란, 임시 객체라던지 리터럴 상수에 대한 참조를 의미한다)

r-value참조가 추가된 가장 주된 이유는 'move' 문법 때문이다. 전통적인 복사와는 다르게 'move'가 의미하는 바는 대상 객체에 원본 오브젝트의 리소스를 옮기고(move) 원복 객체를 "empty" 상태로 만든다는 것이다. 예를 들어 크고 무거운, 하지만 더 이상 사용될 가능성이 없는 스택 영역의 객체를 복사해야 한다고 가정하자, 이 때 복사 대신 move 연산이 사용 될 수 있다.

move 연산을 통해 얻을수 있는 성능 향상을 알아 보기위해 문자열 스와핑을 해보자. 단순하게 아래와 같이 구현 할 수 있다.

void naiveswap(string &a, string & b) 
{
    string temp = a;
    a=b;
    b=temp;
}

위 swap에서는 새로운 메모리 할당과 원본으로 부터 새로 할당된 메모리로 문자열의 복사 과정이 필요하다. 반면에 move 연산은 메모리 할당이나 복사 과정 없이 두 데이터의 메모리를 변경하는 것이다.

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);
}

※ move 연산이라고 해서 C++에서 뭔가 직접적으로 성능향상을 위해 해주는 것은 없다. 다만 넘어오는 인자가 r-value 라는 것을 알려줄뿐이다. 내부에서 성능향상을 위해 어떻게 메모리를 이동시키느냐는 프로그래머의 몫이다.

만일 move 연산을 지원하는 클래스를 만든다면, move생성자와 move 할당 연산자 또한 다음과 같다 :

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

C++11 표준 라이브러리에서는 'move' 문법을 광범위하게 사용하고 있으며 많은 알고리즘과 컨테이너들이 move 관련 최적화를 지원하고 있다.

r-value 레퍼런스에 대한 보다 자세한 내용은 [C++] r-value 레퍼런스(reference) 완벽 가이드에서 확인할 수 있다.

C++11 표준 라이브러리(C++11 Standard Library)

Threading Library

C++11는 스레드 실행에 관련된 thread와 동기화에 사용되는 promise, future 와 같은 멀티 스레드 환경에의 에 관련된 라이브러리 및 async() 템플릿 함수, thread_local 스토리지 타입이  추가 되었다.

New Smart Pointer Classes

기존 auto_ptr 을 대체하는 shared_ptr, unique_ptr가 추가 되었다. 기존 표준 라이브러리와 당연히 호환 되며 안전하게 컨테이너 클래스에 저장 할 수 있을 뿐 아니라 및 여러 알고리즘의 인자로도 사용 가능하다.

New C++ Algorithms

all_of(), any_of(), none_of() 와 같은 알고리즘이 추가 되었다.  

#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() 처럼 배열 복사를 손쉽게 해주는 알고리즘도 추가 되었다.

#include
int source[5]={0,12,34,50,80};

int target[5];

//copy 5 elements from source to target

copy_n(source,5,target);

#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() 를 이용해 일정하게 증가하는 배열을 만들 수도 있다. 

include <numeric> 

int a[5]={0}; 
iota(a, a+5, 10); //changes a to {10,11,12,13,14}

char c[3]={0};  
iota(c, c+3, 'a'); //{'a','b','c'}

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!