[C++11] std::tuple

tuple은 여러개를 하나의 것으로 묶은 것을 의미합니다. tuple이 C++11에 새롭게 도입되어 사용되는 가장 일반적인 형태는 함수의 반환값으로 사용입니다. 함수는 하나의 값 또는 객체만을 반환하는데 tuple을 사용하면 다수의 값 또는 객체들을 반환시킬 수 있습니다. 이에 대한 예제 코드는 아래와 같습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

tuple getSomething(double a, double b)
{
    return make_tuple(a+b, a/b, "Hello Tuple");
}

int _tmain(int argc, _TCHAR* argv[])
{
    auto r = getSomething(10.0, 20.0);

    cout << "Sum: " << get<0>(r) << endl;
    cout << "Div: " << get<1>(r) << endl;
    cout << "Msg: " << get<2>(r) << endl;

    return 0;
}

위의 코드 중 8번에 getSomething이라는 함수가 있습니다. 이 함수의 반환형이 tuple이고 double, double, string 템플릿 인자를 갖습니다. 즉, 이 tuple은 실수값 2개와 문자열값 1개로 구성되어 있다는 것이고 getSomething의 반환값은 실수값 2개와 문자열값 1개를 반환한다는 의미로 해석할 수 있습니다. 이 함수의 실제 호출은 15번 줄에서 이루어지는데요. 이 함수의 반환값인 tuple의 각 구성 요소에 접근하기 위해 std::get이라는 함수를 사용하며 이 std::get 함수의 사용 예가 바로 17번 ~ 19번 코드입니다.

[C++11] 스마트포인터(Smart Pointer)

C++11에서 새롭게 도입된 스마트 포인터는 unique_ptr, shared_ptr, weak_ptr입니다. unique_ptr은 실제 데이터에 대한 소유권을 오직 하나만 가지도록 하는 것이고 shared_ptr은 실제 데이터에 대한 소유권을 여러 개의 스마트포인터가 공유하면서 참조 카운트 방식으로 관리하여 참조 카운트가 0일때 실제 데이터를 해제하는 것입니다. weak_ptr은 share_ptr처럼 실제 데이터를 공유하지만 참조 카운트의 값을 증가하거나 감소시키지 않는다는 특징이 있습니다. 이 세개의 스마트포인터에 대해 하나씩 살펴 보도록 하겠습니다. 먼저 unique_ptr입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    return 0;
}

위의 코드중 9번은 정수값 10을 관리하는 unique_ptr로써 10번 코드에서처럼 스마트포인트가 관리하는 실제 데이터 값을 화면에 표시하고 있습니다. 중요한 것은 unique_ptr은 실제 데이터에 대한 소유를 오직 하나의 스마트포인트만이 가진다는 것입니다. 이에 대해 확인할 수 있도록 다음 코드를 살펴보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    unique_ptr p2 = p1;

    return 0;
}

새롭게 추가된 코드는 오직 12번으로써 기존의 unique_ptr에 대한 스마트 포인터를 또 다른 unique_ptr에 대입함으로써 하나의 실제 데이터를 2개의 unique_ptr이 소유하려고 하는 것입니다. 이에 대해 컴파일 타임에서 12번 코드는 오류가 납니다. 그러나 경우에 따라서는 실제 데이터에 소유를 다른 unique_ptr로 이동시켜야 할 때가 있습니다. 이에 대해 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    unique_ptr p1(new int(10));
    cout << *p1 << endl;

    unique_ptr p3 = move(p1);
    //cout << *p1 << endl;

    cout << p1.get() << endl;
    cout << p3.get() << endl;

    return 0;
}

새로운 코드는 12번입니다. 표준 함수인 move를 이용해 unique_ptr이 가지고 있는 소유권을 다른 unique_ptr이 갖도록 합니다. 이렇게 되면 p1이 관리하는 실제 데이터의 주소는 null이 되어 소유권이 없다는 의미가 되고 원래 p1이 관리하는 실제 데이터의 주소값을 p3가 갖게 됩니다. 결국 주석 처리된 13번 코드가 실행된다면 오류가 발생할 것입니다. 15번과 16번 코드에서 보이는 unique_ptr의 get 함수는 스마트 포인트가 관리하는 실제 데이터의 주소값을 얻는 함수이므로 15번 코드는 null에 해당하는 0을, 16번 코드는 실제 데이터의 주소값을 표시하게 됩니다.

