小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

從 C 98 到 C 17,元編程是如何演進的?|技術頭條

 flyk0tcfb46p9f 2019-01-25

作者 | 祁宇

責編 | 郭芮

出品 | CSDN(ID:CSDNnews)

不斷出現(xiàn)的C++新的標準,正在改變元編程的編程思想,新的idea和方法不斷涌現(xiàn),讓元編程變得越來越簡單,讓C++變得簡單也是C++未來的一個趨勢。

很多人對元編程有一些誤解,認為代碼晦澀難懂,編譯錯誤提示很糟糕,還會讓編譯時間變長,對元編程有一種厭惡感。不可否認,元編程確實有這樣或那樣的缺點,但是它同時也有非常鮮明的優(yōu)點:

  • zero-overhead的編譯期計算;
  • 簡潔而優(yōu)雅地解決問題;
  • 終極抽象。

在我看來元編程最大的魅力是它常常能化腐朽為神奇,幫我們寫出dream code!

C++98模版元編程思想

C++98中的模版元編程通常和這些特性和方法有關:

  • 元函數(shù);
  • SFINAE;
  • 模版遞歸;
  • 遞歸繼承;
  • Tag Dispatch;
  • 模版特化/偏特化。

元函數(shù)

元函數(shù)就是編譯期函數(shù)調(diào)用的類或模版類。比如下面這個例子:

template

struct add_pointer { typedef T* type; };

typedef typename add_pointer::type int_pointer;

addpointer就是一個元函數(shù)(模版類),元函數(shù)的調(diào)用是通過訪問其sub-type實現(xiàn)的,比如addpointer::type就是調(diào)用add_pointer元函數(shù)了。

這里面類型T作為元函數(shù)的value,類型是元編程中的一等公民。模版元編程概念上是函數(shù)式編程,對應于一個普通函數(shù),值作為參數(shù)傳給函數(shù),在模版元里,類型作為元函數(shù)的參數(shù)被傳來傳去。

SFINAE

替換失敗不是錯誤。

template

struct enable_if {};

template

struct enable_if { typedef T type; };

template

typename enable_if::type

foo(T t) {}

foo(1); //ok

foo('a'); //compile error

在上面的例子中,調(diào)用foo('a')模版函數(shù)的時候,有一個模版實例化的過程,這個過程中會替換模版參數(shù),如果模版參數(shù)替換失敗,比如不符合編譯期的某個條件,那么這個模版實例化會失敗,但是這時候編譯器不認為這是一個錯誤,還會繼續(xù)尋找其他的替換方案,直到所有的都失敗時才會產(chǎn)生編譯錯誤,這就是SFINAE。SFINAE實際上是一種編譯期的選擇,不斷去選擇直到選擇到一個合適的版本位置,其實它也可以認為是基于模板實例化的tag dispatch。

模版遞歸,模版特化

template struct fact98 {

static const int value = n * fact98::value;

};

template <> struct fact98<0> {

static const int value = 1;

};

std::cout << fact98<5>::value << std::endl;

這是模版元編程的hello world例子,通過模版特化和模版遞歸實現(xiàn)編譯期計算。

在C++98中模版元編程的集大成者的庫是boost.mpl和boost.fusion,boost.mpl主要提供了編譯期類型容器和算法,boost.fusion通過異構的編譯期容器融合編譯期和運行期計算。

struct print{

template

void operator()(T const& x) const{

std::cout << typeid(x).name() << std::endl;

}

};

template

void print_names(Sequence const& seq){

for_each(filter_if<><_> >(seq), print());

}

boost::fusion::vector stuff(2018, 'i', 'purecpp');

print_names(stuff);

<_>

上面這個例子遍歷boost::fusion::vector異構容器,打印其中的string類型。

關于C++98模版元的書可以看《modern c++ design》和《c++ templates》。

Modern C++ metaprogramming編程思想

C++11中的元編程思想

Modern C++新標準對于元編程有著深刻的影響,一些新的編程思想和方法涌現(xiàn),但總體趨勢是元編程變得更簡單了。比如C++98中的add_pointer元函數(shù),我們需要寫一個模版類:

template

struct add_pointer { typedef T* type; };

