前言

感兴趣的推荐看原文吧,我这里只是学习记录,价值一般,就是做个记录

https://www.researchgate.net/publication/323994820_Practical_C_Metaprogramming

我们写模板元编程的想法已经有很长时间了,因为我们想要演示它变得多么容易。我们也想证明它的有用性和效率。
我们的意思是它不仅是一个有效的解,有时还是最好的解。
最后但并非最不重要的是,即使您不是每天都使用元编程,理解它的概念将使您成为更好的程序员:您将学会以不同的方式看待问题,并提高您对该语言的掌握和理解。

真正掌握c++元编程是困难的,需要花费大量的时间。您需要了解编译器是如何工作的,以克服它们的缺陷和限制。当你遇到错误时,你收到的反馈通常是晦涩难懂的。
这是坏消息

好消息是你不需要掌握c++元编程,因为你站在巨人的肩膀上。
在本报告中,我们将逐步向您展示该技术及其实际应用程序,并为您提供一个工具列表,您可以使用这些工具来直接使用它。
然后,根据你的品味和愿望,你可以决定你想要去的兔子洞有多深。、

理解元编程

元编程是一种技术,如果使用得当,可以大大提高你的前置能力。如果使用不当,可能会导致不可维护的代码并大大增加开发时间。

基于一种先入为主的概念或教条而忽略元编程是适得其反的。然而,正确理解该技术是否适合您的需要,对于卓有成效和有回报的使用是至关重要的。我们喜欢使用的一个类比是,您应该将元程序看作一个机器人,您可以编写它来为您完成一项工作。当你给机器人编好程序后,它会很乐意为你完成一千次的任务而不会出错。此外,机器人比你更快,更精确。

但是,如果您做错了什么,可能不会立刻发现问题出在哪里。你对机器人的编程有问题吗?是机器人里的虫子吗?或者你的程序是正确的,但是结果却出乎意料?

这使得元编程变得更加困难:反馈不是即时的,而且因为添加了一个中间变量,就向等式中添加了更多的变量。

这也是为什么在使用此技术之前,您必须确保您知道如何为机器人编程。

介绍

这份报告的目的就是要证明这种理解
c++元编程将使你成为一个更好的c++程序员,以及一个更好的软件工程师。

void f(int*p, size_t l)
{for(size_t i = 0;i < l;++i){p[i] = i;}
}int my_array[5]
f(my_array,5);比较:int my_array[5];
std::iota(my_array,my_array + 5, 0);

生成的汇编代码即使不完全相同,也可能是等效的,但在后一种情况下,由于您学习了STL函数,因此在效率和信息密度方面都获得了提高。一个项目负责人谁不知道的iota功能,只需要看看它

要成为一个优秀的软件工程师,不仅要看他工具箱的大小,更重要的是要看他在正确的时间选择正确的工具的能力

另一方面,c++元编程背后的概念是非常一致和逻辑的:它是函数式编程!
这就是为什么它表面上看起来晦涩难懂,但当你学习了基本概念之后,它就变得有意义了

什么是元编程?

根据定义,元编程是一种程序设计,它的输入和输出本身就是程序。换句话说,它是编写代码,其工作就是编写代码本身。它可以被看作是抽象的最终层次,因为代码片段实际上被看作是数据并被这样处理

这听起来可能很深奥,但实际上是一个众所周知的做法。如果您曾经编写过一个Bash脚本从一个模板文件生成C文件,那么您已经完成了元编程。如果您曾经编写过C宏,那么您已经完成了元编程。在另一个领域,您可以讨论从UML模式生成Java类是否实际上只是元编程的另一种形式。

在某种程度上,您可能在职业生涯的各个阶段都做过元编程,而自己却不知道。

元编程的早期历史

在计算机科学的历史中,各种语言都在进化,以支持不同形式的元编程。最古老的语言之一是LISP语言家族,其中程序本身被认为是一段数据,著名的LISP宏可以用来从内部扩展语言。其他的lan‐guages依赖于深度反射能力来处理编译时或运行时的此类任务。

x 宏

// in components.h
PROCESS(float, x )
PROCESS(float, y )
PROCESS(float, z )
PROCESS(float, weight )
// in particle.c
typedef struct
{
#define PROCESS(type, member) type member;
#include "components.h"
#undef PROCESS
} particle_t;
void save(particle_t const* p, unsigned char* data)
{
#define PROCESS(type, member) \
memmove(data, &(p->member), sizeof(p->member)); \
data += sizeof(p->member); \
/**/
#include "components.h"
#undef PROCESS
}

进入c++ 模板

然后是c++及其泛型类型和通过模板机制实现的函数。模板最初是一个非常简单的特性,允许代码通过类型和常量参数化,这样就可以从给定代码段的现有变体集合中产生更多的泛型代码。人们很快发现,通过支持部分专门化,递归或条件语句的编译时等价语句是可行的,不久,Erwin Unruh提出了一个非常有趣的程序2,建立每个质数的列表,在1和任意极限之间。很平凡,不是吗?不过,这个枚举是通过编译时的警告来完成的。

