您正在查看: Other 分类下的文章

语义化版本 2.0.0

语义化版本 2.0.0

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

简介

在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你专案的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API 。这可以透过文件定义或代码强制要求来实现。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:X.Y.Z (主版本号.次版本号.修订号)修复问题但不影响API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。

我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。

语义化版本控制规范(SemVer)

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  6. 修订号 Z(x.y.Z | x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

  7. 次版本号 Y(x.Y.z | x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。

  8. 主版本号 X(X.y.z | X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。

  9. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本编译元数据可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译元数据可(SHOULD)被忽略。因此当两个版本只有在版本编译元数据有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译元数据不在这份比较的列表中)。由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定:只有数字的标识符以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。数字的标识符比非数字的标识符优先层级低。若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定依赖于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。

作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。

如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

FAQ

在 0.y.z 初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

关于

语义化版本控制的规范是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立。

如果您有任何建议,请到 GitHub 上提出您的问题

许可证

知识共享 署名 3.0 (CC BY 3.0)

转载自:https://semver.org/lang/zh-CN/

C++新特性学习

C++11,14,17新特性学习

模板元编程

模板元编程概念:利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元编程则由编译器在编译器解释执行。
优势在于:1.以编译耗时为代价换来卓越的运行时性能,2.提供编译期类型计算。
劣势在于:1.代码可读性差,2.调试困难,3.编译时间长,4.可移植性差(对编译器来说)
模板元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。元数据不是运行时变量,只能是编译期常量,不能修改,常见的元数据包括enum,静态常量,基本类型和自定义类型等。元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被调用,因为它的功能和形式和运行时的函数类似,因而被成为元函数。元函数实际上表现为C++的一个类、模板类或模板函数。例如:

```cpp
template<int N,int M>
struct meta_func {
    enum {
        value = N+M
    };
}
```

调用元函数获取value值:std::cout << meta_func<1,2>::value << std::endl;
meta_func的执行过程是在编译期完成的,实际执行时是没有计算动作,而是直接使用编译期的计算结果的。元函数只处理元数据,元数据是编译期常量和类型。
因此模板元编程不能使用运行时的关键字,常用的编译期关键字如下:
1.enum,static const,用来定义编译期的整数常量;
2.typedef/using,用于定义元数据;
3.T、Types...,声明元数据类型;
4.template,主要用于定义元函数;
5.“::”,域位运算,用于解析类型作用域获取计算结果(元数据)
模板元编程中的条件判断,是通过type_traits来实现的,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。
模板元中的for等逻辑可以通过递归、重载、和模板特化(偏特化)等方法实现。

类型萃取关键词:
1.decltype 计算表达式的返回类型,并不执行表达式的计算。类似的还有std::result_of

2.编译期选择 std::conditional,它在编译期根据一个判断式选择连个类型中的一个,类似于运行时的三元表达式“?:”,用法如下:

std::conditional<true,int,float>::type 此时type的类型为int
std::conditional<false,int,float>::type 此时type为float

3.std::decay(退化),它对于普通类型来说是移除引用和cv符(const,volatile)。除了普通类型外,它还可以用于数组和函数,具体转化规则如下:
1.若T为“U的数组”或“U的数组的引用”,则成员 typedef type U*
2.若T为函数类型F或函数的引用,则成员typedef type 为 函数指针
3.否则,成员typedef type 定义为 std::remove_cv<std::remove_reference>::type,即普通类型对应的转化规则移除引用和cv符。
4.std::enable_if来实现编译期的if-else逻辑,它利用SFINAE(substitude failure is not an error)特性,根据条 件选择重载函数的元函数std::enable_if,它的原型是:
template<bool B, class T = void> struct enable_if;
根据enable_if的字面意思就可以知道,它使得函数在判断条件B仅仅为true时才有效,它的基本用法:

template <class T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type foo(T t)
{
    return t;
}
auto r = foo(1); //返回整数1
auto r1 = foo(1.2); //返回浮点数1.2
auto r2 = foo(“test”); //compile error

可以通过enable_if来实现编译期的if-else逻辑,比如下面的例子通过enable_if和条件判断式来将入参分为两大类,从而满足所有的入参类型:

template
typename std::enable_if<std::is_arithmetic::value, int>::type foo1(T t)
{
cout << t << endl;
return 0;
}

template <class T>
typename std::enable_if<!std::is_arithmetic<T>::value, int>::type foo1(T &t)
{
    cout << typeid(T).name() << endl;
    return 1;
}
对于arithmetic类型的入参则返回0,对于非arithmetic的类型则返回1,通过arithmetic将所有的入参类型分成了两大类进行处理。从上面的例子还可以看到,std::enable_if可以实现强大的重载机制,因为通常必须是参数不同才能重载,如果只有返回值不同是不能重载的,而在上面的例子中,返回类型相同的函数都可以重载。

可变模板参数的应用
template<typename... T>void f(T... args);
上面的定义中,省略号的作用:
1.声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数
2...args参数包可以展开为一个个独立的参数
展开可变模板参数函数的方法有两种:一种是通过递归函数来展开,另外一种是通过逗号表达式来展开参数包,如下:
1.递归式:
需要一个递归终止函数,如:
void print()
template<typename T,class ...Types>
void print(T first,Types... args) {
std::cout << first << std::endl;
print(args...);
}
print(1,2,3,4);
递归调用的过程为:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
//print();
递归终止函数还可以写成这样:
template
void print(T v) {
std::cout << v << std::endl;
}
void print(T first,Types... args) {
print(first);
print(args...);
}
递归调用的过程为:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
2.逗号表达式展开:
template
void printarg(T t)
{
cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
    int arr[] = {(printarg(args), 0)...};
}

expand(1,2,3,4);
同时还利用了初始化列表的技术。还可以利用lambda表达式:
template<class F, class... Args>void expand(const F& f, Args&&...args) 
{
    //这里用到了完美转发。
    initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
expand([](int i){cout<<i<<endl;}, 1,2,3);

3.折叠表达式:
就1中的例子来说,print函数可以改成这样:
template<typename... Types>
void print(Types const&... args) {
(std::cout << ... << args) << '\n';
}

可变参数模板类:
可变参数模板类是一个带可变模板参数的模板类,如标准库中的std::tuple。可以能的定义如下:
template<typename... Types>
class tuple;
这个可变参数模板类可以携带任意类型任意个数的模板参数:
std::tuple tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);
可变参数模板的模板参数个数也可以为0,所以下面的定义也是合法的:
std::tuple<> tp;
可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。如下:
1.模板篇特化和递归方式展开参数包
可变参数模板类的展开一般需要定义两到三个类,包括类声明和偏特化的模板类:
//前置声明,表明这是一个可变参数模板类
template<typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

//递归终止特化类,通过这个特化类来终止递归
template<typename Last>
struct Sum<Last>
{
    enum { value = sizeof (Last) };
};
上面的前置声明可以包含任意个数的参数,下面的声明就要求模板参数至少要有一个:
template<typename First, typename... Args>struct sum;他的展开方式为:

template<typename First, typename... Args>struct sum;//前置声明,可以省略
template<typename First, typename... Rest>//定义
struct Sum
{
    enum { value = Sum<First>::value + Sum<Rest...>::value };
};

template<typename Last>//递归终止定义
struct Sum<Last>
{
    enum{ value = sizeof(Last) };
};

递归终止模板类写法有多个:
template<typename... Args> struct sum;
template<typename First, typenameLast>
struct sum<First, Last>
{
enum{ value = sizeof(First) +sizeof(Last) };
};
在展开到两个参数时终止。还可以展开到0个参数时终止:
template<>struct sum<> { enum{ value = 0 }; };
还可以使用std::integral_constant来消除枚举定义value。利用std::integral_constant可以获得编译期常量的特性,可以将前面的sum例子改为这样:
//前置声明
template<typename First, typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};

//递归终止
template<typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};
sum<int,double,short>::value;//值为14