typedef typename add_pointer::type int_pointer;

而在C++11中我們只需要使用C++11的新特性模版別名就可以定義一個add_pointer元函數(shù)了,代碼變得更簡潔了。

template using add_pointer = T*;

using int_pointer = add_pointer;

在C++11中,元函數(shù)由模版類變?yōu)槟0鎰e名了。C++11中提供了大量元函數(shù)在type_traits庫中,這樣我們不用再自己寫了,直接拿過來使用就行了。

C++11中另外的一個新特性variadic template可以作為一個類型容器,我們可以通過variadic templates pack訪問模版參數(shù),不需要通過模版遞歸和特化來訪問模版參數(shù)。

template struct meta_list {};

using list_of_ints = meta_list;

template struct list_size;

template<> class List, class... Elements>

struct list_size<>>

: std::integral_constant {};

constexpr auto size = list_size<>>::value;

constexpr auto size1 = list_size::value;

constexpr auto size2 = list_size<>>::value;

通過variadic template pack讓編譯器幫助我們訪問類型,比C++98中通過模版遞歸和特化來訪問類型效率更高。

C++11中另外一個新特性constexpr也讓我們編寫元函數(shù)變得更簡單了。

在C++98中:

template  struct fact98 {

static const int value = n * fact98::value;

};

template <> struct fact98<0> {

static const int value = 1;

};

std::cout << fact98<5>::value << std::endl;

在C++11中:

constexpr int fact11(int n) {

return n <= 1 ? 1 : (n * fact11(n - 1));

}

我們不再需要通過模版特化和遞歸來做編譯期計算了,我們直接通過新的關鍵字constexpr來實現(xiàn)編譯期計算,它修飾一個函數(shù),表明這個函數(shù)是在編譯期計算的,這個函數(shù)和一個普通函數(shù)看起來幾乎沒有分別,唯一的差別就是多了一個constexpr,比C++98的寫法簡單多了。

不過在C++11中constexpr的限制比較多,比如說constexpr函數(shù)中只能是個表達式,無法使用變量,循環(huán)等語句,在C++14中就去掉這個限制了,讓我們可以更方便地寫編譯期計算的函數(shù)了。

C++14中的元編程思想

//in c++11

constexpr int fact11(int n) {

return n <= 1 ? 1 : (n * fact11(n - 1));

}

//in c++14

constexpr int fact14(int n) {

int s = 1;

for (int i = 1; i <= n; i++) { s = s * i; }

return s;

}

可以看到在C++14中我們寫constexpr編譯期計算的函數(shù)時,不必受限于表達式語句了,可以定義變量和寫循環(huán)語句了,這樣也不用通過遞歸去計算了,直接通過循環(huán)語句就可以得到編譯期計算結果了,使用起來更方便了。

在C++14中除了constexpr增強之外,更重要的幾個影響元編程思想的特性是constexpr, generic lambda, variable template。新標準、新特性會產(chǎn)生新的編程思想,在C++14里元編程的編程思想發(fā)生了重大的變化!

在2014年Louis Dionne用C++14寫的一個叫Hana的元編程庫橫空出世,它的出現(xiàn)在C++社區(qū)引起震動,因為它所采用的方法不再是經(jīng)典的模版元的那一套方法了,是真正意義上的函數(shù)式編程實現(xiàn)的。模版元在概念上是函數(shù)式編程,而Hana是第一次在寫法上也變成函數(shù)式編程了,這是C++元編程思想的一個重大改變。

Boost.Hana的編程思想

通過一個例子來看Boost.Hana的編程思想:

template

struct type_wrapper {

using type = T;

};

template

type_wrapper type{};

//type to value

auto the_int_type = type;

//value to type

using the_real_int_type = decltype(the_int_type)::type;

這里我們定義了一個類型的wraper,里面只有一個子類型,接著定義這個wraper的變量模版,有了這個變量模版,我們就可以很方便的實現(xiàn)type-to-value和value-to-type了。

某個具體類型的變量模版就代表一個值,通過decltype這個值就能得到變量模版的類型了,有了這個變量模版,我們就可以通過Lambda寫元函數(shù)了,這里的Lambda是C++14中的generic lambda,這個Lambda的參數(shù)就是一個變量模版值,在Lambda表達式中,我們可以對獲取值的sub type并做轉換,然后再返回變換之后的變量模版值。

