[C++11] 람다(Lambda) 표현식

C++11은 람다 표현식 기능을 제공합니다. 람다 표현식은 함수를 미리 정의하지 않고 필요한 시점에서 사용하고 바로 버리는 것으로 생각할 수 있습니다. 람다가 제공되기 이전에 어떤 함수를 사용하고자 했다면, 그 함수를 미리 정의하고 사용하게 됩니다. 사용하고 난 뒤에 그 함수는 계속 존재합니다.아래는 람다 표현식을 이용해 만들어진 함수 예입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    auto func = []() {
        cout << "Hello World" << endl;
    };

    func();

    return 0;
}

8번 코드에서 람다 표현식을 이용해 함수를 정의하고 있습니다. 정의된 함수는 예제처럼 어떤 변수에 함수를 할당해 놓고 12번 코드에서 실행할 수 도 있지만 어떤 변수에 할당하지 않고 바로 실행할 수 도 있습니다. 즉, 아래처럼 말입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    []() {
        cout << "Hello World" << endl;
    }();

    return 0;
}

첫번째 예제와는 다르게 이 두번째 예제는 필요한 시점에 함수를 정의하고 바로 실행하고 함수를 폐기하고 있습니다. 8번 코드에서 보는 것처럼 람다 표현식은 2가지로 구성됩니다. []과 {}이며 []는 람다 소개자(Lambda Introducer) 또는 Capture Clause라고 하며 {}는 람다 몸체(Lambda Body)입니다. 람다 몸체에 함수 구현 코드가 존재하며 람다 소개자에 외부 변수를 어떤 식으로 참조할지에 대한 Capture 지정자과 함수에 입력될 인자(Parameter) 리스트가 지정됩니다. 예를 들어 람다 표현식으로 정의한 함수의 인자로 int를 받아 출력하는 함수는 다음과 같습니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    [](int a) {
        cout << a << endl;
    }();

    return 0;
}

이제 람다 함수(람다 표현식으로 정의된 함수)에 대해 몇가지 예를 통해 좀더 정리해 보겠습니다. 다음 코드를 살펴 보겠습니다.

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

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    for_each(v.begin(), v.end(), [](int v) {
        cout << v << endl;
    });

    return 0;
}

위의 코드 중 12번의 for_each 함수는 컨테이너의 각 요소를 인자로 어떤 함수(또는 함수객체)를 호출하는 것으로 for_each의 세번째 인자가 바로 호출되는 함수(또는 함수객체)가 됩니다. 일반적으로 호출할 함수를 미리 정의해 두어야 하나 여기서는 람다 함수를 이용해 미리 함수를 정의하지 않고 필요한 시점에 함수를 정의하고 사용하고 있습니다. 다음 예제를 보겠습니다.

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

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    auto it = find_if(v.begin(), v.end(), [](int v)->bool {
        return (v % 2) == 1;
    });

    cout << *it << endl;

    return 0;
}

위의 코드 중 12번에 있는 find_if 함수는 컨테이너를 구성하는 요소 중 어떤 조건을 만족하는 첫번째 요소의 값을 반환하는 함수인데, 조건을 지정하기 위해 함수 또는 함수 객체를 세번째 인자에 지정됩니다. 여기서는 람다 함수를 통해 조건 함수가 지정되었는데 이 함수의 반환 타입(Type)을 지정하기 위해서 ->bool를 사용함으로써 반환 타입이 bool이라고 명시하고 있습니다. 엄격하게는 이 ->bool 코드를 생략할 수 있는데, 이는 컴파일러가 이 람다함수의 반환타입을 추론할 수 있기 때문입니다. 컴파일러의 추론 대신 직접 반환 타입을 알려주기 위해 ->(type) 연산자를 사용할 수 있다는 것에 대한 예제입니다.

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

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };
    int sum = 0;

    for_each(v.begin(), v.end(), [&](int v) {
       if (v % 2 == 0) sum += v;
    });
    
    cout << sum << endl;

    return 0;
}

위 코드 중 13번의 람다 함수를 보면 []에 해당하는 람다소개자의 형태가 [&]처럼 되어 있습니다. 이 [&]의 의미는 람다 함수 앞단에 모든(All) 외부 변수를 참조 타입(Reference Type)으로 잡아(Capture) 사용하겠다는 의미입니다. 실제 람다 함수 구현부를 보면 외부에 존재하는 sum 변수를 사용하고 있습니다. 참조 형태로 사용하겠다고 했으므로 람다 함수 내부에서 변수값을 변경하면 외부에서도 변경되는 것을 알 수 있습니다. 람다 소개자가 []일 경우에는 어떤한 외부 변수도 사용하지 않겠다는 의미이며 [&]는 모든 외부 변수를 참조로 사용하겠다는 것이고, [=]는 모든 외부 변수를 상수값(const)으로 값 자체를 복사(Copy)하여 사용하겠다는 의미입니다. [&a]는 외부 변수 중 a만을 참조값으로 사용하겠다는 의미이고 [&a, =b]는 a는 참조값으로 b는 상수값으로 사용하겠다는 의미입니다.