让我们花点时间来思考一下这个发现的范围。这意味着我们可以把模板变成一种非常粗糙且在语法上不实用的函数式语言,后来Todd Veldhuizen甚至将其完成。如果您的计算机科学课程需要更新,这基本上意味着,给予必要的努力,任何函数可计算图灵机
(即。通过使用c++模板,可以将其转换为编译时的等效程序。c++模板元编程的时代来临了。

c++模板元编程是一种基于元编程技术的使用
(以及滥用)c++模板属性来执行编译时的任意命令。即使模板是图灵完成的,我们也只需要计算能力的一小部分。c++模板元编程应用的经典程序包括:

复杂的常数计算
•程序类型构建
•代码片段的生成和复制

这些应用程序通常由一些库进行备份,比如boost、MPL、fusion,以及一组模式,包括tag dispatching,、递归继承和SFINAE 。所有这些组件都在c++ 03的生态系统中蓬勃发展,并被大量其他库和应用程序所使用。

这些组件的主要目标是提供具有STL外观的编译时构造

由于某些原因,即使有了这些熟悉的接口,元程序定位工具仍被专家继续使用,而且经常被忽视,被认为过于复杂。元程序的编译时间也经常被批评为阻碍了一个不合理的、基于运行时的开发过程。

你可能听说过的大多数关于模板元推进的批评都源于这个限制,而这个限制不再适用,我们将在本报告的其余部分看到。

如何开始元编程

当你第一次尝试元编程时,我们的建议是在算法和类型操作方面进行实验,正如我们在第2章和第3章所展示的,在实际项目中,从最简单的事情开始:静态断言。

在开始时,编写元程序来进行编译时检查是最安全、最简单的事情。当你错了,你会得到一个编译时错误,或者一个检查会马上通过,但是这不会以任何方式影响你程序的可靠性和正确性。

这也将使您的头脑为c++中出现概念的那一天做好准备。

检查整数类型

有些程序或库会混淆变量的基础整数类型。如果你的假设是错误的,有一个编译错误是一个很好的方法来防止难以跟踪的错误:

static_assert(std::is_same<obfuscated_int,
std::uint32_t>::value,
"invalid integer detected!");

您可能不关心整数的精确类型—可能只关心大小。这张支票很容易写:

static_assert(sizeof(obfuscated_int) == 4,
"invalid integer size detected!");

检查内存模型

一个整数是指针的大小吗?你是在32位上编译还是
64位平台?你可以有一个编译时检查这个

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

在这种情况下,如果目标平台不是64位的,程序将无法编译。这是检测无效编译器/平台使用情况的好方法

但是,我们可以做得更好,在不使用宏的情况下构建基于平台的价值。为什么不使用宏呢?metaprogram 可以比宏更高级,而且输出的误差通常更精确(即。,您将得到错误所在的行,而对于预处理器宏,情况通常不是这样)。

让我们假设您的程序有一个读缓冲区。如果在a上编译,则可能希望读取缓冲区的值不同
32位平台或64位平台,因为在32位平台上,可用的用户空间少于3 GB。

下面的程序将在32位平台上定义100mb的缓冲区值,在64位平台上定义1gb的缓冲区值:

static const std::uint64_t default_buffer_size =
std::conditional<sizeof(void *) == 8,
std::integral_constant<std::uint64_t, 100 * 1024 * 1024>,
std::integral_constant<std::uint64_t, 1024 * 1024 * 1024>
>::type::value;

下面是宏中的等效内容:

#ifdef IS_MY_PLATFORM_64
static const std::uint64_t default_buffer_size
= 100 * 1024 * 1024;
#else
static const std::uint64_t default_buffer_size
= 1024 * 1024 * 1024;
#endif

如果您在宏值中输入错误,如果您忘记了标题,或者如果您编译的外部平台没有正确定义该值,那么宏将静默地设置错误的值

而且,通常很难想出好的宏来检测正确的平台(尽管Boost是这样的)。boost.Predef现在大大降低了任务的复杂性)。

总结

随着c++ 11和后来的c++ 14的出现,情况发生了变化,其中新的语言特性,如可变的lambdas、constexpr函数,以及更多的使元程序的设计更容易。这个报告将回顾这些更改,并向您展示现在如何构建元编程工具箱、理解它并以更高的效率和更优雅的方式使用它。
所以,让我们一头钻进去——我们将从一个简短的故事开始,告诉你你能做什么。

C++ Metaprogramming in Practice

示例代码

典型的代码维护分配

有一个示例代码,是老接口,没法更改

// we assume alpha and beta to be parameters to the mathematical
// model underlying the weather simulation algorithms--any
// resemblance to real algorithms is purely coincidental
void adjust_values(double * alpha1,
double * beta1,
double * alpha2,
double * beta2);

假设我们有一个对外接口,是如此定义的:

class reading
{
/* stuff */
public:
double alpha_value(location l, time t) const;
double beta_value(location l, time t) const;
/* other stuff */
};

我们的工作就是,提供一个接口,作为这两个接口中间的转换器,适配器

创建一个简单的接口

一个示例的接口,用来做中间的适配器:

std::tuple<double, double, double, double> get_adjusted_values(
const reading & r,
location l, time t1, time t2)
{
double alpha1 = r.alpha_value(l, t1);
double beta1 = r.beta_value(l, t1);
double alpha2 = r.alpha_value(l, t2);
double beta2 = r.beta_value(l, t2);
adjust_values(&alpha1, &beta1, &alpha2, &beta2);
return std::make_tuple(alpha1, beta1, alpha2, beta2);
}

The std::tuple<> pattern

您可以看到,我们使用一个元组来“返回一堆其他不相关的东西”。这是现代c++中常见的模式,稍后您将看到为什么使用元组在元程序设置方面有一些优势。

这个代码很直接,且能符合我们的需求了,但潜在的问题是:

1. 它是容易出错的,因为我们正在工作的库的接口。测试可以捕获其中一些错误,但不能捕获所有这些错误--------这个暂时还没看懂内涵是啥,先记着吧

2. 代码非常重复;就几个功能而言,它是可以简单的通过这样的方式做手动的适配的,但如果有100 个类似的函数呢?

3. 如何维护代码?如果任何功能发生变化,维护成本将呈指数级增长。

4. 如果函数的名称改变了怎么办?如果对象改变了呢?如果方法改变了怎么办?

上面的四种改动,可能是我们开发过程中,常见的问题,这篇文章的主要目的就是,通过对于模板编程的介绍,来写出通用的,自动适配的模板代码,可以完美的转发上述两个接口的代码。

两个可能的步骤,

template <typename Reading>
std::tuple<double, double, double, double> get_adjusted_values(
const Reading & r,
location l, time t1, time t2)
{
double alpha1 = r.alpha_value(l, t1);
double beta1 = r.beta_value(l, t1);
double alpha2 = r.alpha_value(l, t2);
double beta2 = r.beta_value(l, t2);
adjust_values(&alpha1, &beta1, &alpha2, &beta2);
return std::make_tuple(alpha1, beta1, alpha2, beta2);
}

将函数直接模板化:

template <typename AlphaValue, typename BetaValue>
std::tuple<double, double, double, double> get_adjusted_values(
AlphaValue alpha_value, BetaValue beta_value,
location l, time t1, time t2)
{
double alpha1 = alpha_value(l, t1);
double beta1 = beta_value(l, t1);
double alpha2 = alpha_value(l, t2);
double beta2 = beta_value(l, t2);
adjust_values(&alpha1, &beta1, &alpha2, &beta2);
return std::make_tuple(alpha1, beta1, alpha2, beta2);
}

调用方法为:

reading r;
// some code
auto res = get_adjusted_values(
[&r](double l, double t){ return r.alpha_value(l, t); },
[&r](double l, double t){ return r.beta_value(l, t); },
/* values */);

使得values 和 指针一起工作