template 

type_wrapper type{};

constexpr auto add_pointer = [](auto t) {

using T = typename decltype(t)::type;

return type<>>; //type to value

};

constexpr auto intptr = add_pointer(type);

static_assert(std::is_same_v); //value to type

這里的add_pointer元函數(shù)不再是一個模版類或者模版別名了,而是一個Lambda表達式。這里面關鍵的兩個地方是如何把類型變?yōu)橹岛桶阎底優(yōu)轭愋?,通過C++14的變量模版就可以實現(xiàn)這個目標了。

Boost.Hana的目標是通過類型容器融合編譯期和運行期計算,替代boost.mpl和boost.fusion!比如下面的例子:

auto animal_types = hana::make_tuple(hana::type_c, hana::type_c, hana::type_c);

auto animal_ptrs = hana::filter(animal_types, [](auto a) {

return hana::traits::is_pointer(a);

});

static_assert(animal_ptrs == hana::make_tuple(hana::type_c, hana::type_c), '');

auto animals = hana::make_tuple(Fish{ 'Nemo' }, Cat{ 'Garfield' }, Dog{ 'Snoopy' });

auto names = hana::transform(animals, [](auto a) {

return a.name;

});

assert(hana::reverse(names) == hana::make_tuple('Snoopy', 'Garfield', 'Nemo'));

我們既可以操作類型容器中的類型,又可以操作類型容器中的運行期的值,Hana可以幫我們很方便地融合編譯期與運行期的計算。

Boost.Hana的特點:

  • 元函數(shù)不再是類或類模版,而是lambda;
  • 不再基于類型,而是基于值;
  • 沒有SFINAE,沒有模版遞歸;
  • 函數(shù)式編程;
  • 代碼更容易理解;
  • 元編程變得更簡單;
  • 融合編譯期與運行期。

以Boost.Hana為代表的元編程實現(xiàn)不再是經(jīng)典的type level的思想了,而是以C++14新特性實現(xiàn)的lambda level的函數(shù)式編程思想了。

C++17元編程思想

在C++17中,元編程得到了進一步地簡化,比如我們之前需要借助模版特化,SFINAE才能實現(xiàn)的編譯期選擇,現(xiàn)在通過if constexpr就可以很輕松的實現(xiàn)了。

在C++98中:

template

auto& get(person& p);

template<>

auto& get<0>(person& p) {

return p.id;

}

template<>

auto& get<1>(person& p) {

return p.name;

}

template<>

auto& get<2>(person& p) {

return p.age;

}

在C++17中:

template

auto& get(person& p) {

if constexpr (I == 0) {

return p.id;

}

else if constexpr (I == 1) {

return p.name;

}

else if constexpr (I == 2) {

return p.age;

}

}

這里不再需要模版特化了,也不需要拆分成多個函數(shù)了,就像普通的if-else語句一樣寫編譯期選擇的代碼,簡潔易懂!

在C++14中:

template 

std::enable_if_t<>, std::string> to_string(T t){

return t;

}

template

std::enable_if_t, std::string> to_string(T t){

return std::to_string(t);

}

在C++17中:

template

std::string to_string(T t){

if constexpr(std::is_same_v)

return t;

else

return std::to_string(t);

}

這里不再需要SFINAE了,同樣可以實現(xiàn)編譯期選擇,代碼更加簡潔。

C++元編程的庫以這些庫為代表,這些庫代表了C++元編程思想不斷演進的一個趨勢:

  • C++98:boost.mpl,boost.fusion
  • C++11:boost.mp11,meta,brigand
  • C++14:boost.hana

從C++98到Modern C++,C++新標準新特性產(chǎn)生新的idea,讓元編程變得更簡單更強大,Newer is Better!

Modern C++元編程應用

編譯期檢查

元編程的一個典型應用就是編譯期檢查,這也是元編程最簡單的一個應用,簡單到用一行代碼就可以實現(xiàn)編譯期檢查。比如我們需要檢查程序運行的系統(tǒng)是32位的還是64位的,通過一個簡單的assert就可以實現(xiàn)了。

