[Plus C++0x] 람다(Lambda) 이야기 (마지막회)

C++0x 2010.06.03 00:00 Posted by 비회원
람다 함수 활용(2)

앞선 포스팅에서 람다 함수의 캡쳐(Capture) 기능에 대해 설명 했었습니다. (여기! 있습니다)
캡쳐 기능을 사용 하면 다음과 같이 활용 할 수 있습니다.

④ 템플릿을 대체하는 데 활용
람다 함수의 캡처 기능을 사용하면 람다 함수 몸체에서 외부 변수들을 마음껏 사용할 수 있을 뿐만 아니라 클로저(Closure)가 되어 함께 묶입니다. 이 기능을 활용하면 기존에 파라미터 타입과 개수 처리를 일반화하기 위해 사용하던 템플릿 사용을 지양할 수 있습니다. 아래 두 함수가 템플릿과 람다 함수를 이용해서 원하는 시점에 호출 되게 하려면 어떻게 하면 될까요?

// 템플릿 객체와 람다 함수를 이용해 간접적으로 호출될 예제 함수들


void Function_Arg1(int arg1)

{

cout << "Function_Arg1 : " << arg1 << endl;

}

 

void Function_Arg2(int arg1, const char* arg2)

{

cout << "Function_Arg2 : " << arg1 << "," << arg2 << endl;

}



위 두 함수를 특정 파라미터와 묶어 특정 시점에 호출해서 사용하고 싶을 경우 템플릿을 이용하면 아래와 같은 방법으로 사용할 수 있습니다.

// 템플릿 객체를 이용해 함수를 인자와 함께 묶어 특정 시점에 호출하는 예


int arg1 = 1004;

const char* arg2 = "Lambda!";

 

vector<ITask*> taskList;

 

// 1. 컨테이너에 담는다.

taskList.push_back( new Task_1<void, int>(&Function_Arg1, arg1) );

taskList.push_back( new Task_2<void, int, const char*>(&Function_Arg2, arg1, arg2) );

// 2. 특정 시점에 컨테이너를 순회하며 실행시킨다.

for( auto i = taskList.begin(); i != taskList.end(); ++i )

{

(*i)->Do();

}



위와 같은 코드로 템플릿 객체를 사용하려면 다음과 같은 코드가 필요합니다.

// 파라미터 1개짜리 함수를 담기 위한 템플릿 클래스


template<typename RetType, typename ArgType1>

class Task_1 : public ITask

{

typedef function<RetType(ArgType1)> FunctionType;

 

public:

Task_1(FunctionType f, ArgType1 a1)

: function_(f), arg1_(a1)

{

}

 

virtual void Do()

{

cout << "Task_1::Do()" << endl;

function_(arg1_);

}

private:

FunctionType   function_;

ArgType1       arg1_;

};

 

// 파라미터 2개짜리 함수를 담기 위한 템플릿 클래스

template<typename RetType, typename ArgType1, typename ArgType2>

class Task_2 : public ITask

{

typedef function<RetType(ArgType1,ArgType2)> FunctionType;

 

public:

Task_2(FunctionType f, ArgType1 a1, ArgType2 a2)

: function_(f), arg1_(a1), arg2_(a2)

{

}

 

virtual void Do()

{

cout << "Task_2::Do()" << endl;



실제 C++ 프로젝트를 진행하다 보면 위와 비슷한 형태의 템플릿 클래스를 자주 구현하게 됩니다. 이 때 발생하는 문제점은 함수의 파라미터가 늘어날 때마다 템플릿 클래스를 추가해 줘야 하고 코드가 직관적이지 않다는 점입니다. BOOST_PP를 이용하면 자동화할 수 있지만 디버깅이 굉장히 어렵고 작성자만 이해할 수 있는 코드가 만들어지곤 합니다. 때론 작성자도 이해 못하죠 ;)

그럼 이런 상황에서 람다 함수를 이용하면 어떨까요?
아래 예제를 보면 람다 함수의 캡처 기능을 이용해 깔끔하게 구현되는 것을 볼 수 있습니다. 뿐만 아니라 실행시킬 함수의 파라미터가 몇 개든 타입이 무엇이든 추가되는 코드는 없습니다. 앞으로 많은 부분에서 람다 함수를 이용해 템플릿 사용을 줄일 방안이 제안 되길 기대해 봅니다.


// 람다 함수를 이용해 함수와 인자를 묶어 특정 시점에 호출하는 예제


int arg1 = 1004;

const char* arg2 = "Lambda!"; 

...
...
typedef function<void(void)> LambdaType;


vector<LambdaType> lambdaList;

 

lambdaList.push_back( [=](){ Function_Arg1(arg1); } );

lambdaList.push_back( [=](){ Function_Arg2(arg1, arg2); } );

 

for_each( lambdaList.begin(), lambdaList.end(), [](LambdaType lambda)

{

lambda();

});




마치면서

4회에 걸쳐『Plus C++0x』람다(Lambda) 이야기를 했습니다. 막연히 람다(Lambda) 의 기능에 대해 설명 하기 보다는 이면에 깔린 배경 개념을 소개 함으로써 현대 프로그래밍 언어가 갖는 특징을 이야기 하고 싶었습니다.

다음 시리즈에서는 우측 값 참조(RValue Reference)에 대해 알아보고 C++0x에서 어떤 의미를 갖는지를 설명하려고 합니다. 람다(Lambda) 관련해서는 많은 내용을 한 번에 준비해서 쓰려니 고생스럽더군요. 이번엔 차근차근 포스팅 하면서 글을 완성할 수 있으면 좋겠네요 ;)