[C++11] static_assert

static_assert는 컴파일 타임에서 지정한 조건을 만족하지 않을 경우 개발자에게 에러를 제공하는 기능입니다. 한가지 예로 static_assert의 기능을 정리합니다.

#include "stdafx.h"
#include 

using namespace std;

template int func() {
    static_assert(N <= 100, "Less than 100");
    int sum = 0;
    for(int i=1; i<=N; i++) {
        sum += i;
    }

    return sum;
}

int _tmain(int argc, _TCHAR* argv[])
{
    cout << func<10>() << endl;
    cout << func<1000>() << endl;

    return 0;
}

위의 코드에서 7번에 static_assert가 사용되었습니다. 이 static_assert의 첫번째 인자는 조건이고 두번째는 조건이 false일 경우 개발자에게 제공할 Error 메세지입니다. 실제로 18번 코드는 static_assert의 조건을 만족하므로 문제가 없으나 19번 코드는 static_assert의 조건을 만족하지 못하므로 Less than 100이라는 에러 메세지를 개발단계에서 개발자에게 경고를 해주게 됩니다.

[C++11] std::thread

기존의 C++에서의 Thread는 운영체제에 종속되는 API로 제공되거나 비표준이 아닌 라이브러리를 통해 제공되다가 C++11에서 완전한 표준으로써 자리잡아 단일 API로 동일한 코드로 개발이 가능하게 되었습니다. 아래의 코드는 C++11에서 제공하는 std::thread를 사용한 간단한 예입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;

void thread_function(int tid) {
    cout << "abcdefghijklmnopqrstuvwxyz" << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    const int cntThreads = 100000;
    thread t[cntThreads];

    for(int i = 0; i < cntThreads; ++i) {
        t[i] = thread(thread_function, i);
    }

    for(int i = 0; i < cntThreads; ++i) {
        t[i].join();
    }

    return 0;
}

위의 코드는 10만개의 스레드를 생성하고 있고 각 스레드는 7번 코드에서 정의한 thread_function 함수를 실행합니다. 17번 코드가 스레드를 생성하고 실행하는 코드로 첫번째 인자는 실행할 함수이고 두번째는 스레드의 id입니다. 그리고 21번 코드는 스레드가 완전히 종료될때까지 주 스레드를 대기시키는 join 함수입니다. 위의 코드를 실행해 보면 10만개의 스레드가 abc..... 문자열이 서로 뒤섞여 표시되는 것을 볼 수 있는데, 이를 위해 동기화를 해줄 필요가 있습니다. 다음 코드는 이러한 동기화를 위해 C++11에서 제공되는 std::mutex를 사용한 예제입니다.

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

using namespace std;

mutex m;

void thread_function(int tid) {
    m.lock();

    cout << "abcdefghijklmnopqrstuvwxyz" << endl;

    m.unlock();
}

int _tmain(int argc, _TCHAR* argv[])
{
    const int cntThreads = 100000;
    thread t[cntThreads];

    for(int i = 0; i < cntThreads; ++i) {
        t[i] = thread(thread_function, i);
    }

    for(int i = 0; i < cntThreads; ++i) {
        t[i].join();
    }

    return 0;
}

변경된 부분은 8번 코드에서 동기화를 위한 mutex 객체를 하나를 정의했고 스레드 함수 내에서 abc.... 문자열을 표시하는 부분을 동기화 하기 위해 이 코드의 앞뒤에 mutex의 lock과 unlock 함수를 호출하고 있습니다. 이렇게 하면 이들 코드 사이에 있는 코드가 동기화가 되어 문자열이 뒤섞이지 않습니다.

[C++11] 정밀한 시간 측정을 위한 chrono

하드웨어의 발전으로 보다 정밀한 시간을 측정할 수 있게 되었고, 이를 위해 C++11에서는 chrono를 제공합니다. chrono를 이용하여 측정한 시간값의 정밀도는 나노(nano, 0.000000001)초라고 합니다. 아래는 코드는 어떠한 연산에 대한 소요 시간을 측정하는 경우로써 chrono를 사용한 코드입니다.

#include "stdafx.h"
#include 
#include 

using namespace std;
using namespace chrono;

int _tmain(int argc, _TCHAR* argv[])
{
    system_clock::time_point tp1 = system_clock::now();
	
    double v = 0;
    for(long i = 0; i < 100000000; i++) {
        v += 0.1;
    }

    system_clock::time_point tp2 = system_clock::now();

    nanoseconds t = tp2 - tp1;
    cout << "nanoseconds: " << t.count() << endl;
    cout << "seconds: " << t.count() / 1000000000.0 << endl;

    return 0;
}

실행 시간을 측정한 연산에 대한 코드는 12번 ~ 15번입니다. 이 코드 부분의 앞뒤로 chrono를 이용하여 시간을 측정하고 있습니다. 측정된 시간의 차이(17번 코드)를 통해 연산에 소요되는 시간을 알 수 있습니다. 시간 차이에 대한 단위는 nano이며 이를 초(second)로 변환하기 위해 21번 코드처럼 1000000000값으로 나누어 출력하고 있습니다.