다음은 shared_ptr에 대해 정리해 보겠습니다. shared_ptr은 unique_ptr과 다르게 관리하는 실제 데이터의 소유를 여러개의 shared_ptr이 공유한다는 것입니다. 이렇게 공유를 하다가 실제 데이터에 대해 소유하는 shared_ptr 객체가 존재하지 않을 경우, 즉 참조 개수(Reference Count)가 0개일 경우 실제 데이터를 메모리에서 해제하게 됩니다. 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr p1(new int(10));
    shared_ptr p2 = p1;

    cout << *p1 << endl;
    cout << *p2 << endl;

    cout << p2.use_count() << endl;
    p1.reset();
    cout << p2.use_count() << endl;
    p2.reset();
    cout << p2.use_count() << endl;

    return 0;
}

위의 코드에서 9번 코드에서 생성한 shared_ptr 객체 p1을 같은 타입인 p2에 대대입(assign)함으로써 하나의 실제 데이터에 대해 2개의 shared_ptr 객체가 소유하고 있습니다. 12번과 13번 코드를 통해 이 2개의 shared_ptr 객체가 관리하는 동일한 실제 데이터의 값을 화면에 표시할 것이고요. shared_ptr 객체가 소유하는 실제 데이터에 대한 참조 개수는 use_count 함수를 통해 얻을 수 있고 15번, 17번, 19번 코드를 통해 확인하고 있습니다. 15번 코드는 참조 개수로 2를 출력할 것입니다. shared_ptr의 reset 함수를 통해 실제 데이터에 대한 소유를 더 이상 하지 않겠다고 명시적으로 지정되었으므로 17번 코드는 이제 1을 출력할 것입니다. 그리고 18번 코드에서 다시 한번 reset 함수를 호출했으므로 19번 코드에서 참조 개수로써 이제 0을 표시할 것입니다. 사실 실제 업무단에서는 reset 함수를 직접 해줄 필요가 없습니다. 객체의 scope를 벗어나면 자동으로 이루어지기 때문입니다.

이제 마지막으로 weak_ptr에 대해 정리해 보겠습니다. weak_ptr은 shared_ptr과 참조 카운트를 이용해 실제 데이터에 대한 소유를 함께 한다는 점은 동일하지만 참조 카운트를 증가시키지도 감소시키지도 않는다는 것입니다. 다음 코드를 살펴 보게습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr sp1(new int(10));
    weak_ptr wp1 = sp1;

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    return 0;
}

shared_ptr인 sp1을 weak_ptr인 wp1에 할당하고 있고 sp1과 wp1에 대한 참조 카운트를 표시하는 코드입니다. weak_ptr은 참조 카운트의 개수를 증가시키지 않는다고 했으므로 1과 1을 표시할 것입니다. weak_ptr은 lock이라는 함수를 제공하는데 이 함수는 shared_ptr 객체를 생성해 반환해 줍니다. 즉, weak_ptr의 lock 함수가 실행도면 shared_ptr 객체가 생성되므로 자동으로 참조 카운트는 1이 증가될 것입니다. 이에 대한 내용을 아래의 코드를 통해 살펴볼 수 있습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    shared_ptr sp1(new int(10));
    weak_ptr wp1 = sp1;

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    {
        shared_ptr sp2 = wp1.lock();

        cout << sp1.use_count() << endl;
        cout << wp1.use_count() << endl;
    }

    cout << sp1.use_count() << endl;
    cout << wp1.use_count() << endl;

    return 0;
}

15번 ~ 20번 코드를 살펴보면 새로운 블럭 범위 안에서 weak_ptr 객체인 wp1의 lock 함수를 통해 shared_ptr 객체를 생성하여 sp2에 할당하고 있습니다. 18번과 19번 코드에서 참조 카운트를 출력해 보면 각각 2의 값이 표시됩니다. 즉, 실제 데이터에 대해 3개의 스마트 포인터가 소유하고 있는데 말입니다. 3개가 소유함에도 참조 카운트가 2인 이유는 2개는 shared_ptr이고 1개는 weak_ptr이기 때문입니다. 20번 코드까지 실행되면 자동으로 sp2 객체가 소멸되면서 실제 데이터에 대한 소유도 해제됩니다. 즉 참조 카운트가 1개 감소합니다. 감소했다는 것은 22번과 23번 코드에서 각각 모두 1이 표시되는 것을 통해 확인할 수 있습니다.