( 마이크로소프트웨어 6월호의 『생각의 직관적인 표현, 람다(Lambda)』를 보시면 보다 잘 정리 되고 추가 된 내용을 보실 수 있습니다. )


고자료
1. MSDN -
http://msdn.microsoft.com/en-us/library/dd293608.aspx
2. MSDN - http://msdn.microsoft.com/en-us/library/dd293599.aspx
3. MSDN - http://channel9.msdn.com/posts/kmcgrath/Lambda-Expressions-in-C/
4. MSDN - http://blogs.msdn.com/vcblog/archive/2008/11/18/stupid-lambda-tricks.aspx
5. Wikipedia - http://en.wikipedia.org/wiki/First-class_function
6. Wikipedia -  http://en.wikipedia.org/wiki/Higher-order_function
7. VSTS 2010 Team Blog -  http://vsts2010.net/category/Language%20Development/C++0x


저작자 표시 비영리 변경 금지
신고

[Plus C++0x] 람다(Lambda) 이야기 (3)

C++0x 2010.06.01 00:00 Posted by 비회원

람다 함수 활용(1)

지금까지 First-Class Object, Higher-Order Function, Closure와 같은 조금은 지루한 개념들을 살펴봤습니다. 이런 개념들을 설명하지 않으면 람다 함수를 함수 포인터나 함수 객체를 대체하는 Syntactic Sugar로 오해할 수도 있기 때문입니다. 또 어떻게 람다 함수를 일반 변수처럼 사용할 수 있는지 배경 지식을 설명하기 위해서였습니다.

그럼 이 람다 함수를 어디에 사용하면 좋을까요?앞으로 4가지 활용 예를 살펴 보겠습니다.


① 지연 호출(Deferred Call)에 활용
vector와 같은 자료구조에 저장한 후에 필요한 특정 시점에 호출하는 것입니다.
아래 예제에서는 람다 함수를 이용해 Task를 만들고 TaskManager라는 컨테이너 클래스를 통해 특정 시점에 람다 함수가 실행되는 것을 나타내고 있습니다.

TaskManager manager;

 

// TaskManager에 Task로 저장한다.

manager.AddTask( [](){ cout << “task1” << endl;} );

manager.AddTask( [](){ cout << “task2” << endl;} );

 

 

// 같은 스레드 혹은 다른 스레드에서 실행시킨다.

manager.Run();



② 비동기 호출과 결과 코드의 응집성을 높이는 데 활용

비동기 처리 코드의 문제점은 비동기 요청 함수를 호출하는 곳과 결과를 처리하는 함수가 동떨어져 있어 로직 흐름을 파악하기 어렵다는 것입니다. 비동기 요청 함수를 호출할 때 결과 처리에 대한 코드를 람다 함수로 구현해 파라미터로 전달하면 코드 응집성이 높아질 수 있습니다.
아래 예제에서는 Client 클래스의 요청에 대한 처리가 비동기적으로 이뤄질 때 람다 함수를 이용해 요청 시점에 결과를 어떻게 처리할 것인지 기술하는 것을 나타내고 있습니다.

 

class Client

{

public:

// 비동기 요청에 대한 결과를 처리할 람다 함수 타입

typedef function<void(int result)> AsynResultProcessor;

 

public:

void AsyncRequest(AsynResultEvent resultEvent)

{

resultEvent_ = resultEvent;

}

 

void Process()

{

Result_ = 1004;

 

// 미리 저장된 결과 처리 람다 함수를 호출한다.

resultProcessor_(result_);

}

private:

AsynResultProcessor    resultProcessor_;

int                      result_;

};

 

Client client;

 

// 응답을 어떻게 처리할 것인지 요청 시 기술할 수 있다.

client.AsyncRequest( [](int result)

{

// 어떻게 결과를 처리할 것인지 기술

cout << result << endl;

});

 

// 특정 시점에 처리한다.

client.Process();




③ 일회성 함수를 쉽게 구현하기 위해 활용

특히 STL을 이용하는 데 유용합니다. STL 알고리즘 함수들의 입력 파라미터로 람다 함수를 넘겨주면 따로 함수 객체를 정의하는 번거로움이 사라지고 코드 응집성이 높아지므로 STL 함수의 작동을 더 쉽게 이해할 수 있습니다. 아래 예제에서는 함수 객체와 람다 함수 이용을 비교해서 보여주고 있습니다.

 

//  for_each()를 위한 함수 객체

//  일회성 호출을 위해 많은 코드가 필요하고 가독성도 떨어진다.

struct LambdaFunctor

{

void operator()(int n) const

{

cout << n << " ";

}

};

 

int main()

{

vector<int> v;

 

for (int i = 0; i < 10; ++i)

{

v.push_back(i);

}

 

// 1. 함수 객체를 이용한 코드

for_each(v.begin(), v.end(), LambdaFunctor());

 

// 2. 람다 함수를 이용한 코드

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

 

cout << endl;

}




포스팅이 길어졌네요~
다음 글에서 람다 활용에 대해 마저 이야기 하고 "람다 이야기" 시리즈를 마치도록 하겠습니다.