template<>
struct Sum<> {
    return 0;
}

2.继承方式展开参数包:
//整型序列的定义
template<int...>
struct IndexSeq{};

//继承方式,开始展开参数包
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> {};

// 模板特化,终止展开参数包的条件
template<int... Indexes>
struct MakeIndexes<0, Indexes...>
{
    typedefIndexSeq<Indexes...> type;
};

折叠表达式:
1.left fold expression:
(...op pack) =====> (((pack1 op pack2) op pack3) ...op packN)
2.right fold expression:
(pack op...) =====> (pack op (...(packN-1 op packN-2)))
3.left init fold expression:
(init op ...op pack) =====> ((( init op pack1 ) op pack2 ) ... op packN )
4.right init fold expression
(pack op ...op init) =====> ( pack1 op ( ... ( packN op init )))

右值引用和移到语义:
右值引用相关概念:右值,纯右值,将亡值,universal references,引用折叠,移动语义和完美转发。
int i = getVar();
以上面代码为例,从函数getVar()获取一个整形值,然而这会产生几种类型的值呢?答案是会产生两种类型的值,一种是左值,一种是函数返回的临时的值,这个临时的值在表达式结束后就销毁了,而左值在表达式结束后依然存在(直到其作用域结束),这个临时的值就是右值,具体来说是一个纯右值,右值是不具名的。区分左值和右值的一个简单办法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。
所有具名变量或对象都是左值,而匿名变量则是右值。如 int i = 0;这条语句,i是左值,0是字面量为右值。具体来说0是纯右值(prvalue),在C++11以上中所有的值必属于左值,将亡值,纯右值三者之一。比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。而将亡值是C++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。关于将亡值我们会在后面介绍,先看下面的代码:
int j = 5;
auto f = []{return 5;};
上面的代码中5是一个原始字面量, []{return 5;}是一个lambda表达式,都是属于纯右值,他们的特点是在表达式结束之后就销毁了。