static_assert(sizeof(void *) == 8, 'expected 64-bit platform');

當系統(tǒng)為32位時就會產(chǎn)生一個編譯期錯誤并且編譯器會告訴你錯誤的原因。

這種編譯期檢查比通過#if define宏定義來檢查系統(tǒng)是32位還是64位好得多,因為宏定義可能存在忘記寫的問題,并不能在編譯期就檢查到錯誤,要到運行期才能發(fā)現(xiàn)問題,這時候就太晚了。

再看一個例子:

template

struct Matrix {

static_assert(Row >= 0, 'Row number must be positive.');

static_assert(Column >= 0, 'Column number must be positive.');

static_assert(Row + Column > 0, 'Row and Column must be greater than 0.');

};

在這個例子中,這個Matrix是非常安全的,完全不用擔心定義Matrix時行和列的值寫錯了,因為編譯器會在編譯期提醒你哪里寫錯了,而不是等到運行期才發(fā)現(xiàn)錯誤。

除了經(jīng)常用staticassert做編譯期檢查之外,我們還可以使用enableif來做編譯期檢查。

struct A {

void foo(){}

int member;

};

template

std::enable_if_t> foo(Function&& f) {

}

foo([] {}); //ok

foo(&A::foo); //compile error: no matching function for call to 'foo(void (A::*)())'

比如這個代碼,我們通過std::enableift來限定輸入?yún)?shù)的類型必須為非成員函數(shù),如果傳入了成員函數(shù)則會出現(xiàn)一個編譯期錯誤。

元編程可以讓我們的代碼更安全,幫助我們盡可能早地、在程序運行之前的編譯期就發(fā)現(xiàn)bug,讓編譯器而不是人來幫助我們發(fā)現(xiàn)bug。

編譯期探測

元編程可以幫助我們在編譯期探測一個成員函數(shù)或者成員變量是否存在。

template< class, class = void >

struct has_foo : std::false_type {};

template< class T >

struct has_foo< T, std::void_t<>().foo())> > :

std::true_type {};

template< class, class = void >

struct has_member : std::false_type {};

template< class T >

struct has_member< T, std::void_t<>().member)> > :

std::true_type {};

struct A {

void foo(){}

int member;

};

static_assert(has_foo< A >::value);

static_assert(has_member< A >::value);

我們借助C++17的void_t,就可以輕松實現(xiàn)編譯期探測功能了,這里實際上是利用了SFINAE特性,當decltype(std::declval().foo())成功了就表明存在foo成員函數(shù),否則就不存在。

通過編譯期探測我們可以很容易實現(xiàn)一個AOP(Aspect Oriented Programming)功能,AOP可以通過一系列的切面幫我們把核心邏輯和非核心邏輯分離。

server.set_http_handler('/aspect', [](request& req, response& res) {

res.render_string('hello world');

}, check{}, log_t{});

上面這段代碼的核心邏輯就是返回一個hello world,非核心邏輯就是檢查輸入?yún)?shù)和記錄日志,把非核心邏輯分離出來放到兩個切面中,不僅僅可以讓我們的核心邏輯保持簡潔,還可以讓我們可以更專注于核心邏輯。

實現(xiàn)AOP的思路很簡單,通過編譯期探測,探測切面中是否存在before或者after成員函數(shù),存在就調(diào)用。

constexpr bool has_befor_mtd = has_before::value;

if constexpr (has_befor_mtd)

r = item.before(req, res);

constexpr bool has_after_mtd = has_after::value;

if constexpr (has_after_mtd)

r = item.after(req, res);

為了讓編譯期探測的代碼能復用,并且支持可變模版參數(shù),我們可以寫一個通用的編譯期探測的代碼:

#define HAS_MEMBER(member)\

template\

struct has_##member\

{\

private:\

template static auto Check(int) -> decltype(std::declval().member(std::declval()...), std::true_type()); \

template static std::false_type Check(...);\

public:\

enum{value = std::is_same<>(0)), std::true_type>::value};\

};

HAS_MEMBER(before)

HAS_MEMBER(after)

具體代碼可以參考這里:https://github.com/qicosmos/feather。