저작자 표시 비영리 변경 금지
신고

[Plus C++0x] 람다(Lambda) 이야기 (2)

C++0x 2010.05.27 23:13 Posted by 비회원

지난 글에서 이야기 했던 람다(Lambda)의 배경 개념에 대해 알아보겠습니다.

람다 함수는 First-Class Object?
프로그래밍 언어를 이루는 class, struct, int와 같은 개체들 중에서 아래 조건을 만족하면 First-Class Object로 분류합니다. (First-Class Object는 분류의 한 갈래를 의미하는 것이지 상하 관계를 의미하진 않습니다.)

- 변수와 자료구조에 저장하고 사용할 수 있다.
- 함수의 입력 값으로 사용할 수 있다.
- 함수의 반환 값으로 사용할 수 있다.
- 실행 시간에 생성할 수 있다.

즉, 기존 C++에서는 class, struct, int 등이 First-Class Object 였고  아래 예제에서 볼 수 있듯이 C++0x에서 람다 함수(Lambda Function)가 First-Class Object의 조건들을 만족시킵니다.

...
int main()

{

string text = "C++0x Lambda!";

 

// 1. "코드 조각" 변수에 대입하기

function<void()> lambda = [=]()

{                               

cout << text << endl;

};

 

// 2. "코드 조각"을 자료구조에 저장하기

vector< function<void()> > container;

{               

container.push_back( lambda );

container.push_back( [=](){ cout << text << endl; } );

};

 

// 3. "코드 조각"을 함수의 입력 파라미터로 사용하기

for_each( container.begin(), container.end(), [](const function<void()>& f){ f(); } );

 

return 0;

}


위 조건들을 통해 First-Class Object들은 프로그래밍 언어에서 별 다른 제약 조건 없이 변수로 사용할 수 있게 됩니다. 최근 여러 언어들이 함수를 First-Class Object로 제공하는 이유도 함수를 값 처럼 사용할 수 있게 되면 여러 모로 유용하기 때문입니다. 특히 함수는 다른 First-Class Object들을 생성하거나 조작하는 일련의 코드 묶음이기 때문입니다.

C++0x에서는 람다 함수(Lambda Function)를 First-Class Object로 제공함으로써 코드 조각을 일반 변수처럼 사용할 수 있게 해줍니다.



람다 함수는 Higher-Order Function?

함수의 입력 값으로 함수를 전달 받거나 함수의 결과 값으로 함수를 반환할 수 있을 때 Higher-Order Function이라고 합니다. 아래 예제 코드를 보면 쉽게 알 수 있습니다.

 

int main()

{

// 1. 람다 함수를 반환 값으로 한다.

auto g = [](int x) -> function<int (int)>

{          

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

};

 

// 2. 람다 함수를 입력 값으로 받는다.

auto h = [](const function<int (int)>& f, int z)

{

return f(z) + 1;

};

 

auto a = h( g(7), 8 );

 

cout << a << endl;

}



예제를 보면 h( g(7), 8 )과 같이 표현된 것을 볼 수 있습니다. 마치 수학 책에서 보던 h( g(x), y )처럼 표현할 수 있는 것입니다. 람다 함수는 C++0x에서 First-Class Object 조건을 만족시키기 때문에 Higher-Order Function의 요구사항 또한 만족시킵니다.


람다 함수는 Closure?
클로저(Closure)는 함수를 호출한 상위 코드 블록의 변수들이 호출된 함수와 묶인 것을 뜻합니다. 즉 호출된 함수는 상위 코드 블록의 외부 변수와 묶여 자기만의 상태를 갖게 되는 것입니다.
C++0x에서는 람다 함수(Lambda Function)가 상위 코드 블록의 변수들과 묶여 클로저(Closure)가 될 수 있다. 이때 람다 함수에서 외부 변수들을 참조하는 것을 ‘캡처’라고 말합니다.
캡처(Capture)는 두 가지 방법으로 할 수 있습니다. 람다 시작을 나타내는 [] 기호 사이에 =과 & 기호를 이용할 수 있습니다. [=]은 값 복사를 의미하고 [&]는 값 참조를 의미합니다. 아래 예제 코드를 보시죠.

 

void capture()

{

int a = 0;

int b = 1;

int c = 2;

 

// 1. default 값 복사 캡처. 상위 코드 블록의 지역 변수 모두 값 복사 가능

[=](){ cout << a << “ “ << b << endl; }();  // 0 1 출력

 

// 2. default 값 참조 캡처. 상위 코드 블록의 지역 변수 모두 값 참조 가능

[&](){ cout << a << “ “ << b++ << endl;}(); // 0 1 출력

 

// 3. default 값 복사 캡처, b와 c 참조 캡처

[=, &b, &c](){ cout << a << “ “ << b << “ ” << c << endl; }();  // 0 2 2 출력

}



이번 포스팅에선 몇 가지 배경 개념들을 알아봤는데요, 도움이 되셨는지 모르겠습니다 ;)
다음 글에선 람다 함수의 활용 방안에 대해 알아보겠습니다.

저작자 표시 비영리 변경 금지
신고

[Plus C++0x] 람다(Lambda) 이야기 (1)

C++0x 2010.05.27 21:59 Posted by 비회원