template <typename ValueFunction, typename PointerFunction>
double magic_wand(ValueFunction vf,
PointerFunction pf,
double param)
{
double v = vf(param);pf(&v);
return v;
}

如果仔细观察我们上面的中间层函数的话,某种程度上,就是将一个值类型的函数调用,传递给一个指针类型的函数调用,而且值的类型是一样的。

magic_wand 是简单的,将一个函数调用,转换到了一个指针调用的过程,但是,我们的目表示将其模板化,自动适应

需要两种能力:

1. 类型操作

2. 能够处理任意数量的参数并对其进行迭代

换句话说,我们希望编写修改类型而不是值的c++。模板元编程是编译时类型操作的完美工具。

类型操作101

自从c++ 11以来,标准库已经提供了大量的函数来操作类型。例如,如果你想把double 编程double*,你可以这样做:

#include <type_traits>
// double_ptr_type will be double *
using double_ptr_type = std::add_pointer<double>::type;
#include <type_traits>
// double_type will be double
using double_type = std::remove_pointer<double *>::type;
// note that removing a pointer from a nonpointer type is safe
// the type of double_too_type is double
using double_too_type = std::remove_pointer<double>::type;

这些类型操作(添加和删除*、引用和const)是基本的构建块,在处理类型约束时非常有用。例如,当您实际需要一个值时,您的template参数可能必须是一个const引用。有了这些工具,您可以确保您的类型正是您所需要的类型。

泛型函数转换器

gamic 的通用版本可以采用任意的函数,将结果连接到一个结构中,将这些结果传递到我们的遗留C函数(将应用天气模型),并返回其输出。类似:

MagicListOfValues generic_magic_want(OldCFunction old_f,
ListOfFunctions functions,
ListOfParameters params)
{
MagicListOfValues values;
/* wait, something is wrong, we can't do this
for(auto f : functions)
{
values.push_back(f(params));
}
*/
olf_f(get_pointers(values));
return values;
}

唯一的问题是我们不能那样做

为什么?第一个问题是,我们需要一个值集合,但是这些值可能具有异构类型。当然,在我们的检查中,我们返回double,我们可以使用vector

另一个问题是性能问题——既然在编译时就知道了集合的大小,为什么还要在运行时调整集合的大小?既然可以使用堆栈,为什么还要使用堆呢?

这就是我们喜欢tuples 的原因。元组允许存储异构类型,它们的大小在编译时是固定的,它们可以避免大量的动态内存分配。

不过,这也提出了一些问题。我们如何基于我们的遗留C函数的参数来构建这些tuple?我们如何枚举tuple?我们如何处理函数列表?我们如何传递参数?

通过模板提取c 函数的参数:

该过程的第一步是,对于给定的函数F,构建一个匹配参数的元组。
我们将使用部分模板匹配的模式匹配算法来实现

template <typename F>
struct make_tuple_of_params;
template <typename Ret, typename... Args>
struct make_tuple_of_params<Ret (Args...)>
{
using type = std::tuple<Args...>;
};
// convenience function
template <typename F>
using make_tuple_of_params_t =
typename make_tuple_of_params<F>::type;

神奇的……操作符

在c++ 11中,…的语义操作符已经被改变和极大地扩展,使我们能够对编译器说,“我期望一个任意长度类型的列表。”它和原来的C省略号操作符没有关系了。该操作符是现代c++ template元编程的支柱。

因此,使用我们的新函数,我们可以执行以下操作

template <typename F>
void magic_wand(F f)
{
// if F is in the form void(double *, double *)
// make_tuple_of_params is std::tuple<double *, double *>
make_tuple_of_params_t<F> params;
// ...
}

我们现在有一个元组参数,我们可以加载的结果
c++函数并传递给C函数。唯一的问题是C函数的形式是void(double *, double *, double)
*, double *),我们致力于value。

因此,我们将相应地修改我们的make_tuple_of_params函数-------注意这里通过remove_ptr_t 移除了指针属性了

template <typename Ret, typename... Args>
struct make_tuple_of_derefed_params<Ret (Args...)>
{
using type = std::tuple<std::remove_ptr_t<Args>...>;
};

... 操作符

当我们写Args的时候…,我们以某种方式在我们的std::元组中扩展了参数列表。这是操作符最直接的用法之一。
一般的行为。操作符为参数包中的每个类型复制其左侧的代码片段。在本例中,将携带remove_ptr_t。

例如,如果你的参数是:

int i, double d, std::string s

扩张与std::元组< Args…>将得到:

std::tuple<int, double, std::string>

