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

分享

從 C 98 到 C 17,元編程是如何演進(jìn)的?|技術(shù)頭條

 flyk0tcfb46p9f 2019-01-25

作者 | 祁宇

責(zé)編 | 郭芮

出品 | CSDN(ID:CSDNnews)

不斷出現(xiàn)的C++新的標(biāo)準(zhǔn),正在改變?cè)幊痰木幊趟枷?,新的idea和方法不斷涌現(xiàn),讓元編程變得越來(lái)越簡(jiǎn)單,讓C++變得簡(jiǎn)單也是C++未來(lái)的一個(gè)趨勢(shì)。

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

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

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

C++98模版元編程思想

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

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

元函數(shù)

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

template

struct add_pointer { typedef T* type; };

typedef typename add_pointer::type int_pointer;

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

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

SFINAE

替換失敗不是錯(cuò)誤。

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í)候,有一個(gè)模版實(shí)例化的過(guò)程,這個(gè)過(guò)程中會(huì)替換模版參數(shù),如果模版參數(shù)替換失敗,比如不符合編譯期的某個(gè)條件,那么這個(gè)模版實(shí)例化會(huì)失敗,但是這時(shí)候編譯器不認(rèn)為這是一個(gè)錯(cuò)誤,還會(huì)繼續(xù)尋找其他的替換方案,直到所有的都失敗時(shí)才會(huì)產(chǎn)生編譯錯(cuò)誤,這就是SFINAE。SFINAE實(shí)際上是一種編譯期的選擇,不斷去選擇直到選擇到一個(gè)合適的版本位置,其實(shí)它也可以認(rèn)為是基于模板實(shí)例化的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例子,通過(guò)模版特化和模版遞歸實(shí)現(xiàn)編譯期計(jì)算。

在C++98中模版元編程的集大成者的庫(kù)是boost.mpl和boost.fusion,boost.mpl主要提供了編譯期類(lèi)型容器和算法,boost.fusion通過(guò)異構(gòu)的編譯期容器融合編譯期和運(yùn)行期計(jì)算。

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);

<_>

上面這個(gè)例子遍歷boost::fusion::vector異構(gòu)容器,打印其中的string類(lèi)型。

關(guān)于C++98模版元的書(shū)可以看《modern c++ design》和《c++ templates》。

Modern C++ metaprogramming編程思想

C++11中的元編程思想

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

template

struct add_pointer { typedef T* type; };

typedef typename add_pointer::type int_pointer;

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

template using add_pointer = T*;

using int_pointer = add_pointer;

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

C++11中另外的一個(gè)新特性variadic template可以作為一個(gè)類(lèi)型容器,我們可以通過(guò)variadic templates pack訪問(wèn)模版參數(shù),不需要通過(guò)模版遞歸和特化來(lái)訪問(wèn)模版參數(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;

通過(guò)variadic template pack讓編譯器幫助我們?cè)L問(wèn)類(lèi)型,比C++98中通過(guò)模版遞歸和特化來(lái)訪問(wèn)類(lèi)型效率更高。

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

在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));

}

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

不過(guò)在C++11中constexpr的限制比較多,比如說(shuō)constexpr函數(shù)中只能是個(gè)表達(dá)式,無(wú)法使用變量,循環(huán)等語(yǔ)句,在C++14中就去掉這個(gè)限制了,讓我們可以更方便地寫(xiě)編譯期計(jì)算的函數(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中我們寫(xiě)constexpr編譯期計(jì)算的函數(shù)時(shí),不必受限于表達(dá)式語(yǔ)句了,可以定義變量和寫(xiě)循環(huán)語(yǔ)句了,這樣也不用通過(guò)遞歸去計(jì)算了,直接通過(guò)循環(huán)語(yǔ)句就可以得到編譯期計(jì)算結(jié)果了,使用起來(lái)更方便了。

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

在2014年Louis Dionne用C++14寫(xiě)的一個(gè)叫Hana的元編程庫(kù)橫空出世,它的出現(xiàn)在C++社區(qū)引起震動(dòng),因?yàn)樗捎玫姆椒ú辉偈墙?jīng)典的模版元的那一套方法了,是真正意義上的函數(shù)式編程實(shí)現(xiàn)的。模版元在概念上是函數(shù)式編程,而Hana是第一次在寫(xiě)法上也變成函數(shù)式編程了,這是C++元編程思想的一個(gè)重大改變。

Boost.Hana的編程思想

通過(guò)一個(gè)例子來(lái)看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;

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

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

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ù)不再是一個(gè)模版類(lèi)或者模版別名了,而是一個(gè)Lambda表達(dá)式。這里面關(guān)鍵的兩個(gè)地方是如何把類(lèi)型變?yōu)橹岛桶阎底優(yōu)轭?lèi)型,通過(guò)C++14的變量模版就可以實(shí)現(xiàn)這個(gè)目標(biāo)了。