마이크로소프트웨어 6월호에 실릴 C++ 0x 관련 글을 썼습니다.
제목은 『생각의 직관적인 표현, 람다(Lambda)』입니다.  팀 블로그에서는 『Plus C++0x』라는 제목으로 포스팅 하려고 합니다. 기존에 흥배님께서 C++ 0x에 대해 이미 좋은 글들을 많이 쓰셔서 표절하지 않으면서 무엇을 써야 할지 정말 많은 고민을 했답니다. ;)

그래서 약간 Advanced 하면서 불친절하게(?) 특정 주제에 대해 나름대로 재해석해보기로 했습니다. 첫 주제는 람다(Lambda) 로 잡았구요 다음은 우측값 참조(RValue Reference) 에 대해 쓰려고 합니다. 전체적인 아웃라인은 여기를 보시면 됩니다.


C++0x에 함수형 프로그래밍 언어 기능 추가!?
C++를 사용하다가 요즘 인기 있는 Python, C#, Ruby 같은 언어들을 쓰게 되면 무엇보다 직관적이고 편하다는 생각이 듭니다. 이 언어들은 람다(Lambda)와 클로저(Closure) 같은 함수형 프로그래밍 언어의 기능들을 지원함으로써 생각을 보다 직관적이고 효율적인 코드로 표현할 수 있게 합니다.


그렇다면 C++는 어떨까요?
포인터와 함수 객체 그리고 템플릿으로 점철된 C++ 프로그래밍 세계에도 이런 함수형 프로그래밍 언어의 기능들이 추가되면 직관적이고 덜 수고스러운 코딩을 할 수 있을까요?
마침 비주얼 스튜디오 2010이 발표되면서 비주얼 C++ 10가 공개됐습니다. 인텔리센스 기능 향상, 실시간 에러 검사, 병렬 라이브러리와 같은 막강한 기능이 추가됐지만 무엇보다도 C++0x라는 C++의 새로운 표준을 충실히 구현함으로써 C++ 프로그래머의 생산성을 높여 주목을 끌고 있습니다.
특히 C++0x는 언어의 핵심 기능으로 앞서 언급했던 람다(Lambda)와 클로저(Closure)를 기술하고 있고 이를 충실히 구현한 비주얼 C++ 10을 통해 보다 쉽게 사용할 수 있습니다.

이번 『Plus C++0x 람다이야기』를 통해 전달할 핵심 내용은 다음 두 가지 입니다.

C++0x에서 코드 조각(a piece of code)을 일반 변수처럼 사용할 수 있게 됐다.
C++0x에서 클로저(Closure)를 사용할 수 있게 됐다.


아래 코드를 보시죠.

#include <iostream>

#include <vector>

#include <string>

#include <algorithm>

#include <functional>

 

using namespace std;

 

int main()

{

string text = "C++0x Lambda!";

 

// 1. "코드 조각" 변수에 대입하기

function<void()> lambda = [=]()

{                               

cout << text << endl;

};

 

// 2. "코드 조각"을 자료구조에 저장하기

vector< function<void()> > container;

{               

container.push_back( lambda );

container.push_back( [=](){ cout << text << endl; } );

};

 

// 3. "코드 조각"을 함수의 입력 파라미터로 사용하기

for_each( container.begin(), container.end(), [](const function<void()>& f){ f(); } );

 

return 0;

}



위 예제에서 보는 것처럼 이렇게 일반 변수처럼 대입할 수 있고, 자료구조에 저장할 수 있고 함수의 입출력 값으로 사용할 수 있는 코드 조각을 람다(Lambda) 혹은 람다 함수(Lambda Function)라고 합니다.

람다(Lambda)를 이해하고 어떻게 사용하며 활용할지 설명하기 위해 First-Class Object, Higher-Order Function, Closure 같은 프로그래밍 언어 수업 시간에 나올 법한 개념들을 소개하려고 합니다. 개념적인 이해를 통해 람다(Lambda)와 친해져 보시죠 ;)

