C++11 features
used in Crow
Crow를 통해 살펴보는 C++11의 새 기능
하재승
@ipkn
ipknhama@gmail.com
Crow Framework을 작성하기 위해
사용한 기법들을 소개 (TMP 중심)
auto, lambda등
일반적인 C++ 개발에서 접하기 쉬운
기법들에 대한 설명은 생략
목표
목표
Crow
C++ Framework for writing Web server
http://github.com/ipkn/crow
GitHub Star 1063개
Hacker News vote 194
https://news.ycombinator.com/item?id=8002604
Web server 개념
브라우저로 접속해서 요청을 보내면
해당 요청에 맞는 응답을 보내줌
GET / HTTP/1.1
Host: www.example.com
HTTP/1.1 200 OK
Content-Length: 12
...
Hello World!
간단한 Crow 예제
#include “crow_all.h”
int main() {
crow::SimpleApp app;
CROW_ROUTE(app, “/”)
([]{
return “Hello world!”;
});
app.port(8080).run();
return 0;
}
… as Flask (from Quickstart)
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
목차
Routing
Compile time
Run time
Middleware
서버 구조
소개될 C++11 기능
template metaprogramming
constexpr
variadic template
SFINAE
decltype, declval
std::chrono, operator ””, enum class
왜 굳이 C++로?
https://github.com/ipkn/crow-benchmark
C++?
빠른 속도
멀티 패러다임 언어
절차형 + OOP + Generic (template)
template
compile time code generation
vector<T> →
vector<int>, vector<string>
T는 컴파일 타임에 결정되어야 한다.
Crow 클래스 구조
App
Server
Connection
Router
Rule (TaggedRule)
Trie
Routing
Routing - CROW_ROUTE
어떤 요청에 어떤 응답을 줄지 지정
CROW_ROUTE(app, “/”)
CROW_ROUTE(app, “/post/all”)
CROW_ROUTE(app, “/post/<int>”)
CROW_ROUTE(app,
“/near/<double>/<double>”)
CROW_ROUTE 목표
쓰기 편하게
실수하기 어렵게
잘못된 핸들러를 주면 에러나게
빠르게
실제 함수 호출 시 오버헤드를 최소화
CROW_ROUTE 기능 (1)
여러 타입의 인자 지원
<int>
<uint>
<double>
<str>
<path>
CROW_ROUTE 기능 (2)
컴파일 시간에 핸들러 타입 체크
CROW_ROUTE(app, “/post/<int>”)
([](string a) …
([](int a, int b) …
([](int a) ...
CROW_ROUTE 기능 (3)
여러 종류의 핸들러 지원
[](…)
[](const request& req, …)
[](const request& req, response& res, …)
* 어떻게 여러 종류를 지원하는지에 관해선 나중에 설명 예정
실제 CROW_ROUTE 구현
#define CROW_ROUTE(app, url)
app.route<
crow::black_magic::get_parameter_tag(url)
>(url)
black_magic ???!!!
이름을 말할 수 없는 그분
app.route<get_parameter_tag(url)>(url)
핸들러 형태 체크 → 컴파일 타임 → 템플릿 인자
실행 중에 URL 체크 → 런타임 → 함수 인자
실제 CROW_ROUTE 구현 (2)
app.route<get_parameter_tag(url)>(url)
핸들러 형태 체크 → 컴파일 타임 → 템플릿 인자
실행 중에 URL 체크 → 런타임 → 함수 인자
실제 CROW_ROUTE 구현 (2)
app.route<get_parameter_tag(url)>(url)
핸들러 형태 체크 → 컴파일 타임 → 템플릿 인자
실행 중에 URL 체크 → 런타임 → 함수 인자
실제 CROW_ROUTE 구현 (2)
app.route<get_parameter_tag(url)>(url)
스트링 값(“abc” 등)은 템플릿 인자로 사용할
수 없음
url로 부터 인자에 대한 정보를 얻어
템플릿 인자로 넣을 수 있는 값으로 변환
템플릿 인자부터 ..
constexpr function
constexpr int fact(int n) {
return (n <= 1)
? 1
: fact(n-1)*n;
}
char arr[fact(5)];
constexpr
컴파일 타임에 계산할 수도 있는 함수 또는 값
cin >> n; cout << fact(n); // 런타임
fact(5) == 120, 컴파일 타임에 계산 가능
→ 배열의 크기, 템플릿 인자 등으로 사용 가능
get_parameter_tag
URL 을 받아 인자 정보를 가지는 숫자로 변환
인자가 없는 경우 0
URL 인자는 5종류: <int>=1 <uint>=2 …
6진법으로 표현
(자세한 구현은 생략)
get_parameter_tag
“/”
“/post”
“/post/<int>”
“/near/<int>/<int>”
“/put_score/<double>”
“/wiki/<path>”
0
0
1
11
3
5
숫자에서 다시 타입 리스트 복원
0 → <>
1 → <int>
11 → <int, int>
13 → <double, int>
4 → <string>
5 → <string> * path도 타입은 string
black_magic::S
variadic template으로 타입 리스트를 구현
S<int, double, string> 이 가능
S<a,b>::push<c> === S<c, a, b>
S<a>::push_back<b> === S<a, b>
S<a,b,c>::rebind<Rule> === Rule<a,b,c>
black_magic::S<T…> 구현 (1)
template <typename ... T>
struct S {
template <typename U>
using push = S<U, T...>;
S<a,b>::push<c> === S<c, a, b>
* using A = B; === typedef B A;
black_magic::S<T…> 구현 (2)
template
<template <typename ...> class U>
using rebind = U<T...>;
S<a,b,c>::rebind<Rule> == Rule<a,b,c>
black_magic::arguments<uint64_t>
get_parameter_tag 값을 타입 리스트로
arguments<0>::type == S<>
arguments<3>::type == S<double>
arguments<11>::type == S<int, int>
arguments 구현
template <uint64_t Tag> struct arguments {
using subarg =
typename arguments<Tag/6>::type;
using type =
typename subargs::template push<
typename single_tag_to_type<Tag%6>::type
>;
};
실제로 일어나는 일
argument<31>
subarg = S<double>
type = S<int, double>
arg<3>
subarg = S<>
type = S<double>
partial template specialization
template <int N>
struct single_tag_to_type {};
template <>
struct single_tag_to_type<1> {
using type = int64_t;
};
// C++11은 아니지만..
arguments 구현
template <>
struct arguments<0> {
using type = S<>;
};
URL이 라우팅 규칙이 되기까지
get_parameter_tag(“/<int>/<int>”)
→ arguments<11>
→ S<int, int>
→ TaggedRule<int, int>
Router
vector<unique_ptr<BaseRule>> rules_;
Q&A for Routing 1
get_parameter_tag(“/<int>/<int>”)
→ arguments<11>
→ S<int, int>
→ TaggedRule<int, int>
실제 핸들러 호출하기
Routing 2
남은 문제
/v<uint>/<string>/<path>
“/v2/wiki/example/document”
TaggedRule<uint, string, string>
handler
[](uint32_t ver, string func, string path)
Rule, TaggedRule
최종적으로 호출될 핸들러를 가지고 있음
app.route 의 리턴 값
void handle(
const request& req,
response& res,
const routing_params& params)
Trie based routing
여러 rule 들의 URL을 모아서
요청이 들어왔을 때
어느 Rule에서 처리할 지 판단
트리 구조로 성능 향상
Trie example
GET statuses/home_timeline
GET statuses/retweets/:id
GET statuses/retweets_of_me
POST statuses/retweet/:id
(from Twitter REST API)
statuses/
retweethome_timeline
s/
_of_meuint
uint
/
Trie
add( string url, rule )
find( string url )
-> rule, routing_params
http://en.wikipedia.org/wiki/Trie
routing.h
routing_params
vector<int> int_params;
vector<uint> uint_params;
vector<double> double_params;
vector<string> string_params;
Trie example (1)
/near/<double>/<double>
“/near/37.1/10.1”
routing_params.double_params == {37.1, 10.1}
Trie example (2)
/v<uint>/<string>/<path>
“/v2/wiki/example/document”
routing_params
.uint_params == {2}
.string_params ==
{“wiki”, “example/document”}
핸들러 호출하기
/v<uint>/<string>/<path>
[](unsigned int, string, string)
handler(rp.uint_params[0],
rp.string_params[0],
rp.string_params[1])
compile time
호출부 구현
template <typename CP,
int NInt, int NUint, int NDouble, int NString,
typename S1, typename S2>
struct call;
S1 - URL로 부터 정의된 핸들러 인자 타입의 리스트
S2 - 호출을 위한 타입+인덱스 정보의 리스트
N?? - 인자 처리를 위한 인덱스 값들, CP - 호출 파라미터
TaggedRule<Args…>::handle
void handle(req, res, routing_params) {
call<CallParams, 0, 0, 0, 0, S<Args…>, S<>>(
call_params);
}
call_params
req + res + routing_params + handler
call 호출 과정 - /v<uint>/<string>/<path>
0 0 [uint, str, str] []
1 0 [str, str] [(uint, 0)]
1 1 [str] [(uint, 0) (str, 0)]
1 2 [] [(uint, 0) (str, 0) (str, 1)]
call을 재귀호출
NUint NStr S1 S2
call 호출 과정 - /v<uint>/<string>/<path>
0 0 [uint, str, str] []
NUint NStr S1 S2
call 호출 과정 - /v<uint>/<string>/<path>
0 0 [uint, str, str] []
→ 1 0 [str, str] [<uint, 0>]
NUint NStr S1 S2
struct call<CP, NInt, NUint, NDouble, NString,
S< int64_t, Args1...>, S<Args2...>> {
void operator()(CP cp) {
using pushed = typename S<Args2...>::
template push_back<call_pair<int64_t, NInt>>;
call<CP, NInt+1, NUint, NDouble, NString,
S<Args1...>, pushed>()(cp);
}
};
call 호출 과정 - /v<uint>/<string>/<path>
0 0 [uint, str, str] []
1 0 [str, str] [<uint, 0>]
→ 1 1 [str] [<uint, 0> <str, 0>]
NUint NStr S1 S2
call 호출 과정 - /v<uint>/<string>/<path>
0 0 [uint, str, str] []
1 0 [str, str] [<uint, 0>]
1 1 [str] [<uint, 0> <str, 0>]
→ 1 2 [] [<uint, 0>, <str, 0>, <str, 1>]
NUint NStr S1 S2
struct call<CP, NInt, NUint, NDouble, NString,
S<>, S<Args2...>> {
void operator()(CP cp) {
// 일부 생략
cp.res = cp.handler(
cp.routing_params.template get
<typename Args2::type>(Args2::pos) ...
);
cparams.res.end();
return;
T routing_params::get<T>(index)
template<> string
routing_params::get<string>(index) const
{
return string_params[index];
}
핸들러 호출하기
handler(rp.uint_params[0],
rp.string_params[0],
rp.string_params[1]);
handler(rp.get<uint>(0),
rp.get<string>(0),
rp.get<string>(1));
handler( rp.get<Args2::type>(Args2::pos)... )
핸들러 호출하기
Args… = <uint, 0>, <str, 0>, <str, 1>
handler( rp.get<Args::type>(Args::pos)... )
<uint,0>을 대입한 경우를 보면 = rp.get<uint>(0)
...을 통해서 반복되므로
rp.get<uint>(0), rp.get<str>(1), rp.get<str>(2)
struct call<CP, NInt, NUint, NDouble, NString,
S<>, S<Args2...>> {
void operator()(CP cp) {
// 일부 생략
cp.res = cp.handler(
cp.routing_params.template get
<typename Args2::type>(Args2::pos) ...
);
cparams.res.end();
return;
Variadic template from example
void print(A a, B b, C c, …)
모든 인자를 출력해주는 함수
print()
print(1) - 1
print(2, “hello”, 40) - 2hello40
print 구현
void print() {}
template <typename A, typename ... Args>
void print(A a, Args... args)
{
cout << a;
print(args...);
}
print(2, “hello”, 40) 실행
A = int
Args… = const char*, int
args… = “hello”, 40
cout << 2;
print(“hello”, 40);
print 실행
print<int, const char*, int>(2, “hello”, 40) - 2
print<const char*, int>(“hello”, 40) - hello
print<int>(40) - 40
print()
2hello40
Variadic template 대입의 예
args… = 1, 5, 10
(args+1)… = 1+1, 5+1, 10+1
func(args)… = func(1), func(5), func(10)
func(args…) = func(1, 5, 10)
Q&A for Routing 2
Trie
Routing parameter
Variadic template
Middleware
Middleware?
C 요청 -> S 응답 -> C 받음
요청 --[MW]--> 응답 --[MW]--> 받음
before_handle(req, res)
after_handle(req, res)
Example Middleware
Static page serving (cache control)
Cookie parsing, Session support
JSON parsing
Profiling, Logging
Middleware of express.js (Node.js)
app.use(function (req, res, next) {
req.cookies = parse_cookies(req);
next();
});
req에 임의로 변수추가 가능
Middleware 구현의 제약들
근데 우린 C++이잖아… 변수는 다 선언되어야
unordered_map<string, any> ???
+ express의 문제점: Dependency
세션은 사용하는데 쿠키파서가 없는 경우
이 문제를 어떻게 해결하면 좋을까?
각 MW별로 저장해야할 값들이 존재
어떤 MW는 다른 MW의 값을 참조해야 한다
(C++이니까)
필요한 MW가 빠진 경우 컴파일 에러가 발생해
야함
struct MW::context
MW가 필요한 값을 저장하는 struct
내용에 특별한 제한이 없음
App<A, B, C> app;
A, B, C 세 MW를 사용하는 app
SimpleApp == App<>
Crow에서의 해결 방식
ctx_A : public A::context
ctx_AB : public ctx_A, public B::context
ctx_ABC : public ctx_AB, public C::context
context : public ctx_ABC
실제로 ctx_AB === partial_context<A, B>
context 체인 상속 - App<A, B, C>
Crow에서의 해결 방식
template <typename AllContext>
before_handle(req, res, context& ctx,
AllContext& all_ctx)
from MW - all_ctx.get<A>()
from handler - app.get_context<A>(req)
Crow에서의 해결 방식 - 전체 호출 과정
A::before_handle(req, res, ctx_A)
B::before_handle(req, res, ctx_AB)
C::before_handle(req, res, ctx_ABC)
app.handle(req, res)
C::after_handle(req, res, ctx_ABC)
B::after_handle(req, res, ctx_AB)
A::after_handle(req, res, ctx_A)
Dependency 해결 (1)
Session MW는 Cookie MW에 의존한다면
app<Cookie, Session> - OK
app<Session> - error
app<Session, Cookie> - error
의존: all_ctx.get<Cookie>를 사용한다.
Dependency 해결 (2)
App<A, B, C> app;
A::before_handle(req, res, ctx_A)
B::before_handle(req, res, ctx_AB)
C::before_handle(req, res, ctx_ABC)
app.handle(req, res)
C::after_handle(req, res, ctx_ABC)
B::after_handle(req, res, ctx_AB)
A::after_handle(req, res, ctx_A)
Dependency 해결 (3)
ctx_A :: get<A> - OK
ctx_A :: get<B> - error
ctx_AB :: get<A> - OK
ctx_AB :: get<B> - OK
ctx_AB::get
template <typename T>
typename T::context& get() {
return
static_cast<typename T::context&>(*this);
}
상속한 MW의 context 타입으로만 캐스팅 가능
다른 MW과 연관이 없는 MW라면?
template <typename AllContext>
before_handle(req, res,
context& ctx, AllContext& all_ctx)
대부분의 경우
before_handle(req, res, context& ctx)
SFINAE
Substitution failure is not an error.
템플릿 인자를 대입하는 과정에서 발생하는 에
러는 에러로 간주하지 않는다. (모두 실패하는
경우에는 에러)
또는 이를 이용한 프로그래밍 기법을 지칭함
SFINAE 예제
template <typename T>
true_type has_iterator_checker(typename T::iterator *);
template <typename T>
false_type has_iterator_checker(...);
decltype(has_iterator_checker<int>()) === false_type
decltype(has_iterator_checker<string>()) === true_type
after_call_helper
template <typename MW, typename Context>
void after_call_helper(MW& mw, …,
decltype(
declval<MW>().before_handle(
declval<request&>(), declval<response&>(),
declval<typename MW::context&>(),
declval<Context&>()
))* dummy = 0)
decltype, declval<T>
decltype(X)
X의 선언형
declval<T>()
컴파일 타임에만 사용할 수 있는 T의 객체형
template <typename A, typename B>
auto add(A a, B b) -> decltype(a+b)
{ return a+b; }
template <typename A, typename B>
decltype(declval<A>() + declval<B>())
add(A a, B b)
C++14: return type deduction
after_call_helper
template <typename MW, typename Context>
void after_call_helper(MW& mw, …,
decltype(
declval<MW>().before_handle(
declval<request&>(), declval<response&>(),
declval<typename MW::context&>(),
declval<Context&>()
))* dummy = 0)
after_call_helper
template <typename MW, typename AllContext>
void after_call_helper(MW& mw, …,
decltype(
mw.before_handle(
declval<request&>(), declval<response&>(),
declval<typename MW::context&>(),
declval<AllContext&>()
))* dummy = 0)
after_call_helper
template <typename MW, typename Context>
void after_call_helper(MW& mw, …,
decltype(
mw.after_handle(req, res, ctx, all_ctx)
)
* dummy = 0)
after_call_helper
template <typename MW, typename Context>
void after_call_helper(MW& mw, …,
다음 함수를 호출할 수 있는가?
mw.before_handle(req, res, ctx, all_ctx)
)
after_call_helper
template <typename MW, typename AllContext>
void after_call_helper(MW& mw, …,
decltype(
declval<MW>().after_handle(
declval<request&>(), declval<response&>(),
declval<typename MW::context&>(),
declval<AllContext&>()
))* dummy = 0)
std::enable_if
SFINAE를 이용하기 편하게 만들어둔 도구
조건부로 구현을 선택
template <typename T>
enable_if<is_integral<T>::value> func(T x) …
handler 형태를 고를 때 사용 (생략)
http://en.cppreference.com/w/cpp/types/enable_if
CookieParser - Example Middleware
CookieParser::context
uo_map<string, string> jar,
cookies_to_add;
get_cookie(key)
set_cookie(key, value)
before - 파싱 + jar에 저장
after - Set-Cookie 헤더 추가
Q&A for Middleware
App<A, B, C> app;
context inheritance chain
Middleware call chain
CookieParser
SFINAE
Crow 서버 구조
고려해야 할 점
멀티 쓰레드 작업 배분
Connection 객체의 생성, 소멸 관리
HTTP 타임 아웃 (Keep-Alive)
초기 버전
boost::asio + joyent/http-parser
asio::io_service 하나
concurrency 개수 만큼 io_service.run()
shared_ptr<Connection> 으로 수명 관리
asio::deadline_timer로 타임아웃 처리
초당 처리 횟수가 100,000 이상..
shared_ptr의 atomic 연산이 성능 부하
deadline_timer 내부 자료구조 업데이트 부하
여러 개의 싱글 쓰레드 서버의 결합
N 개의 작업용 쓰레드 (asio::io_service ✕ N)
요청이 들어올 때마다
어느 쓰레드가 처리할지 RR로 분배
해당 요청에 대한 모든 처리는
담당한 쓰레드 안에서만 이루어짐
deadline_timer 성능 저하 문제
각 쓰레드 별로 thread_local한 타이머를 구현
관련 코드: dumb_timer_queue.h
shared_ptr 제거
Connection 객체를 하나의 쓰레드가 담당
atomic 연산 불필요
변경 후 성능 4배 향상
쓰레드간 통신 전혀 없음
* shared_ptr을 쓰지말란 얘기는 아닙니다.
그 외 사용된
C++11 기능들 소개
std::chrono
std::chrono::
system_clock
steady_clock
high_resolution_clock
(거의) 같은 인터페이스 제공
std::chrono 사용법
auto t1 = clock::now();
do_something();
auto t2 = clock::now();
if (t2 - t1 > seconds(10)) …
duration_cast<seconds>(t2 - t1).count()
system_clock
to_time_t, from_time_t 제공
steady_clock
무조건 증가함이 보장됨
high_resolution_clock
제일 높은 해상도를 제공
(가능한 짧은 시간 간격)
operator “”
C++11
std::chrono::seconds(10)
std::chrono::minutes(20)
std::string(“hello”)
C++14
10s
20min
“hello”s
POST request
CROW_ROUTE(app, “/update_score”)
.methods(“POST”_method)
([](const crow::request& req){
auto json = crow::json::load(req.body);
...
});
operator “” _method
constexpr crow::HTTPMethod operator ""
_method(const char* str, size_t len)
{
return
crow::black_magic::is_equ_p(str, "GET", 3)
? crow::HTTPMethod::GET :
…
};
enum class
enum class HTTPMethod
{
GET,
POST,
...
};
int m = HTTPMethod::GET; // error!
사용된 boost 모듈들
asio
lexical_cast
string
iequals
operators
<끝>(내용 발표는)
미리 나온 질문들
현업에서 어디까지 최신 기술을 쓰는게
좋을까요?
현업에서 어디까지 최신 기술...?
C++11 is not bleeding edge
technology. (개인적인 의견)
2011년?
트랜스포머3, 테라, 스카이림, 포탈2
node.js 0.3.3/0.6.6 → 0.12.0, RoR 3.1 → 4.2,
iPhone4 / iOS 5.0, 갤럭시 S2
현업에서 어디까지 최신 기술...?
팀에서 TDD를 사용하기 위해 고려해야할 점?
http://agile.egloos.com/5827377
사회적 자본이 더 중요한 경우가 많다.
C++14 - minor patch to 11
vector v(1000000)
auto lambda = [v = std::move(v), a =
3](auto x, auto y)
{return a + x + y;};
C++14 - minor patch to 11
그외 …
01010b, 1’234’5678’9 == 123456789
make_unique
...
C++14 - minor patch는 훼이크
auto p = [](auto& s, auto...x){
[](...){}((s<<x,1)...);
};
p(cout, 42, "hello?");
위 코드의 실행 결과를 설명하시오. (3점)
rvalue
정확한 얘기를 하려면
lvalue, glvalue, xvalue, rvalue, prvalue를...
http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2010/n305
5.pdf
정확히
rvalue
vector<string> lines;
while(cin) {
string line;
getline(cin, line)
lines.push_back(line);
}
rvalue
vector<string> lines;
while(cin) {
string line;
getline(cin, line)
lines.push_back(std::move(line));
}
string s0("숭");
string s1("그");
string s2("리");
string s3("당");
string s4("당 숭당당");
string dest =
s0 + " " + s1 + " " + s2 + " " + s3 + " " + s4;
rvalue
Strong exception guarantee
vector<T> v1, v2
v1 = std::move(v2);
move_if_noexcept
20150212 c++11 features used in crow

20150212 c++11 features used in crow