和扩展std::(tuple< std::(add_(pointer_t <>…>将得到:

std::tuple<int *, double *, std::string *>

现在函数的工作方式如下

template <typename F>
void magic_wand(F f)
{
// if F is in the form void(double *, double *)
// make_tuple_of_params is std::tuple<double, double>
make_tuple_of_derefed_params<F> params;
// ...
}

获取函数和参数的列表

现在我们可以提取C函数参数的内容,我们需要将它们装配到对象中,这样我们就可以轻松地在c++中操作它们。

实际上,你可能会忍不住这样写:

template <typename Functions, typename Params>
void magic_wand(/* stuff */, Functions... f, Params... p)
{
// stuff
}

毕竟,您有一个函数列表和一个参数列表,并且您希望同时拥有它们。唯一的问题是,编译器如何知道第一个列表何时结束,第二个列表何时开始?
元组再次发挥了作用:

template <typename... Functions, typename... Params>
void magic_wand(/* stuff */,
const std::tuple<Functions...> & f,
const std::tuple<Params...> & p1,
const std::tuple<Params...> & p2)
{
// stuff
}

这使编译器能够知道需要多个任意且不相关长度的元组。当然,如果您希望使用两组以上的参数,那么您可以创建元组的元组,但是没有必要使我们的示例比需要的更复杂。

调用方法变为:

magic_wand(/* stuff */,
// our C++ functions
std::make_tuple(
[&r](double l, double t){ return r.alpha_value(l, t); },
[&r](double l, double t){ return r.beta_value(l, t); }),
// first set of params
std::make_tuple(l, t1),
// second set of params
std::make_tuple(l, t2));

这意味着在magic_wand函数内部,我们将拥有元组,其中包含我们需要调用的函数以及需要传递给它们的参数。

填充C函数的值

我们取得了进展,但还没有到达终点。一方面,我们有元组值要传递给C函数;另一方面,我们有一个函数和参数的元组。
现在我们想用结果填充值的元组,这意味着调用元组中的每个函数并传递正确的参数:

template <typename LegacyFunction,
typename... Functions,
typename... Params>
auto magic_wand(
LegacyFunction legacy,
const std::tuple<Functions...> & functions,
const std::tuple<Params...> & params1,
const std::tuple<Params...> & params2)
{
make_tuple_of_derefed_params_t<LegacyFunction> params = {
/* we would like to do
for(auto f : functions)
{
f(params1);
}
for(auto f : functions){
f(params2);
}*/
};
// rest of the code
}

在模板元编程中,没有迭代构造。不能使用for对类型列表进行迭代。但是,您可以使用递归对元组的每个成员应用callable。这种方法从2003年就开始使用,效果很好,但它的缺点是生成大量的中间类型,因此增加了编译时间

只要可能,你应该使用…操作者对列表中的每个成员实施呼叫指令。这样会更快,不会生成所有不需要的中间类型,而且代码通常更简洁。
我们怎样才能利用...opearator ?在这里,我们将创建一个与元组大小匹配的序列,以便对每个成员使用专用权限:

template <typename F, typename Params, std::size_t... I>
auto dispatch_params(F f,
Params & params,
std::index_sequence<I...>)
{
return f(std::get<I>(params)...);
}

这里发生的事情如下:

template <typename F, typename Params, std::size_t... I>
auto dispatch_params(F f,
Params & params,
std::index_sequence<I...>)
{
// not real C++ code
return f(std::get<0>(params),
std::get<1>(params),
std::get<2>(params),...,
std::get<N>(params)); // where N is the last index
}

其优点是所有的工作都由编译器完成,而且比递归(或宏)快得多。
诀窍是创建一个索引序列,它的唯一目的是为我们提供一个要应用…操作者——大小合适。具体做法如下:

static const std::size_t params_count = sizeof...(Params);
std::make_index_sequence<params_count>();

列表的编译时大小
在编译时,当需要知道列表中有多少元素时,可以使用sizeof…()。
注意,在本例中,我们将其存储到静态const变量中,但实际上使用std::integral_constant会更好。你将在第3章中了解更多。

我们离解决问题已经很近了;也就是说,自动化facade代码的生成以使模拟库适应我们的分布式系统。
但问题还没有完全解决,因为我们需要以某种方式解决
对函数进行“迭代”。我们将修改调度函数,使其接受函数元组作为参数并接受索引,如下所示:

template <std::size_t FunctionIndex,
typename FunctionsTuple,
typename Params,
std::size_t... I>
auto dispatch_params(FunctionsTuple & functions,
Params & params,
std::index_sequence<I...>)
{
return (std::get<FunctionIndex>(functions))
(std::get<I>(params)...);
}

我们将使用相同的index_sequence技巧在元组的每个函数上调用dis patch_params:

template <typename FunctionsTuple,
std::size_t... I,typename Params,
typename ParamsSeq>
auto dispatch_functions(FunctionsTuple & functions,
std::index_sequence<I...>,
Params & params,
ParamsSeq params_seq)
{
return std::make_tuple(dispatch_params<I>(functions,
params,
params_seq)...);
}

前面的代码使我们能够将连续调用的每个元组元素的结果聚合为单个元组。
因此,最后的守则是:

template <typename LegacyFunction,
typename... Functions,
typename... Params>
auto magic_wand(
LegacyFunction legacy,
const std::tuple<Functions...> & functions,
const std::tuple<Params...> & params1,
const std::tuple<Params...> & params2)
{
static const std::size_t functions_count =
sizeof...(Functions);
static const std::size_t params_count = sizeof...(Params);
make_tuple_of_derefed_params_t<LegacyFunction> params =
std::tuple_cat(
dispatch_functions(functions,
std::make_index_sequence<functions_count>(),
params1,
std::make_index_sequence<params_count>()),
dispatch_functions(functions,
std::make_index_sequenc<functions_count>(),
params2,
std::make_index_sequence<params_count>()));
/* rest of the code */
}

调用遗留的C函数

现在我们已经将c++方法调用的结果加载到一个元组中。
现在我们想把这些值的指针传递给C函数。

到目前为止,我们已经了解了所有的概念,我们知道如何解决这个问题。
我们需要确定结果元组的大小,这可以通过调用std::tuple_size函数(这是编译时)来完成,并按照我们之前所做的那样传递所有参数:

template <typename F, typename Tuple, std::size_t... I>
void dispatch_to_c(F f, Tuple & t, std::index_sequence<I...>)
{
f(&std::get<I>(t)...);
}

唯一的问题是,我们将把地址获取到tuple成员,因为C函数需要一个指向要更新的值的指针。它是安全的,因为std::get<>返回对元组值的引用。
完成后的功能如下:

template <typename LegacyFunction,
typename... Functions,
typename... Params>
auto magic_wand(
LegacyFunction legacy,
const std::tuple<Functions...> & functions,
const std::tuple<Params...> & params1,
const std::tuple<Params...> & params2)
{
static const std::size_t functions_count =
sizeof...(Functions);
static const std::size_t params_count = sizeof...(Params);
using tuple_type =
make_tuple_of_derefed_params_t<LegacyFunction>;
tuple_type t =
std::tuple_cat(
dispatch_functions(functions,
std::make_index_sequence<functions_count>(),
params1,
std::make_index_sequence<params_count>()),
dispatch_functions(functions,
std::make_index_sequenc<functions_count>(),
params2,
std::make_index_sequence<params_count>()));
static const std::size_t t_count =
std::tuple_size<tuple_type>::value;
dispatch_to_c(legacy,
params,std::make_index_sequence<t_count>());
return params;
}

简化代码

如果我们不需要指定元组连接结果的类型不是很好吗?毕竟,编译器知道它将是哪种类型的元组。但在这种情况下,我们如何计算结果元组的大小呢?
我们可以使用decltype指令来访问变量的类型:

auto val = /* something */;
decltype(val) // get type of val

这简化了代码并消除了对make_tuples_of_params_t仿函数的需要,如下所示:

template <typename LegacyFunction,
typename... Functions,
typename... Params>
auto magic_wand(LegacyFunction legacy,
const std::tuple<Functions...> & functions,
const std::tuple<Params...> & params1,
const std::tuple<Params...> & params2)
{
static const std::size_t functions_count =
sizeof...(Functions);
static const std::size_t params_count =
sizeof...(Params);
auto params = std::tuple_cat(
dispatch_functions(functions,
std::make_index_sequence<functions_count>(),
params1,
std::make_index_sequence<params_count>()),
dispatch_functions(functions,
std::make_index_sequence<functions_count>(),
params2,
std::make_index_sequence<params_count>()));
static constexpr auto t_count =
std::tuple_size<decltype(params)>::value;
dispatch_to_c(legacy,
params,
std::make_index_sequence<t_count>());return params;
}

您还可以通过使用右值引用和确保使用完美的转发语义来提高代码的效率。

最终调用方法:

template <typename Reading>
std::tuple<double, double, double, double>
get_adjusted_values(Reading & r,
location l,
time t1,
time t2)
{
return magic_wand(adjust_values,
std::make_tuple(
[&r](double l, double t)
{
return r.alpha_value(l, t);
},
[&r](double l, double t)
{
return r.beta_value(l, t);
}),
std::make_tuple(l, t1),
std::make_tuple(l, t2));
}

c++元编程和应用程序设计

编译时和运行时范例

c++运行时代码是基于这样一个事实,即用户可以定义对某些数据类型所代表的值进行操作的函数
(本机或用户定义)以产生新值或副作用。
然后,c++程序就是上述函数调用的编排,以在运行时推进应用程序的目标

如果由运行时行为定义的函数、值和类型的概念非常简单,那么当谈到它们在编译时的等价时,事情就变得模糊了。

编译时的值

编译时计算需要对在编译时定义为有效的值进行操作。下面是编译时值的概念:

1. 类型,它们是在编译时定义的实体
2. 整数常量,使用关键字constexpr定义或作为模板参数传递给模板类。

不幸的是,类型和integral constants 在c++中是两个完全不同的东西。此外,目前还没有办法进入
将模板参数指定为类型或整数常数。能够使用两种值
(类型和积分常数),我们需要一种方法使这两种值同质。

这样做的标准方法来自Boost。MPL,是将值包装在一个类型中。我们可以通过使用std::integral_constant来做到这一点,我们可以大致实现如下:

template<typename T, T Value>
struct integral_constant
{
using type = T;
static constexpr T value = Value;
};

这个结构是一个包含值的简单框。这个盒子的类型取决于值和值的类型,使得它不太大。2稍后我们可以通过::type内部类型定义检索值类型,或者通过::value常量检索数值

因为整型常量可以很容易地转换为类型,所以我们可以认为编译时值唯一需要的样式就是类型。
这是一个非常强大的命题,因此我们将把它定义为元编程中的一个基本公理。

编译时的函数

我们可以将运行时函数看作是某些类型的值和给定类型(可能为空)的结果之间的映射。在一个完美的世界里,这些功能将以与其数学上的、纯粹的等高物相同的方式运行,但在某些情况下,我们可能需要考虑副作用(如I/O或内存访问)。

函数在编译时处于不会发生副作用的环境中。从本质上讲,它们是纯函数,生活在嵌入c++的小函数语言中。根据第一个公理,元编程中惟一有趣的值是类型。因此,编译时函数是将类型映射到其他类型的组件

这个表述看起来很熟悉。它基本上与运行时函数的定义相同,只是将“值”替换为“类型”。现在的问题是,当c++语法不允许我们在某个地方写return float时,我们如何指定这样的组件?

我们再次利用了Boost的开创性工作。MPL通过重用其元功能的概念。引用文档说明,元功能是“表示在编译时可调用的函数的类或类模板”。这样的类或类模板遵循一个简单的协议。元功能的输入作为模板参数传递,返回的类型作为内部类型定义提供。

一个简单的元功能可以写成如下:

template<class T>
struct as_constref
{
using type = T const&;
};

顾名思义,该元功能将类型转换为对所述类型的常数值的引用。调用一个元功能只是访问它内部的::类型,如下所示:

using cref = as_constref<float>::type;

这个原则已经从MPL泄露到了标准中。type_traits标题提供了大量这样的元参数,支持分析、创建或修改基于它们的属性的类型。

专业技巧
类型操作的大多数基本需求都由type_traits提供。我们强烈建议正在接受培训的项目经理对这一标准部件非常熟悉。

类型容器

c++运行时开发依赖容器的概念来表示复杂的数据操作。这样的容器可以定义为持有可变数量的值并遵循给定存储模式(连续单元格、链接单元格等)的数据结构。
然后我们可以对容器应用操作和算法来修改、查询、删除或插入值。STL提供了预先制作的容器,如list、set和vector。

如何才能在编译时得到类似的概念呢?显然,我们不能要求分配内存来存储我们的值。
此外,我们的“价值观”实际上是一种类型,这样的存储的意义很小。我们需要做的逻辑飞跃是理解容器也是值,碰巧包含零个或多个其他值;如果我们应用系统的“值是类型”格言,这意味着编译时容器必须是包含零个或多个其他类型的类型。但是一个类型如何包含另一个类型呢?
这个问题有多种解决方案。

第一个想法可能是编译时容器是一个内部使用语句数量可变的类型,如前面的例子:

struct list_of_ints
{
static constexpr std::size_t size = 4;
using element0 = char;
using element1 = short;
using element2 = int;
using element3 = long;
};

不过,这种解决方案存在一些问题。首先,不需要构造新类型就无法添加或删除类型。
然后,访问给定类型是复杂的,因为它要求我们能够将一个整型常量映射到类型名。
另一个想法是使用可变模板来存储类型作为可变类型的参数包。我们的list_of_ints就变成了:

template<typename... Values> struct meta_list {};
using list_of_ints = meta_list<chr,short,int,long>;

这个解决方案没有上述的缺点。这个meta_list上的参数可以通过使用参数包的内在属性来执行,因为不需要名称映射。插入和删除元素是直观的;我们只需要处理参数包的内容。
可变元模板的这些属性定义了元编程的第二个公理:任何可变元模板结构实际上都是一个编译时容器

公理2号
任何接受数量可变的类型参数的模板类都可以视为类型容器。

编译时的操作

现在我们已经将类型容器定义为至少具有一个模板参数包参数的任意模板类。这些容器上的操作是通过使用模板参数包的内在c++支持来定义的。

我们可以做到以下几点:

•检索有关包的信息。
•扩展或收缩包装内容。
•重新包装参数包。
•对参数包的内容应用操作。

使用Pack-Intrinsic信息

让我们试着做一个简单的元功能,通过写一个方法来访问一个容器的大小来操作一个类型控制器:

template<class List> struct size;
template<template<class...> class List, class... Elements>
struct size<List<Elements...>>
: std::integral_constant<std::size_t, sizeof...(Elements)>
{};

让我们看一下这段代码。首先,我们声明一个大小结构,它只接受一个模板参数。此时,该参数的性质是未知的;因此,我们不能给大小一个适当的定义。然后,我们部分专门化了表单List<Elements…>的所有类型的size。语法有点吓人,所以让我们分解它。该专业企业的模板参数如下:

List
等待模板参数包作为参数的模板模板参数
Elements
模板参数包

通过这两个参数,我们专门化了size<Elements…>。我们可以把它写成元素。,这将触发包中每个类型的扩展,这正是List在其自己的参数中所需要的。从现在开始,这种描述类型容器的可变结构以便指定算法的技术将是我们的主要工具。

看看我们如何使用这个编译时算法,以及编译器如何解释这个调用。考虑以下情况,我们试图评估std::tuple<int,float,void>的大小:

constexpr auto s = size<std::tuple<int,float,void>>::value;

根据std::tuple的定义,这个调用将匹配大小<List<Elements…> >专业化。非常简单的是,列表将被std::tuple代替,元素将被参数包{int, float, void}代替。当它存在的时候,它的大小…操作符将被调用并返回3。然后size将公开继承std::integral_constant<std::size_t,3>并转发其内部值常量。我们可以使用任何类型的可变结构而不是元组,而且过程也会是类似的。

添加和删除包元素

下一个自然的步骤是尝试修改类型容器中的元素。我们可以通过使用参数包的结构描述来做到这一点。作为示例,让我们尝试编写push_back<List,元素>,它将在给定类型容器的末尾插入一个新元素。
实现以现在熟悉的方式开始:

template<class List, class New> struct push_back;

至于大小,我们声明了带有所需类型接口的push_back结构,但没有定义。下一步是专门化这个类型,以便它能够匹配类型容器并继续:

template<template<class...> class List,
class... Elements, class New>
struct push_back<List<Elements...>, New>
{
using type = List<Elements...,New>;
};

由于编译时元编程没有值的概念,我们将元素添加到现有类型容器的唯一方法是重新构建一个新的类型容器。算法非常简单:扩展容器内的存在参数包,并在最后添加更多的元素。根据List和…的定义,这将在末尾插入新元素时构建一个新的有效类型

删除类型容器中的现有元素遵循类似的推理,但依赖于参数包的递归结构。不要害怕!正如我们之前所说的,递归在模板元推进中通常是不明智的,但在这里我们将只利用参数包的结构,我们不会做任何循环。让我们从一个假设的remove_front算法的基本代码开始:

template<class List> struct remove_front;
template<template<class...> class List, class... Elements>
struct remove_front<List<Elements...>>
{
using type = List</* what goes here??? */>;
};

正如你所看到的,我们还没有偏离到目前为止我们所看到的。现在,让我们来思考一下我们如何去掉任意参数包的第一种类型,以便我们能够完成我们的实现。让我们来列举一下:

List<Elements...>
包含至少一个元素(头部)和其他类型的潜在空包(尾部)。在这种情况下,我们可以把它写成
List <头,尾……
List<Elements...>
这是空的。在这种情况下,它可以被写为List<>。

如果我们知道有一种头部类型存在,我们就可以移除它。如果列表为空,则任务已经完成。代码反映了这一过程:

template<class List> struct remove_front;
template<template<class...> class List
, class Head, class... Elements>
struct remove_front<List<Head,Elements...>>
{
using type = List<Elements...>;
};
template<template<class...> class List>
struct remove_front<List<>>
{
using type = List<>;
};

对参数包的递归特性的自省是我们的另一个工具。它有一些局限性,考虑到把一个包分成一个头和一个尾的类型更加复杂,但它帮助我们建立基本的块,我们可以在更复杂的环境中重用。

包装重新包装

到目前为止,我们主要处理的是访问和修改参数包。其他算法可能需要使用封闭类型容器。
作为示例,让我们编写一个将任意类型的容器转换为std::元组的元功能。我们怎么做呢?因为std::元组之间的差异…列表>和< T…>是封闭的模板类型,我们可以改变它,如下所示:

template<class List> struct as_tuple;
template<template<class..> class List, class... Elements>
struct as_tuple<List<Elements...>>
{
using type = std::tuple<Elements...>;
};

但是等一下:还有更多!通过将新的容器类型作为参数传递,可以将类型容器更改为元组或变体或任何其他类型。让我们将as_tuple泛化为rename:

struct rename;
template<template<class..> class Container
, template<class..> class List
, class... Elements
>
struct rename<Container, List<Elements...>>
{
using type = Container<Elements...>;
};

代码非常类似。我们使用模板模板参数可以被自然地传递的事实来提供其实际目标的重命名。一个示例调用可以如下所示:

using my_variant = rename<boost::variant
, std::tuple<int,short>
>;

容器的转换

这些工具—类型容器的重新包装、迭代和类型内省—将我们引向最终的和最有趣的元程序:容器转换。这些转换直接受到STL算法的启发,将有助于引入结构化元编程的概念

连接的容器

转换的第一个例子是将两个存在型容器串联起来。考虑任意两个列表L1<T1…>和
L2 < T2…>,我们希望获得一个新的列表,等于L1<T1…,T2…>。

我们从运行时经验中得到的第一个直觉可能是,当我们反复调用push_back时,要找到一种方法来“循环”类型。即使它是正确的实现,我们也需要用循环来对抗这种强迫性的思考。类型循环需要计算线性数量的中间类型,从而导致不可持续的编译时间。处理这个用例的正确方法是找到一种自然的方法来利用容器的可变特性。

实际上,我们可以将append看作一种重新包装,在这种包装中,我们将比之前包含的更多类型放入到一个给定的可变结构中。示例实现可以如下所示:

template<typename L1, typename L2> struct append;
tempate< template<class...> class L1, typename... T1
, template<class...> class L2, typename... T2
>
struct append< L1<T1...>, L2<T2...> >
{
using type = L1<T1...,T2...>;
};

在通常的声明之后,我们定义append为等待两个不同的可变结构,填充两个不同的参数包。注意,与非变量模板的常规专门化一样,我们可以使用多个参数包,只要它们在专门化中被正确地包装。我们现在可以访问所有必需的元素。结果被计算为第一变矢型瞬变,两个参数包展开。

专业技巧
处理编译时容器不需要循环。试着表达你的算法尽可能多的直接操纵参数包。

转向编译时转换

append算法相当简单。现在让我们跳转到一个更复杂的示例:编译时相当于std::transform。让我们首先说明这样一个超规划的接口可能是什么。在运行时环境中,std::transform对目标容器的每个值调用一个可调用对象,并将结果填充到另一个容器中。同样,这必须传递给一个元功能,这个元功能将在parameter包中迭代类型,应用任意的元功能,并生成一个新的参数包来返回。

即使“使用…迭代参数包的内容”是一个众所周知的练习,我们需要找到一种方法来传递任意的元功能到我们的编译时转换变量。运行时可调用对象是为所谓的函数调用操作符(通常表示为操作符())提供重载的对象。通常这些对象是常规函数,但也可以是匿名函数(又名lambda函数)或提供此类接口的成熟用户定义类。

generalizing metafunctions

在编译时环境中,我们可以通过让转换元程序等待一个模板模板参数来直接传递元功能。这是一个有效的解决方案,但是对于运行时函数,我们可能希望绑定现有元功能的任意参数,以最大化代码重用。

让我们介绍一下Boost。元功能类的MPL概念。元功能类是一个结构,它可能是模板,也可能不是模板,它包含一个名为apply的内部模板结构。
这个内部元功能将处理我们的新类型的实际计算。在某种程度上,这个应用相当于可调用对象的广义运算符()。例如,让我们将std::remove_ptr转换为一个元功能类

struct remove_ptr
{
template<typename T> struct apply
{
using type = typename std::remove_ptr<T>::type;
};
};

我们如何使用这个所谓的元功能类?这与元功能有点不同:

using no_ptr = remove_ptr::apply<int*>::type;

注意进入应用模板内部结构的要求。对其进行包装以使最终用户免受复杂性的影响是需要技巧的。
请注意,metafunction类不再是模板,而是依赖其内部应用程序来执行其命令。如果您是一个敏锐的读者,您将会看到我们可以将其一般化,将任何元功能转换为元功能类。让我们引入lambda元功能

template<template<class...> class MetaFunction>
struct lambda
{
struct type
{
template<typename Args...> struct apply
{
using type = typename MetaFunction<Args...>::type;
};
};
};

这个lambda结构实际上是一个元功能,因为它包含要检索的内部类型。这个类型结构使用包扩展来调整lambda的模板模板参数,以便正确使用。还要注意,像运行时lambda函数一样,这个内部类型实际上是匿名的。

Implementing transform

现在我们有了一个适当的协议来将元功能传递给编译时转换。让我们写一个一元转换,工作在类型容器:

template<typename List, typename F> struct transform;
template<template<class...> class List,
struct transform<List<Elems...>,F>
{
using call = typename F::template apply<T>::type;
using type = List< call<Elems>... >;
};

此代码与我们前面编写的代码相似,但稍微复杂一些。像往常一样,它首先使用参数包声明和定义transform作为对容器类型的作用。实际代码使用classic...方法对容器的元素执行迭代。我们还需要做的是在每个类型上调用元函数类F。我们这样做是因为……是否将解包并将其左侧的代码片段应用于包中的每个类型.为明确起见,我们使用一个中间template,该template使用一个语句来保持对单一类型的实际元功能类应用。
现在,作为一个例子,让我们在一个类型列表上调用std::remove_ptr:

using no_pointers = transform< meta_list<int*,float**, double>
, lambda<std::remove_ptr>
>::type;

注意,算法的抽象能力被调换到编译时世界。在这里,我们使用高级元功能在类型容器上应用众所周知的计算模式。
还要观察lambda构造如何帮助我们更容易地使用和重用现有的元功能。

元功能遵循与功能功能相似的规则:它们可以被组合、绑定或转化为各种相似但又不同的界面。元功能和元功能类之间的转换只是冰山一角。

元编程的高级用法

借助一点想象力和知识,您可以执行比使用模板元编程执行编译时检查高级得多的操作。本节的目的只是让您了解什么是可能的。

重新访问的命令模式

命令模式是一种行为设计模式,在该模式中,您将执行命令所需的所有信息封装到一个对象或结构中。这是一个很好的模式,在c++中经常使用运行时多态性编写。
撇开运行时多态的倾向不谈
“工厂中的工厂”反模式,虚函数表会导致不可忽略的性能成本,因为它们阻止编译器主动优化和内联代码。

“工厂的工厂”反模式
这种反模式可能发生在面向对象的programming中,当你花费更多的时间编写代码来管理抽象,而不是编写代码来解决问题。

从严格的软件设计角度来看,它还迫使您将对象联系在一起,因为它们将在某个时间点执行相同的功能。
如果泛型编程教会了我们什么的话,那就是您不需要在对象之间创建一个关系来让它们使用一个函数。

你所需要做的就是让对象共享共同的属性:

struct first_command
{
std::string operator()(int) { /* something */ }
};
struct second_command
{
std::string operator()(int) { /* something */ }
};

并具有接受命令的功能:

template <typename Command>
void execute_command(const Command & c, int param)
{
c(param);
}

对此你可能会反驳道:“我如何通过一个结构来传递这些命令,因为我只知道在运行时运行哪个com‐命令?”
有两种方法可以做到这一点:手动地使用不受限制的联合,或者使用Boost.Variant这样的变体。模板元编程起到了拯救作用,因为你可以安全地在类型列表中列出命令的类型,并从该列表中构建变体(或联合)。
这不仅会使代码更简洁、更高效,而且还会减少出错的可能性:在编译时,如果忘记实现f,就会出现错误

编译时序列化

编译时序列化是什么意思?当你想序列化一个对象时,有很多事情你在com‐pile time就已经知道了——记住,你在编译时做的所有事情不需要在运行时再做。
这意味着更快的序列化和更有效的内存使用。

递归地,这意味着如果序列化严格由整数组成的结构,则可以在编译时确切地知道需要多少内存,这意味着可以在编译时分配所需的中间缓冲区。

使用模板元编程,您可以在编译时分支用于序列化的正确代码。这意味着,对于每一个结构,你能够精确计算内存要求,你将避免动态内存分配,产生巨大的性能改善和减少内存使用。

辅助函数和库

你必须重新发明轮子,编写你自己的基本函数,就像我们在本章看到的那样吗?幸运的是,没有。从c++ 11开始,标准中已经包含了大量的帮助函数,我们强烈建议您在可能的情况下使用它们。

在元计划方面,该标准还没有完全体现出来;例如,它缺乏正式的“类型列表”类型、算法和更高级的元功能。

幸运的是,有一些库可以防止您重新发明轮子,并且可以用于所有主要的编译器。这将为您省去处理编译器特性的麻烦,并使您能够专注于编写元程序。

Boost提供了两个库来帮助您进行模板元编程

MPL,由Aleksey Gurtovoy和David Abrahams撰写

一个完整的c++ 03模板元编程工具箱,附带容器、算法和迭代器。除非您使用的是c++ 03编译器,否则我们建议您不要使用这个库。

Hana, Louis Dionne的作品
一个新的元编程范例,它大量使用lambdas。Hana对编译器的要求是出了名的苛刻

这份报告的作者也是Brigand, 一个c++ 14 元编程库,很好地填补了Boost之间的差距。MPL Boost.Hana。

我们强烈建议您使用现有的库,因为它们将帮助您构建代码,并让您了解元编程可以做什么。

Summary

在本章中,我们进入了c++中的类型领域,我们看到了它们可以像运行时值一样被操纵

我们定义了类型值的概念,并看到了这样的概念如何导致类型容器的定义;即包含其他类型的类型。我们看到了参数包的表达性如何导致设计元程序的非递归方式。我们定义的经典容器操作符的小而功能的子集展示了各种可用的技术,以一种系统的方式设计这种超符号。

我们希望我们达到了我们的目标,让您体验元计划,并证明它不仅仅是一些不应该在研究机构之外使用的神秘技术。无论您是想查看本文讨论的库,编写您自己的第一个met‐aprogram,还是想重温您最近编写的代码,我们只有一个希望:通过阅读本报告,您可以学到一些东西,使您成为一个更好的程序员。

practical c++ metaprogramming(翻译及学习)相关推荐

  1. Elastic Nodes Example 翻译及学习整理

    文章目录 Elastic Nodes Example 翻译及学习整理 题记: 简介: Node Class Definition Edge Class Definition GraphWidget C ...

  2. EfficientDet(EfficientNet+BiFPN)论文超详细解读(翻译+学习笔记+代码实现)

    前言 在之前我们介绍过EfficientNet(直通车:[轻量化网络系列(6)]EfficientNetV1论文超详细解读(翻译 +学习笔记+代码实现) [轻量化网络系列(7)]EfficientNe ...

  3. CUDA10.0官方文档的翻译与学习之编程接口

    目录 背景 用nvcc编译 编译工作流 二进制适配性 ptx适配性 应用适配性 C/C++适配性 64位适配性 cuda c运行时 初始化 设备内存 共享内存 页锁主机内存 可移植内存 写合并内存 映 ...

  4. 【轻量化网络系列(2)】MobileNetV2论文超详细解读(翻译 +学习笔记+代码实现)

    前言 上一篇我们介绍了MobileNetV1,主要是将普通Conv转换为dw和pw,但是在dw中训练出来可能会很多0,也就是depthwise部分得到卷积核会废掉,即卷积核参数大部分为0,因为权重数量 ...

  5. Deep Residual Learning for Image Recognition(ResNet)论文翻译及学习笔记

    [论文翻译]:Deep Residual Learning for Image Recognition [论文来源]:Deep Residual Learning for Image Recognit ...

  6. OMPL库教程翻译/OMPL学习

    本文翻译自OMPL教程,以备自己查阅.不准确之处欢迎在评论区提意见. 前言 一.简介 The Open Motion Planning Library(OMPL)是基于采样方法的开源运动规划库,其规划 ...

  7. 深度学习(12):SemanticKITTI论文翻译与学习

    SemanticKITTI论文是发表在CVPR 2019上的一篇在KITTI Vision Odometry Benchmark数据集上制作语义分割数据集SemanticKITTI的文章,为基于车载激 ...

  8. MATLAB中内置的BP神经网络函数 help newff翻译【学习笔记】

    MATLAB中内置的BP神经网络函数 help翻译 原文请参考:help newff newff 创建前馈反向传播网络. 在 R2010b NNET 7.0 中已过时. 最后在 R2010a NNET ...

  9. 集精准翻译与学习助手于一身 搜狗翻译APP实现重磅升级

    搜狗搜索正在通过翻译将中文世界与全世界紧密连接.凭借领先的人工智能技术,搜狗搜索于今年6月发布搜狗翻译APP,集文本.语音.对话.拍照翻译四大功能于一体,给用户带来多种场景下"秒翻秒懂&qu ...

最新文章

  1. JQuery事件绑定,bind与on区别
  2. python——变量的类型、不同类型变量的计算、变量的输入以及格式化输出
  3. python中赋值不正确的_python中关于赋值、浅拷贝与深拷贝的问题
  4. thinkpad bios联想logo_最强12吋ThinkPad,X201终极改造:8代酷睿+双内存+NVMe
  5. 算法和数据结构(四)
  6. html中的各种协议,html 中使用 wtai 协议
  7. 我们每天努力上班赚钱,财富离我们很远
  8. 1024程序员节再次引爆星城!千万程序员线上线下互动,共迎新程序员时代
  9. 1283 最简单的计算机
  10. 允许使用抽象类类型 isearchboxinfo 的对象_Java面向对象之final、abstract抽象、和变量生命周期...
  11. STM32电机库(ST-MC-Workbench)学习记录——电流采样参数设置
  12. clickhouse 入门介绍和预演
  13. 智能座舱Tier1“抢攻“ADAS,环视/泊车是第一突破口
  14. shopnc route.php,shopnc自动结算的问题
  15. JAVA小功能手机短信发送
  16. https://juejin.im/entry/559f1d31e4b0876bf61e4d20
  17. 笔记本外接显示屏调节亮度不刺眼
  18. 东梓关富春江畔有感  文/江湖一劍客
  19. MYSQL UNION 同列类型不同时的处理方法
  20. [大数据文章之其一] 大数据对你来说意味着什么?

热门文章

  1. Mob免费短信验证初探
  2. UBT20:ubuntu安装火焰截图
  3. Qt编写地图综合应用22-动态轨迹
  4. java工程师知识架构图图_阿里技术专家教你画架构图、Java 工程师成神之路 | 2019 年 2 月收藏排行...
  5. 教你python自动识别图文验证码的解决方案!
  6. AtCoder Beginner Contest 234 G - Divide a Sequence
  7. linux命令学习1
  8. wxpython问卷调查界面_7步教你搭建智能问卷调查系统
  9. opencv之图片简单压缩
  10. iOS14 系统 YBImageBrowser显示图片黑屏问题