또 어떻게 사용 할지 몇 가지 사용 패턴을 제안해 볼까 합니다.
(약간 불친절하게 글이 진행됩니다. http://vsts2010.net에 있는C++0x 관련 글들을 먼저 읽어 볼 것을 권장합니다.)


람다 함수는 함수 안에 정의할 수 있는 이름 없는 함수(anonymous function)? 

...

void outer_function()

{

void inner_function()

{

}

}


위 코드를 보면 outer_function() 함수 안에 inner_function() 함수가 정의되어 있습니다. C++에서 이런 문법 사용이 가능할까요? 물론 사용할 수 없습니다. 즉, 함수 안에서 함수를 선언하고 정의할 수 없습니다. 그러나 람다 함수(Lambda Function)는 함수 안에서 선언하고 정의하고 호출할 수 있습니다. 


void outer_function()

{

// 이름 없는 함수! 람다!

[](){}();

}


위 예제에서 아무 동작을 하지 않는 람다 함수가 선언, 정의, 호출 됐습니다.

다음 편에서 이런 동작을 가능하게 하는 개념적 배경인 First-Class Object, Higher-Order Function, Closure 에 대해 알아보도록 하겠습니다.

저작자 표시 비영리 변경 금지
신고

- 럭키 세븐 -_-v

기분도 좋게 일곱번째 글이 되는 오늘은 아낌없이 표현해주는 나무, expresion tree를 가지고 이야기 해보도록 하겠습니다. LINQ의 뒤를 든든하게 받치고 있는 요소지만, 전면에 거의 드러나지 않아서 이게 뭔지 알아보려고 노력안하면 볼일이 없는 친구입니다. 저 역시 LINQ로 프로젝트를 진행하면서도 얉은 지식과 호기심으로 인해서 이런게 있다는 것도 모르고 있었는데요. 그리고 6월에 있었던 세미나에서는 '컴파일러같은 툴 개발자들에게나 적합한 기능인거 같다'는 망언을 하기도 했었습니다. 뭐 100% 틀린 말은 아니겠지만, 이해가 부족한 탓에 나온 망언이었던거니 혹시 마음상한 분 있으셨다면 여친한테 밴드라도 붙여달라고 하시면 좋을거 같네요. 여친없으시면 어머니한테라도...


- 표현해주는 나무나 빨랑 설명해봐

일단 expression tree는 실행가능한 코드를 데이터로 변환가능한 방법을 제공해주는 요소입니다. 이렇게 데이터로 변환하게 되면 컴파일하기 전에 코드를 변경한다거나 하는 일이 매우 수월해지는데요. 예를 들면, C#의 LINQ 쿼리식을 sql 데이터베이스같이 다른 프로세스상에서 수행하는 코드로 변환하는 경우 말이죠.

Func<int, int, int> function = (a, b) => a + b;

위와 같은 문장은 머리,가슴,배는 아니지만 세부분으로 구성됩니다.

1. 선언부 : Func<int, int, int> function
2. 등호 연산자 : =
3. 람다 식 : (a, b) => a + b;

현재 변수 function은 두 숫자를 받아서 어떻게 더하는지를 나타내는 코드를 참조하고 있습니다. 그리고 위의 람다 표현식을 메서드로 표현해본다면 대략 아래와 같은 모양이 되겠죠.

public int function(int a, int b)
{
    return a + b;
}

Func는 System네임스페이스에 아래와 같이 선언되어 있습니다.

public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2); 

이 선언이 우리가 두개의 숫자를 더하는 간단한 람다식을 선언하는 걸 도와준 셈이죠.


- 근데, 어케 코드에서 표현해주는 나무로...?

위에서 우리는 실행가능한 코드를 function이라는 변수를 통해서 참조할 수 있다는걸 봤습니다. 근데, expression tree는 실행가능한 코드나 아니고, 자료구조의 한 형태입니다. 그러면, 어떻게 표현식을(코드를) expression tree로 변환하는 걸까요? 그래서 LINQ가 좋은걸 준비해뒀습니다.

using System.Linq.Expressions;

....

Expression<Func<int, int, int>> expression = (a, b) => a + b; 

위와 같이만 하면, 람다식이 expression tree로 쑉~! 하고 변환이 됩니다. 그럼 이걸 좀 더 눈에 보이게 살펴볼까요? 여기에 가시면, C#으로 구현된 예제들을 받을 수 있는데요, 예제중에 ExpressionTreeVisualizer(이하, ETV)라는게 있습니다. expression tree를 TreeView컨트롤을 이용해서 보기쉽게 쪼개주는 놈이죠. (a, b) => a + b;를 한번 확인해볼까요?



위의 간단한 예제코드는, Expression<TDelegate>클래스를 사용하고 있는데요, 그 클래스의 4가지 프로퍼티를 ETV를 통해서 확인해보실 수 있습니다. 좀 더 확실하게 하기 위해서 '-'를 눌러서 다 접어볼까요?



위 그림을 보시면, 딱 4가지만 남아있죠?

  • Body : expression의 몸체를 리턴한다.
  • Parameters : 람다식의 파라미터를 리턴한다.
  • NodeType : expression tree의 특정노드의 ExpressionType을 리턴한다. ExpressionType은 45가지의 값을 가진 열거형타입인데, expression tree에 속할 수 있는 모든 노드의 목록이 포함되어 있다. 예를 들면, 상수를 리턴하거나, 파라미터를 리턴한다거나, 둘 중에 뭐가 더 큰지 결정한다거나(<,>), 두 값을 더한다거나(+) 하는 것들이 있다.
  • Type : expression의 정적인 타입을 리턴한다. 위의 예제같은 경우에는 Func<int, int, int>가 되겠다.

아래와 같은 코드를 통해서 위의 프로퍼티들의 값을 확인해볼 수 있습니다.

using System;
using System.Linq.Expressions;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            Expression<Func<int, int, int>> expr = (a, b) => a + b;

            BinaryExpression body = (BinaryExpression)expr.Body;
            ParameterExpression left = (ParameterExpression)body.Left;
            ParameterExpression right = (ParameterExpression)body.Right;

            Console.WriteLine(expr.Body);
            Console.WriteLine("표현식의 왼쪽 : {0}\n노드의 타입 : {1}"
                + "\n표현식의 오른쪽 : {2}\n몸체의 타입 : {3}",
                left.Name, body.NodeType, right.Name, body.Type);
        }
    }
}


그리고 실행결과는 아래와 같습니다.



결과에서 expression의 모든 요소들이 각각의 노드로 이루어진 자료구조라는 것을 확인해보실 수 있습니다. 그리고 반대로 expression tree를 코드로 변환해서 실행하는 것도 매우 간단합니다. 아래와 같이 딴 한줄이면 됩니다.