在看下面这行代码:
    T&& k = getVar();和 int i = getVar();很像,只是比后者多了“&&”,这个声明就是右值引用。我们知道左值引用是对左值的引用,对应的,右值引用就是对右值的引用,而且右值是匿名变量,我们也只能通过应用的方式来获得右值。
    这里,getVar()产生的临时值不会像后者那样,在表达是结束后就销毁了,而是被续命,他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。
    右值引用的一个特点:通过右值引用的声明,右值又重获新生,其生命周期与右值引用类型变量的生命周期一样的长。
    右值引用的第二个特点:右值引用独立于左值和右值。即右值引用类型的变量可能是左值也可能是右值,比如:int&& var1 = 1;
    var1类型为右值引用,但var1本身是左值,因为具名变量都是左值。

    关于右值引用一个有意思的问题是:T&&是什么,一定是右值吗?如例:
    template<typename T>
    void f(T&& t){}
    f(10); //t是右值
    int x = 10;
    f(x); //t是左值
    从上面的代码中可以看到,T&&表示的值类型不确定,可能是左值又可能是右值,这一点看起来有点奇怪,这就是右值引用的一个特点。
    右值引用的第三个特点:T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。
    我们再回过头看上面的代码,对于函数template<typename T>void f(T&& t),当参数为右值10的时候,根据universal references的特点,t被一个右值初始化,那么t就是右值;当参数为左值x时,t被一个左值引用初始化,那么t就是一个左值。需要注意的是,仅仅是当发生自动类型推导(如函数模板的类型自动推导,或auto关键字)的时候,T&&才是universal references。再看看下面的例子:
    template<typename T>
    void f(T&& param); 

    template<typename T>
    class Test {
        Test(Test&& rhs); 
    };

    上面的例子中,param是universal reference,rhs是Test&&右值引用,因为模版函数f发生了类型推断,而Test&&并没有发生类型推导,因为Test&&是确定的类型了。
      正是因为右值引用可能是左值也可能是右值,依赖于初始化,并不是一下子就确定的特点,我们可以利用这一点做很多文章,比如后面要介绍的移动语义和完美转发。
      这里再提一下引用折叠,正是因为引入了右值引用,所以可能存在左值引用与右值引用和右值引用与右值引用的折叠,C++11确定了引用折叠的规则,规则是这样的:
    所有的右值引用叠加到右值引用上仍然还是一个右值引用;
    所有的其他引用类型之间的叠加都将变成左值引用。
    考虑下面的代码:
    T(T&& a) : m_val(val){ a.m_val=nullptr; }
    这行代码实际上来自于一个类的构造函数,构造函数的一个参数是一个右值引用,为什么将右值引用作为构造函数的参数呢?在解答这个问题之前我们先看一个例子。如下:
    class A
    {
    public:
        A():m_ptr(new int(0)){cout << "construct" << endl;}
        A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
        {
            cout << "copy construct" << endl;
        }
        ~A(){ delete m_ptr;}
    private:
        int* m_ptr;
    };
    A getA() {
        return A;
    }
    int main() {
        A a = GetA();
        return 0;
    }
        输出:
    construct
    copy construct
    copy construct
    这个例子很简单,一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。如果不提供深拷贝的拷贝构造函数,上面的测试代码将会发生错误(编译选项-fno-elide-constructors),内部的m_ptr将会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针悬挂问题。提供深拷贝的拷贝构造函数虽然可以保证正确,但是在有些时候会造成额外的性能损耗,因为有时候这种深拷贝是不必要的。这个时候就需要一个移动构造函数:如下:
    lass A
    {
    public:
        A() :m_ptr(new int(0)){}
        A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
        {
            cout << "copy construct" << endl;
        }
        A(A&& a) :m_ptr(a.m_ptr)
        {
            m_ptr = a.m_ptr;
            a.m_ptr = nullptr;
            cout << "move construct" << endl;
        }
        ~A(){ delete m_ptr;}
    private:
        int* m_ptr;
    };
    int main(){
        A a = Get(false); 
    } 
    输出:
    construct
    move construct
    move construct