注:這段宏代碼可以用c++20的std::is_detected替代,也可以寫一個C++14/17的代碼來替代這個宏:

namespace {

struct nonesuch {

nonesuch() = delete;

~nonesuch() = delete;

nonesuch(const nonesuch&) = delete;

void operator=(const nonesuch&) = delete;

};

template

template class Op, class... Args>

struct detector {

using value_t = std::false_type;

using type = Default;

};

template class Op, class... Args>

struct detector<>>, Op, Args...> {

using value_t = std::true_type;

using type = Op;

};

template<> class Op, class... Args>

using is_detected = typename detector::value_t;

template<> class Op, class... Args>

using detected_t = typename detector::type;

template

using has_before_t = decltype(std::declval().before(std::declval()...));

template

using has_after_t = decltype(std::declval().after(std::declval()...));

}

template

using has_before = is_detected;

template

using has_after = is_detected;

編譯期計算

編譯期計算包含了較多內(nèi)容,限于篇幅,我們重點說一下類型萃取的應用:

  • 類型計算;
  • 類型推導;
  • 類型萃?。?/u>
  • 類型轉換;
  • 數(shù)值計算:表達式模版,Xtensor,Eigen,Mshadow。

我們可以通過一個function_traits來萃取可調(diào)用對象的類型、參數(shù)類型、參數(shù)個數(shù)等類型信息。

template

struct function_traits_impl{

public:

enum { arity = sizeof...(Args) };

typedef Ret function_type(Args...);

typedef Ret result_type;

using stl_function_type = std::function;

typedef Ret(*pointer)(Args...);

template

struct args{

static_assert(I < arity, 'index is out of range, index must less than sizeof Args');

using type = typename std::tuple_element>::type;

};

typedef std::tuple<><>>...> tuple_type;

using args_type_t = std::tuple;

};

<>

完整代碼可以參考這里:https://github.com/qicosmos/cinatra。