int result = expr.Compile() (3,5); 


- 근데 당췌 왜 코드를 나무로 바꾸는건데? 식목일이냥?

이제 expression tree에 대해서는 조금 익숙해지셨을 겁니다. 특히 이 expression tree가 LINQ to SQL에 아주 중요한 역할을 하는데요, 일단 아래의 LINQ to SQL 쿼리문을 보시져~.

var query = from u in db.Users
     where u.nickname == "boram"
     select new { u.uId, u.nickname };


위 쿼리문의 결과로 반환되는 타입을 확인해보면 아래와 같습니다.



넵 바로 IQueryable인데요, IQueryable의 정의를 확인해보면 아래와 같습니다.

public interface IQueryable : IEnumerable
{
    Type ElementType { get; }
    Expression Expression { get; }
    IQueryProvider Provider { get; }

즉, 멤버로 Expression타입의 프로퍼티를 가지고 있습니다. IQueryable의 인스턴스는 expression tree를 가지고 있도록 설계된 거죠. 그 expressio tree가 바로 코드로 작성한 LINQ쿼리문의 자료구조입니다. 그런데, 왜 이렇게 LINQ to SQL쿼리를 expression tree형태로 가지고 있는걸까요? 그 핵심은, A라는 프로그램의 코드에 LINQ to SQL쿼리가 있다고 했을때, 실제로 이 쿼리가 수행되는 곳이 A가 아니라 데이터베이스 서버라는 점에 있습니다. 즉, 프로그램에서 직접실행되는게 아니라 데이터베이스가 알아들을 수 있는 SQL쿼리형태로 변환을 해서, 그 쿼리를 데이터베이스에게 날려서 쿼리된 데이터를 받아온다는 거죠. 위의 LINQ to SQL쿼리는 대략 아래와 같은 SQL문으로 변환이 됩니다.

SELECT [t0].[uId], [t0].[nickname]
FROM [dbo].[Users] AS [t0]
WHERE [t0].[nickname] = @p0 

프로그램내에 존재하는 쿼리표현식은 SQL 쿼리로 변환되어서 다른 프로세스에서 사용되게끔 문자열 형태로 보낸다는 거죠. 그러면, 위에서 설명드린대로 실제 쿼리는 데이터베이스의 프로세스내에서 처리가 되구요. 즉, IL코드를 SQL쿼리로 변환하는 거 보다, expression tree같은 자료구조형태가 변환하기 훨씬 쉬울뿐더러, 최적화 같은 중간처리도 훨씬 용이하다는 겁니다. 하지만 LINQ to Objects를 통해서 쿼리를 해보면 결과의 타입은 IEnumerable<T>입니다. 왜 얘네들은 IQueryable<T>가 아닐까요? IEnumerable<T>의 정의를 보면 아래와 같습니다.

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();

즉, Expression타입의 프로퍼티가 없습니다. 왜냐면, LINQ eo Objects는 같은 프로세스내에서 처리될 객체들을 대상으로 쿼리를 하기 때문에 다른 형태로 변환될 필요가 없기 때문입니다. 그렇다면, 굳이 expression tree같은 자료구조로 변환할 필요가 없겠죠. 그래서 대략 아래와 같은 규칙이 성립합니다.
  • 코드가 같은 프로그램(또는 프로세스)내에서 실행되는 경우라면 IEnumerable<T>
  • 쿼리 표현식을 다른 프로그램(또는 프로세스)에서 처리하기 위해서 문자열 형태로 변환해야 한다면 expression tree를 포함하는 IQueryable<T>를 사용 


- 마치면서

일단 오늘은 C# 3.0에 포함되었던 expression tree에 대해서 설명을 드렸습니다. DLR이랑 dynamic이야기 하다가 난데없이 삼천포로 빠진 느낌이 드시겠지만(저는 경남 진주에 살았었는데, 아버지 따라 삼천포 자주 갔었습니다....이 이야기는 왜하는 거지..-_-), DLR에서 expression tree가 중요하게 사용되고, 또한 C# 3.0을 사용해보신 분들이라도 expression tree에 대해서 제대로 못짚고 넘어간 분들도 많으리라 생각합니다. 저 역시 그랬구요. 아무튼, 도움되셨기를 바라면서 다음에 뵙죠!


- 참고자료

1. http://blogs.msdn.com/charlie/archive/2008/01/31/expression-tree-basics.aspx

신고
오래만에 돌아왔습니다.

동영상 자막 번역은 영 아니다라는 판단하에(?), 일반적인 형식으로 C++0xVisual Studio 2010에서의 병렬 프로그래밍에 대해 글을 써볼 생각입니다.

일단 http://code.msdn.microsoft.com/concrtextras 에서 샘플 코드를 받으십시오. 그를 기준으로 당분간은 진행 예정입니다. 당연히 돌려보려면 Visual Studio 2010 베타1이 필요합니다.

오늘은 Parallel Patterns Library를 이용한 피보나치 수 계산 예부터 살펴보죠. 순차 수행 버전과 PPL의 태스크 기능을 이용한 병렬 수행 버전의 성능 비교가 첫번째 핵심 사항입니다. 그리고 malloc 대 Concurrency::Alloc의 성능 비교가 두번째 핵심 사항 되겠습니다.

소스를 찬찬히 살펴보죠.