Boost.Hana的目標(biāo)是通過(guò)類(lèi)型容器融合編譯期和運(yùn)行期計(jì)算,替代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'));

我們既可以操作類(lèi)型容器中的類(lèi)型,又可以操作類(lèi)型容器中的運(yùn)行期的值,Hana可以幫我們很方便地融合編譯期與運(yùn)行期的計(jì)算。

Boost.Hana的特點(diǎn):

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

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

C++17元編程思想

在C++17中,元編程得到了進(jìn)一步地簡(jiǎn)化,比如我們之前需要借助模版特化,SFINAE才能實(shí)現(xiàn)的編譯期選擇,現(xiàn)在通過(guò)if constexpr就可以很輕松的實(shí)現(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;

}

}

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

在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了,同樣可以實(shí)現(xiàn)編譯期選擇,代碼更加簡(jiǎn)潔。

C++元編程的庫(kù)以這些庫(kù)為代表,這些庫(kù)代表了C++元編程思想不斷演進(jìn)的一個(gè)趨勢(shì):

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

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

Modern C++元編程應(yīng)用

編譯期檢查

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

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

當(dāng)系統(tǒng)為32位時(shí)就會(huì)產(chǎn)生一個(gè)編譯期錯(cuò)誤并且編譯器會(huì)告訴你錯(cuò)誤的原因。

這種編譯期檢查比通過(guò)#if define宏定義來(lái)檢查系統(tǒng)是32位還是64位好得多,因?yàn)楹甓x可能存在忘記寫(xiě)的問(wèn)題,并不能在編譯期就檢查到錯(cuò)誤,要到運(yùn)行期才能發(fā)現(xiàn)問(wèn)題,這時(shí)候就太晚了。

再看一個(gè)例子:

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.');

};

在這個(gè)例子中,這個(gè)Matrix是非常安全的,完全不用擔(dān)心定義Matrix時(shí)行和列的值寫(xiě)錯(cuò)了,因?yàn)榫幾g器會(huì)在編譯期提醒你哪里寫(xiě)錯(cuò)了,而不是等到運(yùn)行期才發(fā)現(xiàn)錯(cuò)誤。

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

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::*)())'

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

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

編譯期探測(cè)

元編程可以幫助我們?cè)诰幾g期探測(cè)一個(gè)成員函數(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,就可以輕松實(shí)現(xiàn)編譯期探測(cè)功能了,這里實(shí)際上是利用了SFINAE特性,當(dāng)decltype(std::declval().foo())成功了就表明存在foo成員函數(shù),否則就不存在。

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

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

res.render_string('hello world');

}, check{}, log_t{});

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

實(shí)現(xiàn)AOP的思路很簡(jiǎn)單,通過(guò)編譯期探測(cè),探測(cè)切面中是否存在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);

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

#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替代,也可以寫(xiě)一個(gè)C++14/17的代碼來(lái)替代這個(gè)宏:

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;

編譯期計(jì)算

編譯期計(jì)算包含了較多內(nèi)容,限于篇幅,我們重點(diǎn)說(shuō)一下類(lèi)型萃取的應(yīng)用:

  • 類(lèi)型計(jì)算;
  • 類(lèi)型推導(dǎo);
  • 類(lèi)型萃??;
  • 類(lèi)型轉(zhuǎn)換;
  • 數(shù)值計(jì)算:表達(dá)式模版,Xtensor,Eigen,Mshadow。

我們可以通過(guò)一個(gè)function_traits來(lái)萃取可調(diào)用對(duì)象的類(lèi)型、參數(shù)類(lèi)型、參數(shù)個(gè)數(shù)等類(lèi)型信息。

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。