std::move的理解,move实际上并不移动任何东西,他唯一的功能就是将一个左值强制转换为一个右值引用,如果是一些基本类型比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝(因为没有对应的移动构造函数)。所以,move对于含资源(堆内存或句柄)的对象来说更有意义。
完美转发:在函数模板中,完全依照模板参数的类型(即保持参数的左值,右值特征),将参数传递给模板函数中调用的另外一个函数
std::forward正是做这个事情的,他会按照参数的实际类型进行转发,例如:
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
    processValue(std::forward<T>(val)); //照参数本来的类型进行转发。
}
void Testdelcl()
{
    int i = 0;
    forwardValue(i); //传入左值 
    forwardValue(0);//传入右值 
}
输出:
lvaue 
rvalue
右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参   数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。

转载自:https://github.com/123youyouer/eosex/blob/master/block_chain/C++%E6%96%B0%E7%89%B9%E6%80%A7%E5%AD%A6%E4%B9%A0.md

参考

https://www.jianshu.com/p/e8cdc459f363

EOS BCSkill技术论坛 区块链求职招聘

BCSkill技术社区论坛开放

为了方便问题收集,促进学习激情,我们的BCSkill技术社区新建一个论坛。
网址:http://wiki.bcskill.com/

欢迎学习EOS等区块链技术的小伙伴,以及求职招聘(招聘加群请注明)相关的加入~

求职板块:http://wiki.bcskill.com/?forum-2.htm
招聘版块:http://wiki.bcskill.com/?forum-3.htm
我们社区内有很多区块链技术不错的小伙伴,有需要人手的,快来发帖吧~

此贴定期更新一批邀请码,或者加QQ群791420381直接获取,邀请码永远是免费的。只是为了防止垃圾广告帖,减少不必要的内容维护。

邀请码 (20190626更新)

8D93BADE852A
0B9E8C1154C9
BDBDE23003F0
022DD038C8B6
4F769BD9E717
5E99E0623F53
AECC1DFBF089
942FAA28A72E
029DB38EF967
6E2D228B1A7A
4E531F0F603C
74203AB799D8
29E5197CDD37
A4205E8F887C
CA14F3FC6CD5
8EDE670F3158
3C1CBD30D732
C1223552764A
9E55D43A0B31
94CAF29753D3
0D2DD72A3B8D
347D14F7B608
70EDBDE5BF14
9E5D6377CDE6
EFB9A497172F
0C27D7E52EA7
FBFE103795C8
E8C077491575
4935C0BCD9CA
A3434A1FC97D
D2D9FCF33093
FA52947D11B0
5486B5339604
FDA85E994D7C
A40F20A9982D
9CD1F59EFA91
B6703CDFB303
FA2FC5376CAC
DCEE07FBDF26
F485EEAE2293
41BEC6014BB9
4F65898AF09F
9345EC014A71
F630E100BC1D
F949ED29A2E0
D3ACFD1FC374
234A91A79039
65C94C6763EA
1C0A73B415BF
86A44E0B5AF0

GitLab remote: HTTP Basic: Access denied and fatal Authentication

git config --system --unset credential.helper

then enter new password for Git remote server.

参考

https://stackoverflow.com/questions/47860772/gitlab-remote-http-basic-access-denied-and-fatal-authentication