    8 #include "windows.h"

    9 #include <ppl.h>

   10 

   11 using namespace Concurrency;

   12 

   13 int SPINCOUNT = 25;

   14 

   15 //Spins for a fixed number of loops

   16 #pragma optimize("", off)

   17 void delay()

   18 {

   19     for(int i=0;i < SPINCOUNT;++i);

   20 };

   21 #pragma optimize("", on)


먼저 헤더 파일 포함이 나오고, 각 작업의 계산 부하 조절을 위한 idle loop 함수가 나옵니다. pragma 디렉티브로 최적화를 해당 함수에 대해서만 끄고 있습니다. 그래야 실제 부하 조절 용도로 의미가 있겠죠.

   23 //Times execution of a functor in ms

   24 template <class Functor>

   25 __int64 time_call(Functor& fn)

   26 {

   27     __int64 begin, end;

   28     begin = GetTickCount();

   29     fn();

   30     end = GetTickCount();

   31     return end - begin;

   32 };


이 함수는 단순히 프로파일링(성능 측정)을 위한 유틸리티 함수 되겠습니다.

   34 //Computes the fibonacci number of 'n' serially

   35 int fib(int n)

   36 {

   37     delay();

   38     if (n< 2)

   39         return n;

   40     int n1, n2

   41     n1 = fib(n-1);

   42     n2 = fib(n-2);

   43     return n1 + n2;

   44 }


실제 가장 이해하기 쉬운 재귀 방식의 피보나치 수 구하기 함수입니다. 당연히 이 방식은 중복 계산이 많아 성능이 한참 떨어지게 되는데요, 이 글의 쟁점은 아니므로 넘어갑니다.

   45 //Computes the fibonacci number of 'n' in parallel

   46 int struct_fib(int n)

   47 {

   48     delay();

   49     if (n< 2)

   50         return n;

   51     int n1, n2;

   52 

   53     //declare a structured task group

   54     structured_task_group tasks;

   55 

   56     //invoke the first half as a task

   57     auto task1 = make_task([&n1,n]{n1 = struct_fib(n-1);});

   58     tasks.run(task1);

   59 

   60     //run the second recursive call inline

   61     n2 = struct_fib(n-2);

   62 

   63     //wait for completion

   64     tasks.wait();

   65 

   66     return n1 + n2;

   67 }


다음은 해당 알고리즘의 병렬 구현입니다. structured_task_group 변수를 하나 선언하고, make_task를 이용해 재귀 호출의 한쪽을 담당할 태스크를 만든 뒤, 태스크 그룹을 통해 돌립니다. tasks.wait() 호출을 통해 스폰한 태스크 작업이 마무리될 때까지 대기한 후, 최종 결과를 리턴하고 있습니다. C++0x의 람다가 쓰이고 있는 것을 확인하실 수 있습니다.
재귀적으로 호출이 되면서 꽤나 많은 태스크들이 만들어질텐데, 그냥 스레드 수준에서 직접 이런 작업을 했다면, 실제 하드웨어가 지원하는 병렬 수행의 수보다 과도하게 많은 스레드 생성이 이루어지면서, 상당히 비효율적으로 돌텐데요. PPL의 태스크 개념을 사용해 이렇게 보다 고수준에서 작업하면 그러한 오버헤드를 상당 부분 알아서 최적화 해줍니다.

   69 //Computes the fibonacci number of 'n' allocating storage for integers on heap

   70 int struct_fib_heap(int n)

   71 {

   72     delay();

   73     if (n< 2)

   74         return n;

   75     //n1 and n2 are now allocated on the heap

   76     int* n1;

   77     int* n2;

   78 

   79     //declare a task_group

   80     structured_task_group tg

   81 

   82     auto t1 = make_task([&]{

   83         n1 = (int*) malloc(sizeof(int));

   84         *n1 = struct_fib_heap(n-1);

   85     });

   86     tg.run(t1);

   87     n2 = (int*) malloc(sizeof(int));

   88     *n2 = struct_fib_heap(n-2);

   89     tg.wait();

   90     int result = *n1 + *n2;

   91     free(n1);

   92     free(n2);

   93     return result;

   94 }


malloc으로 힙에 버퍼를 잡아 결과를 리턴하는 버전입니다. Concurrency::Alloc과의 성능 비교를 위한 함수 되겠습니다.

   95 //Computes the fibonacci number of 'n' using the ConcRT suballocator

   96 int struct_fib_concrt_heap(int n)

   97 {

   98     delay();

   99     if (n< 2)

  100         return n;

  101     int* n1;

  102     int* n2;

  103     structured_task_group tg

  104     auto t1 = make_task([&]{

  105         n1 = (int*) Concurrency::Alloc(sizeof(int));

  106         *n1 = struct_fib_concrt_heap(n-1);

  107     });

  108     tg.run(t1);

  109     n2 = (int*) Concurrency::Alloc(sizeof(int));

  110     *n2 = struct_fib_concrt_heap(n-2);

  111     tg.wait();

  112     int result = *n1 + *n2;

  113     Concurrency::Free(n1);

  114     Concurrency::Free(n2);

  115     return result;

  116 }


이번에는 병렬 런타임의 suballocator를 이용하고 있습니다.

  117 int main()