有了這個(gè)function_traits之后就方便實(shí)現(xiàn)一個(gè)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注冊(cè)了兩個(gè)服務(wù)函數(shù)add和translate,客戶端發(fā)起RPC調(diào)用,會(huì)傳RPC函數(shù)的實(shí)際參數(shù),這里需要把網(wǎng)絡(luò)傳過(guò)來(lái)的字節(jié)映射到一個(gè)函數(shù)并調(diào)用,這里就需要一個(gè)RPC路由來(lái)做這個(gè)事情。下面是RestRPC路由的實(shí)現(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注冊(cè)RPC服務(wù)函數(shù)的時(shí)候,函數(shù)類(lèi)型會(huì)保存在invoker中,后面收到網(wǎng)絡(luò)字節(jié)的時(shí)候,我們通過(guò)functiontraits萃取出函數(shù)參數(shù)對(duì)應(yīng)的tuple類(lèi)型,反序列化得到一個(gè)實(shí)例化的tuple之后就可以借助C++17的std::apply實(shí)現(xiàn)函數(shù)調(diào)用了。詳細(xì)代碼可以參考rest_rpc。

編譯期反射

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

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

以序列化引擎iguana(https://github.com/qicosmos/iguana)來(lái)舉例,通過(guò)編譯期反射可以很容易的將元數(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);

以O(shè)RM引擎(https://github.com/qicosmos/ormpp)舉例,通過(guò)編譯期反射得到的元數(shù)據(jù)可以用來(lái)自動(dòng)生成目標(biāo)數(shù)據(jù)庫(kù)的SQL語(yǔ)句:

ormpp::dbng mysql;

ormpp::dbng sqlite;

ormpp::dbng postgres;

mysql.create_datatable();

sqlite.create_datatable();

postgres.create_datatable();

反射將進(jìn)入C++23標(biāo)準(zhǔn),未來(lái)的C++標(biāo)準(zhǔn)中的反射將更強(qiáng)大和易用。

融合編譯期和運(yùn)行期

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

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

auto val = std::integral_constant{};

using int_type = decltype(val);

auto v = decltype(val)::value;

我們可以很方便地將一個(gè)值變?yōu)橐粋€(gè)類(lèi)型,然后由通過(guò)類(lèi)型獲得一個(gè)值。接下來(lái)我們來(lái)看一個(gè)具體的例子:如何根據(jù)一個(gè)運(yùn)行時(shí)的值調(diào)用一個(gè)編譯期模版函數(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;

}

}

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

我們可以借助tuple來(lái)比較完美地解決這個(gè)問(wèn)題:

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);

通過(guò)一個(gè)tuple_switch就可以通過(guò)運(yùn)行期的值調(diào)用編譯期模版函數(shù)了,不用switch-case了。關(guān)于之前需要寫(xiě)很長(zhǎng)的switch-case語(yǔ)句的問(wèn)題,也可以借助元編程來(lái)解決:

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>())會(huì)自動(dòng)生成一個(gè)有100個(gè)int的tuple輔助類(lèi)型,有了這個(gè)輔助類(lèi)型,我們完全不必要去寫(xiě)長(zhǎng)長(zhǎng)的switch-case語(yǔ)句了。

有人也許會(huì)擔(dān)心,這里這么長(zhǎng)的tuple會(huì)不會(huì)生成100個(gè)Lambda實(shí)例化代碼?這里其實(shí)不用擔(dān)心,因?yàn)榫幾g器可以做優(yōu)化,優(yōu)化的情況下只會(huì)生成一次Lambda實(shí)例化的代碼,而且實(shí)際場(chǎng)景中不可能存在100個(gè)分支的代碼。

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

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

以O(shè)RM為例:

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

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

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}

}

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

這樣的好處是可以通過(guò)增加參數(shù)或修改參數(shù)類(lèi)型方式來(lái)擴(kuò)展接口,沒(méi)有繼承,沒(méi)有SFINAE,沒(méi)有模版特化,簡(jiǎn)單直接。

消除重復(fù)(宏)

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

#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);

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

template

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

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

out_stream << static_cast(x);

return out_stream;

}

元編程比宏更好地解決了問(wèn)題。

再看一個(gè)宏的例子:

#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));

這也是宏使用的一個(gè)典型場(chǎng)景——復(fù)用代碼段。當(dāng)很多代碼段都是類(lèi)似的時(shí)候,只有一點(diǎn)點(diǎn)代碼不同,那么就可以通過(guò)宏來(lái)避免手寫(xiě)這些重復(fù)代碼。上面這個(gè)宏把不同的代碼段func1(rootpath),func2(temppath)作為參數(shù)傳進(jìn)來(lái),從而復(fù)用這個(gè)代碼段。

我們可以通過(guò)一個(gè)泛型函數(shù)來(lái)替換這個(gè)宏:

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);

}

}

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

接口易用和靈活性

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

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ù)類(lèi)型:

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做了類(lèi)型擦除,typename functiontraits::argstuple幫我們還原了類(lèi)型。

再來(lái)看另外一個(gè)例子,cinatra(https://github.com/qicosmos/cinatra)注冊(cè)路由函數(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{});

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

過(guò)濾參數(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)...));

}

};

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

總結(jié)

C++新標(biāo)準(zhǔn)給元編程帶來(lái)了巨大的改變,不僅僅讓元編程變得簡(jiǎn)單好寫(xiě)了,還讓它變得更加強(qiáng)大了,幫助我們優(yōu)雅地解決了很多實(shí)際的問(wèn)題。文中列舉到的元編程應(yīng)用僅僅是冰山一角,還有很多其他方面的應(yīng)用。

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

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


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

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多