有了這個function_traits之后就方便實現(xiàn)一個RPC路由了,以rest_rpc為例(https://github.com/qicosmos/rest_rpc):

struct rpc_service {

int add(int a, int b) { return a + b; }

std::string translate(const std::string& orignal) {

std::string temp = orignal;

for (auto& c : temp) c = toupper(c);

return temp;

}

};

rpc_server server;

server.register_handler('add', &rpc_service::add, &rpc_srv);

server.register_handler('translate', &rpc_service::translate, &rpc_srv);

auto result = client.call('add', 1, 2);

auto result = client.call('translate', 'hello');

RPCServer注冊了兩個服務函數(shù)add和translate,客戶端發(fā)起RPC調(diào)用,會傳RPC函數(shù)的實際參數(shù),這里需要把網(wǎng)絡傳過來的字節(jié)映射到一個函數(shù)并調(diào)用,這里就需要一個RPC路由來做這個事情。下面是RestRPC路由的實現(xiàn):

template

void register_nonmember_func(std::string const& name, const Function& f) {

this->map_invokers_[name] = { std::bind(&invoker::apply, f, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) };

}

template

struct invoker {

static void apply(const Function& func, const char* data, size_t size,

std::string& result) {

using args_tuple = typename function_traits::args_tuple;

msgpack_codec codec;

auto tp = codec.unpack(data, size);

call(func, result, tp);

}

};

RPCServer注冊RPC服務函數(shù)的時候,函數(shù)類型會保存在invoker中,后面收到網(wǎng)絡字節(jié)的時候,我們通過functiontraits萃取出函數(shù)參數(shù)對應的tuple類型,反序列化得到一個實例化的tuple之后就可以借助C++17的std::apply實現(xiàn)函數(shù)調(diào)用了。詳細代碼可以參考rest_rpc。

編譯期反射

通過編譯期反射,我們可以得到類型的元數(shù)據(jù),有了這個元數(shù)據(jù)之后我們就可以用它做很多有趣的事情了。可以用編譯期反射實現(xiàn):

  • 序列化引擎;
  • ORM;
  • 協(xié)議適配器。

以序列化引擎iguana(https://github.com/qicosmos/iguana)來舉例,通過編譯期反射可以很容易的將元數(shù)據(jù)映射為json、xml、msgpack或其他格式的數(shù)據(jù)。

struct person{

std::string name;

int age;

};

REFLECTION(person, name, age)

person p = {'tom', 20};

iguana::string_stream ss;

to_xml(ss, p);

to_json(ss, p);

to_msgpack(ss, p);

to_protobuf(ss, p);

以ORM引擎(https://github.com/qicosmos/ormpp)舉例,通過編譯期反射得到的元數(shù)據(jù)可以用來自動生成目標數(shù)據(jù)庫的SQL語句:

ormpp::dbng mysql;

ormpp::dbng sqlite;

ormpp::dbng postgres;

mysql.create_datatable();

sqlite.create_datatable();

postgres.create_datatable();

反射將進入C++23標準,未來的C++標準中的反射將更強大和易用。

融合編譯期和運行期

運行期和編譯期存在一個巨大的鴻溝,而在實際應用中我需要融合編譯期與運行期,這時候就需要一個橋梁來連接編譯期與運行期。編譯期和運行期從概念上可以簡單地認為分別代表了type和value,融合的關鍵就是如何實現(xiàn)type to value以及value to type。

Modern C++已經(jīng)給我們提供了便利,比如下面這個例子:

auto val = std::integral_constant{};

using int_type = decltype(val);

auto v = decltype(val)::value;

我們可以很方便地將一個值變?yōu)橐粋€類型,然后由通過類型獲得一個值。接下來我們來看一個具體的例子:如何根據(jù)一個運行時的值調(diào)用一個編譯期模版函數(shù)?

template

void fun() {}

void foo(int n) {

switch (n){

case 0:

fun<0>();

break;

case 1:

fun<1>();

break;

case 2:

fun<2>();

break;

default:

break;

}

}

這個代碼似乎很好地解決了這個問題,可以實現(xiàn)從運行期數(shù)值到編譯期模版函數(shù)調(diào)用。但是如果這個運行期數(shù)值越來越大的時候,我們這個switch就會越來越長,還存在寫錯的可能,比如調(diào)用了foo(100),那這時候真的需要寫100個switch-case嗎?所以這個寫法并不完美。

我們可以借助tuple來比較完美地解決這個問題:

namespace detail {

template

void tuple_switch(const std::size_t i, Tuple&& t, F&& f, std::index_sequence) {

(void)std::initializer_list {

(i == Is && (

(void)std::forward(f)(std::integral_constant{}), 0))...

};

}

} // namespace detail

template

inline void tuple_switch(const std::size_t i, Tuple&& t, F&& f) {

constexpr auto N =

std::tuple_size<>>::value;

detail::tuple_switch(i, std::forward(t), std::forward(f),

std::make_index_sequence{});

}

void foo(int n) {

std::tuple tp;

tuple_switch(n, tp, [](auto item) {

constexpr auto I = decltype(item)::value;

fun();

});

}

foo(1);

foo(2);

通過一個tuple_switch就可以通過運行期的值調(diào)用編譯期模版函數(shù)了,不用switch-case了。關于之前需要寫很長的switch-case語句的問題,也可以借助元編程來解決:

template

auto make_tuple_from_sequence(std::index_sequence)->decltype(std::make_tuple(Is...)) {

std::make_tuple(Is...);

}

template

constexpr auto make_tuple_from_sequence()->decltype(make_tuple_from_sequence(std::make_index_sequence{})) {

return make_tuple_from_sequence(std::make_index_sequence{});

}

void foo(int n) {

decltype(make_tuple_from_sequence<100>()) tp; //std::tuple

tuple_switch(n, tp, [](auto item) {

constexpr auto I = decltype(item)::value;

fun();

});

}

foo(98);

foo(99);

這里的decltype(maketuplefrom_sequence<100>())會自動生成一個有100個int的tuple輔助類型,有了這個輔助類型,我們完全不必要去寫長長的switch-case語句了。

有人也許會擔心,這里這么長的tuple會不會生成100個Lambda實例化代碼?這里其實不用擔心,因為編譯器可以做優(yōu)化,優(yōu)化的情況下只會生成一次Lambda實例化的代碼,而且實際場景中不可能存在100個分支的代碼。

接口的泛化與統(tǒng)一

元編程可以幫助我們?nèi)诤系讓赢悩嫷淖酉到y(tǒng)、屏蔽接口或系統(tǒng)的差異、提供統(tǒng)一的接口。

以ORM為例:

MySQL connect

mysql_real_connect(handle, '127.0.0.1', 'feather', '2018', 'testdb', 0, nullptr, 0);

PostgreSQL connect

PQconnectdb('host=localhost user=127.0.0.1 password=2018 dbname=testdb');

Sqlite connect

sqlite3_open('testdb', handle);

ORM unified connect interface

ORM::mysql.connect('127.0.0.1', “feather', “2018', 'testdb');

ORM::postgres.connect('127.0.0.1', “feather', “2018', 'testdb');

ORM::sqlite.connect('testdb');

不同的數(shù)據(jù)庫的C connector相同功能的接口是完全不同的,ormpp庫(https://github.com/qicosmos/ormpp)要做的一件事就是要屏蔽這些接口的差異,讓用戶可以試用統(tǒng)一的接口來操作數(shù)據(jù)庫,完全感受不到底層數(shù)據(jù)庫的差異。

元編程可以幫助我們實現(xiàn)這個目標,具體思路是通過可變參數(shù)模版來統(tǒng)一接口,通過policy-base設計和variadic templates來屏蔽數(shù)據(jù)庫接口差異。

template

class dbng{

template

bool connect(Args&&... args){

return db_.connect(std::forward(args)...);

}

template

bool connect(Args... args) {

if constexpr (sizeof...(Args)==5) {

return std::apply(&mysql_real_connect, std::make_tuple(args...);

}

else if constexpr (sizeof...(Args) == 4) {//postgresql}

else if constexpr (sizeof...(Args) == 2) {//sqlite}

}

這里通過connect(Args... args)統(tǒng)一連接數(shù)據(jù)庫的接口,然后再connect內(nèi)部通過if constexpr和變參來選擇不同的分支。if constexpr加variadic templates等于靜態(tài)多態(tài),這是C++17給我們提供的一種新的實現(xiàn)靜態(tài)多態(tài)方法。

這樣的好處是可以通過增加參數(shù)或修改參數(shù)類型方式來擴展接口,沒有繼承,沒有SFINAE,沒有模版特化,簡單直接。

消除重復(宏)

很多人喜歡用宏來減少手寫重復的代碼,比如下面這個例子,如果對每個枚舉類型都寫一個寫到輸出流里的代碼段,是重復而繁瑣的,于是就通過一個宏來消除這些重復代碼(事實上,這些重復代碼仍然會生成,只不過由編譯器幫助生成了)。

#define ENUM_TO_OSTREAM_FUNC(EnumType) \

std::ostream& operator<<(std::ostream& out_stream, const EnumType& x) { \

out_stream << static_cast(x); \

return out_stream; \

}

enum class MsgType { Connected, Timeout };

enum class DataType {Float, Int32};

ENUM_TO_OSTREAM_FUNC(MsgType);

ENUM_TO_OSTREAM_FUNC(DataType);

這看似是使用宏的合適場景,但是宏最大的問題是代碼無法調(diào)試,代碼的易讀性差,但是用元編程,我們不用寫這個宏了,也不用去寫宏定義了。

template

typename std::enable_if<>::value>::type>

std::ostream& operator<<(std::ostream& out_stream, T x) {

out_stream << static_cast(x);

return out_stream;

}

元編程比宏更好地解決了問題。

再看一個宏的例子:

#define CALL(name, ...) \

do { \

result ret = func(name); \

if (ret == 0) { \

__VA_ARGS__; \

do_something(name); \

} \

else { \

do_something (name); \

} \

} while (0)

CALL('root', func1(root_path));

CALL('temp', func2(temp_path));

這也是宏使用的一個典型場景——復用代碼段。當很多代碼段都是類似的時候,只有一點點代碼不同,那么就可以通過宏來避免手寫這些重復代碼。上面這個宏把不同的代碼段func1(rootpath),func2(temppath)作為參數(shù)傳進來,從而復用這個代碼段。

我們可以通過一個泛型函數(shù)來替換這個宏:

template

void Call(const std::string& name, Self * self, F f) {

auto ret = foo(name);

if (ret == 0) {

(self >*f)(name);

do_something(name);

}

else {

do_something(name);

}

}

事實上大部分宏能做的,元編程能做得更好、更完美!

接口易用和靈活性

還是以rest_rpc為例,我們可以注冊任意類型的RPC函數(shù),不管參數(shù)個數(shù)和類型是否相同、返回類型是否相同,這讓我們的注冊接口非常易用和靈活。

struct dummy{

int add(connection* conn, int a, int b) { return a + b; }

};

int add(connection* conn, int a, int b) { return a + b; }

rpc_server server(8080, 4);

dummy d;

server.register_handler('a', &dummy::add, &d);

server.register_handler('b', add);

server.register_handler('c', [](connection* conn) {});

server.register_handler('d', [](connection* conn, std::string s) {

return s;

});

這里我們使用元編程幫我們擦除了函數(shù)類型:

template

void register_nonmember_func(std::string const& name, const Function& f) {

this->map_invokers_[name] = { std::bind(&invoker::apply, f, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) };

}

template

struct invoker {

static void apply(const Function& func, const char* data, size_t size,

std::string& result) {

using args_tuple = typename function_traits::args_tuple;

msgpack_codec codec;

auto tp = codec.unpack(data, size);

call(func, result, tp);

}

};

typename Function做了類型擦除,typename functiontraits::argstuple幫我們還原了類型。

再來看另外一個例子,cinatra(https://github.com/qicosmos/cinatra)注冊路由函數(shù)的例子:

server.set_http_handler('/a', &person::foo);

server.set_http_handler('/b', &person::foo, log_t{});

server.set_http_handler('/c', &person::foo, log_t{}, check{});

server.set_http_handler('/d', &person::foo, log_t{}, check{}, enable_cache{ false });

server.set_http_handler('/e', &person::foo, log_t{}, enable_cache{ false }, check{});

server.set_http_handler('/f', &person::foo, enable_cache{ false }, log_t{}, check{});

這個例子中,用戶可以增加任意切面,還可以增加緩存參數(shù),切面和緩存參數(shù)的順序可以是任意的,這樣完全消除了用戶使用接口時需需要注意參數(shù)順序的負擔,完全是自由靈活的。這里并沒有使用多個重載函數(shù)做這個事情,而是借助元編程,把緩存參數(shù)過濾出來,這樣就可以無視外面?zhèn)魅雲(yún)?shù)的順序了。

過濾參數(shù)的代碼如下:

template

void set_http_handler(std::string_view name, Function&& f, AP&&... ap) {

if constexpr(has_type<>, std::tuple<>...>>::value) {

auto tp = filter<>>(std::forward(ap)...);

std::apply(f, std::move(tp));

}

else {

http_router_.register_handler(name, std::forward(f), std::forward(ap)...);

}

}

template

struct has_type;

template

struct has_type> : std::disjunction<>...> {};

template< typename T>

struct filter_helper{

static constexpr auto func(){

return std::tuple<>();

}

template< class... Args >

static constexpr auto func(T&&, Args&&...args){

return filter_helper::func(std::forward(args)...);

}

template< class X, class... Args >

static constexpr auto func(X&&x, Args&&...args){

return std::tuple_cat(std::make_tuple(std::forward(x)), filter_helper::func(std::forward(args)...));

}

};

這里通過C++17的std::disjunction來判斷是否存在某個類型,通過if constexpr實現(xiàn)編譯期選擇。

總結

C++新標準給元編程帶來了巨大的改變,不僅僅讓元編程變得簡單好寫了,還讓它變得更加強大了,幫助我們優(yōu)雅地解決了很多實際的問題。文中列舉到的元編程應用僅僅是冰山一角,還有很多其他方面的應用。

本文內(nèi)容為作者自 2018 中國 C++大會演講內(nèi)容整理而來。

作者:祁宇,Modern C++開源社區(qū)purecpp.org創(chuàng)始人,《深入應用 C++11》作者,開源庫cinatra、feather作者,熱愛開源,熱愛Modern C++。樂于研究和分享技術,多次在國際C++大會(cppcon)做演講。


    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多