[C++11] std::array

C++11에서는 STL에 array라는 새로운 컨테이너를 추가 하였습니다. vector 컨테이너와 유사하지만 다음과 같은 차이가 있습니다.

  • vector는 heap을 사용하지만 array는 stack을 사용함
  • vector는 크기가 유동적이지만 array는 크기가 고정임
  • vector보다 array가 item 접근 속도가 빠름

이 array 컨테이너 클래스에 대한 가장 기본적은 사용은 다음과 같습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    array arr = {1, 2, 3};

    for(auto i = arr.begin(); i != arr.end(); ++i) {
        cout << *i << endl;
    }
	
    return 0;
}

9번 코드에서 보듯이 array는 타입과 배열의 크기를 갖습니다. 예제에서는 크기를 10으로 지정했는데 실제값은 3개만을 지정하고 있습니다. 이렇게 될 경우 나머지 7개에 대해서는 0의 값으로 채워집니다. 11번 코드는 일반적인 STL의 컨테이너 요소를 순회하며 접근하는 코드를 볼 수 있습니다. array는 vector와 마찬가지로 []를 통해 요소에 접근할 수 있으며 다음 예과 같습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    array arr = {1, 2, 3};
	
    cout << "size: " << arr.size() << endl;
    cout << arr[0] << " " << arr[1] << " " << arr[2] << " ..." << endl;

    return 0;
}

C++11에서의 array는 원래의 배열인 C-Array 타입과 전혀 호환되지 않습니다. C++11의 array를 C-Array로의 타입 변환을 위해서는 다음과 같은 예를 통해 가능합니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    array arr = {1, 2, 3};

    int *pArr = arr.data();
    pArr[1] = 100;
    cout << arr[0] << " " << arr[1] << " " << arr[2] << " ... " << endl;

    return 0;
}

C-Array 타입으로 변환되었을지라도 12번 코드처럼 값을 변경하면 C++11에서의 array 객체에도 적용되는 것을 알 수 있습니다. 이것으로 볼때 C++11의 array는 내부적으로 C-Array를 그대로 사용하는 것을 알 수 있습니다.

[C++11] 유니폼 초기화(Uniform Initializer)

유니폼 초기화는 객체의 초기화를 보다 간단한 코드만으로 구현할 수 있는 기능입니다. 예를 들어 다음과 같은 Product라는 클래스가 있다고 할때 이 Product 클래스의 객체를 초기화하여 생성하는 경우를 예를 들어 보겠습니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

class Product
{
private:
    string name;
    int cost;
public:
    Product(const string& name, int cost) : name(name), cost(cost) {}
};

int _tmain(int argc, _TCHAR* argv[])
{
    Product r("Apple", 100);
	
    return 0;
}

이미 흔히 알고 있는 방식으로 18번 코드와 같이 객체를 초기화하면서 생성하고 있습니다. 이런 초기화 방식에 대해 C++11은 다음과 같은 방식도 지원합니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

class Product
{
private:
    string name;
    int cost;
public:
    Product(const string& name, int cost) : name(name), cost(cost) {}
};

int _tmain(int argc, _TCHAR* argv[])
{
    Product r {"Apple", 100};
	
    return 0;
}

변경된 부분은 15번 코드입니다. 바로 () 대신에 {}를 사용해 초기화를 하고 있습니다. 여기까지는 별 것 없는 것 같습니다만 이러한 초기화 방식이 유니폼 초기화(Uniform Initializer)입니다. 이 별것 없을 것 같은 초기화 방식이 배열이나 콜렉션(Collection, Container)에서는 다음처럼 응용되어 사용할 수 있습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

class Product
{
private:
    string name;
    int cost;
public:
    Product(const string& name, int cost) : name(name), cost(cost) {}
};

int _tmain(int argc, _TCHAR* argv[])
{
    vector v {
        {"Apple", 100},
        {"Orange", 200},
        {"Grape", 300}
    };

    return 0;
}

유니폼 초기화를 이용하여 vector 컨테이너에 3개의 객체를 생성하고 있습니다. 이러한 방식을 이용하지 않고 3개의 객체를 초기화하여 컨테이너에 추가하는 기존의 코드를 생각해 본다면 이러한 유니폼 초기화 방식이 얼마나 직관적인지 쉽게 짐작해 볼 수 있습니다.