  118 {

  119     int num = 30;

  120     SPINCOUNT = 500;

  121     double serial, parallel;

  122 

  123     //compare the timing of serial vs parallel fibonacci

  124     printf("computing fibonacci of %d serial vs parallel\n",num);

  125     printf("\tserial:   ");

  126     serial= (double)time_call([=](){fib(num);});

  127     printf("%4.0f ms\n",serial);

  128 

  129     printf("\tparallel: ");

  130     parallel = (double)time_call([=](){struct_fib(num);});

  131     printf("%4.0f ms\n",parallel);

  132 

  133     printf("\tspeedup: %4.2fX\n",serial/parallel);

  134 

  135     //compare the timing of malloc vs Concurrency::Alloc,

  136     //where we expect to get speedups because there are a large

  137     //number of small malloc and frees.

  138 

  139     //increase the number of tasks

  140     num = 34;

  141 

  142     //reduce the amount of 'work' in each task

  143     SPINCOUNT = 0;

  144 

  145     //execute fib using new & delete

  146     printf("computing fibonacci of %d using heap\n",num);

  147     printf("\tusing malloc:             ");

  148     serial= (double)time_call([=](){struct_fib_heap(num);});

  149     printf("%4.0f ms\n",serial);

  150 

  151     //execute fib using the concurrent suballocator

  152     printf("\tusing Concurrency::Alloc: ");

  153     parallel = (double)time_call([=](){struct_fib_concrt_heap(num);});

  154     printf("%4.0f ms\n",parallel);

  155 

  156     printf("\tspeedup: %4.2fX\n",serial/parallel);

  157 

  158     return 0;

  159 }


마지막으로  main 함수입니다. 단순히 각 함수를 호출하고 시간을 측정한 뒤 적절한 메시지를 출력하고 있습니다. 역시 람다가 쓰여 구문이 간결해졌습니다.

2 코어 머신에서 디버그 버전으로 돌려본 결과는 다음과 같습니다.


순차 대 병렬 버전의 경우 1.29배 속도 향상, malloc 대 Concurrency::Alloc의 경우 2.60배 속도 향상을 확인할 수 있습니다. 병렬 버전에서의 속도 향상이 생각보다 미미한데요. 제 컴퓨터가 좀 문제(?)가 있어서 그럴지도 모르겠습니다; 한편, 병렬 환경에서 메모리 할당 및 해제가 빈번한 경우, 반드시 병렬 런타임의 할당자를 써야겠군요.


추가: 제 블로그에 관련 주제로 글(두가지 C++ 차세대 병렬 플랫폼 간단 비교)을 하나 올렸습니다. 참고하세요. ^^
신고
네번째입니다.

Combinable에 대해 자세히 다루고 PPL을 마무리한 뒤, Asynchronous Agents Library가 소개됩니다.


잘못된 번역이나 부족한 부분 있으면 알려주세요. ^^
신고
세번째입니다.

관련 디버깅 유틸과 PPL 지원 알고리즘, 이미징 예제 병렬화, 동기화 개체자료구조 등이 소개됩니다.


잘못된 번역이나 부족한 부분 있으면 알려주세요. ^^
신고
두번째 10분입니다.

자막 제작에는 Jubler를 사용하였습니다. 추천드립니다. :)

이젠 툴도 어느 정도 익숙해져서, 실제 10분 자막 제작에 드는 시간을 측정해보았습니다. 2시간반이 걸리더군요. ㅠㅠ 그냥 혼자 대충 듣고 이해할 때와는 달리 글로 정확하게 표현을 하려다보니 쉽지 않군요. 제가 빠삭하게 알고 있는 내용도 아니어서 더 시간이 걸린듯.

그래도 PPL이 태스크 개념 중심이라는 것, PPL과 TBB의 연동 계획 등 새로운 것들을 저도 알게 되어 기뻤습니다. 


언제가될지 모르겠으나 다음 회도 기대해주세요.
신고
안녕하세요, 정재원이라고 합니다.

C++ 관련 글을 올리고 계신 흥배님과 마찬가지로 게임 개발자입니다.

그간 여러분들이 올려주시는 좋은 글들을 읽고만 있다가...; 무언가 올리지 않으면 짤리겠다는 위기감에 소재거리를 뒤져보았습니다;;

흥배님과 가능한한 겹치지 않는 주제와 방식으로 무엇이 적당할까 생각하던 중... 본 동영상을 발견하고는, 그래 여기에 자막을 달아보자! 생각하게 되었습니다. 작년 PDC에서 발표된 Parallel Pattern Library에 대한 동영상입니다.

왠지 간단할듯 여겨졌으나... 처음 해보는 자막 작업 쉽지 않더군요 ㅠㅠ 도구를 찾고 익히는데 시간이 더 들었습니다.

한시간이 넘는 것을 한번에 번역해 올리려면, 날새겠다는 생각이 들더군요; 유투브 업로드가 10분 미만의 동영상으로 제한된다는 것에 안도감(?)을 느꼈습니다...

이를 변명삼아 일단 10분 분량을 올립니다. 포스트 수를 늘리기 위한 수작 아니냐 라는 비난의 소리가 들립니다만... 맞습니다 ㅠㅠ 툴에 좀 더 익숙해지면 자주라도 올리겠습니다.


부족한 번역과 분량에 대해 건설적 충고 부탁드리고요, 기타 제 포스트에 제안 사항 있으시면 댓글 달아주십시오.
신고