在 C++11 中,其引入了可变参数模板,但是可变参数模板需要挨个递归处理可变参数。这就导致就算以同一种方式处理可变参数,也需要重复的函数递归,写起来很笨重。
比如下面这个例子,仅仅只需要累加求和,但是却需要写两个模板函数使得递归可行,从而遍历每一个参数,写起来相对麻烦且臃肿。
template<typename T>
T add(T&& num)//基础函数
{
return num;
}
template<typename T, typename ... Args>
T add(T&& num, Args&&... a)//递归变参函数
{
return num + add(forward<Args>(a)...);//递归调用
}
int main()
{
cout << add(1, 2, 3, 4, 5) << endl;
}
输出:
15
折叠表达式是 C++17 新引进的语法特性。使用折叠表达式可以简化对 C++11 中引入的参数包的处理,从而在某些情况下避免使用递归。
折叠表达式共有四种语法形式。分别为一元的左折叠和右折叠,以及二元的左折叠和右折叠。其支持所有二元运算符的简写,这里仅做简单记录,比如说上文中的例子就可以简化为如下写法(二元左折叠):
template<typename T, typename ... Args>
T add(T&& num, Args&&... args)
{
return (num + ... + args);
}
int main()
{
cout << add(1, 2, 3, 4, 5) << endl;
}
/* g++ test.cpp -o test -std=c++17 */
输出:
15
总体而言,折叠表达式为变参模板写法提供了更多的可能性,可以在不影响原功能和代码可读性的前提下实现简化。
简单来说,就是在实例化类模板时,可以自动推出模板参数而不需指定。具体限制为:如果构造函数能够推导出所有模板参数,则可以跳过显式定义模板参数。
下面是几个简单但方便的例子:
int main()
{
std::pair<int, double> p1(2, 4.5); //ok
std::pair p2(2, 4.5); //-std=c++17 ok
std::vector<int> v1 = {1}; //ok
std::vector v2 = {1}; //-std=c++17 ok
std::mutex mx;
std::lock_guard<std::mutex> lock1(mx); //ok
std::lock_guard lock2(mx); //-std=c++17 ok
}
这东西怎么说,我感觉虽然方便但是并不是所有都适合这么搞。因为很容易分不清类型让代码可读性降低,所以我感觉像 pair
、tuple
、lock_guard
这种临时变量用一下还好(因为其中的模板参数往往已经规定,省略一下可读性影响也不大),如果有作用域比较广的变量最好就不要用了,因为很可能需要费神来思考它的模板参数。
从 C++17 开始,可以使用 auto
来声明一个非类型模板参数。
template<auto N> class S
{
...
};
S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char
如果非类型模板形参的类型包含占位符类型 auto
,被推导类型的占位符 (C++20 起),或 decltype(auto),那么它可以被推导。推导会如同在虚设的声明 T x = 模板实参;
中推导变量 x 的类型一样进行,其中 T 是模板形参的声明类型。如果被推导的类型不能用于非类型模板形参,那么程序非良构。
template
struct B { /* ... */ };
B<5> b1; // OK:非类型模板形参的类型是 int
B<'a'> b2; // OK:非类型模板形参的类型是 char
B<2.5> b3; // 错误(C++20 前):非类型模板形参的类型不能是 double
对于类型中使用了占位符类型的非类型模板形参包,每个模板实参的类型会独立进行推导,而且不需要互相匹配:
template
struct C {};
C<'C', 0, 2L, nullptr> x; // OK
constexpr if
是 C++17 中新特性,可以实现在编译期的条件判断。
这个东西主要是为了实现泛型编译期处理中的条件判定,可以实现根据 constexpr if
来编译合适的代码段,从而可以在编译期干一部分模板类型判断相关的事情,提高代码的执行效率。
template <typename T>
void bar(T t)
{
if constexpr (has_foo_v<T>)
{
t.foo();
std::puts("yes");
}
else
{
std::puts("no");
}
}
等价于:
template <typename T, typename=std::enable_if_t<has_foo_v<T>>>
void bar(T t)
{
t.foo();
std::puts("yes");
}
void bar(...)
{
std::puts("no");
}
在 C++17 后,可以给变量加上 inline
标签从而使其成为内联变量。其核心目的就是为了可以在头文件中定义一个全局可用的对象。
class MyClass
{
static inline std::string name = ""; // OK since C++17
...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files
另外声明为 constexpr
的静态成员变量(但不是命名空间作用域变量)是隐式的内联变量,即以下二者等价。
struct D
{
static constexpr int n = 5; // C++11/C++14: //声明但未定义
// since C++17: 定义
};
struct D
{
inline static constexpr int n = 5;
};
想深入了解可以看一下这位大佬的文章: 点我跳转
在我的理解中,这个新特性就是可以对含有多个元素的对象进行一个映射,即绑定指定名称到初始化器的子对象或元素。可能有点类似引用,但不同于引用的是,结构化绑定的类型不必为引用类型。
第一个用法,绑定数组:
int a[2] = {1,2};
auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
第二个用法,绑定元组式类型:
float x{};
char y{};
int z{};
std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
const auto& [a,b,c] = tpl;
// a 指名指代 x 的结构化绑定;decltype(a) 为 float&
// b 指名指代 y 的结构化绑定;decltype(b) 为 char&&
// c 指名指代 tpl 的第 3 元素的结构化绑定;decltype(c) 为 const int
第三个用法,绑定到数据成员:
struct S {
mutable int x1 : 2;
volatile double y1;
};
S f();
const auto [x, y] = f(); // x 是标识 2 位位域的 int 左值
// y 是 const volatile double 左值
我感觉,这个新特性的绑定元组部分还是挺有用的,包括接收返回值和范围for的取值等场景用起来都很方便和优雅。比如说下面这个例子,就可以省去取值的部分。
int main()
{
vector<pair<int, int>> a{{1, 2}, {3, 4}};
for(auto& [x, y] : a)
{
cout << x << y << endl;
}
}
简单来说就是在 if
和 switch
语句中也可以初始化变量了,类似于在 for
循环里声明新的变量。但是我感觉在 if/switch
中初始化变量的需求应该是不会太大,产生需求的主要场景应该是为了控制变量的作用域。
int main()
{
int s = 1;
if(int a = 0; a == s) //A部分
{
cout << a << endl;
}
else if(int b = 1; b == s) //B部分
{
cout << a << endl;
cout << b << endl;
}
else //C部分
{
cout << a << endl;
cout << b << endl;
cout << 222 << endl;
}
switch(int c = 2; c)
{
case 1:
case 2:
cout << c << endl;
break;
default:
cout << 333 << endl;
break;
}
}
输出:
0
1
2
如上为在 if/switch
语句中初始化变量。其中在 if
语句部分中,变量 a
的作用域为 A/B/C,变量 b
的作用域为 B/C。而在 switch
语句部分中,变量 c
的作用域为整个 switch
。
总体而言这个新特性通过在代码块中添加大括号也可以等价实现,但是看上去可能没那么优雅,所以还是有其独特的优点的。
就是一个新的字符字面量,UTF-8字符字面量(每个字符大小为1字节),格式如下,个人感觉没什么好说的。
u8'c字符'
例:
char c = u8'a'; //sizeof(c) = 1
constexpr char str[] = u8"123456"
简化嵌套命名空间定义: namespace A::B::C { ... }
等价于 namespace A { namespace B { namespace C { ... } } }
。
using 声明多个名称: 拥有多于一个 using
声明符的 using
声明,等价于对应的单个 using
声明符的 using
声明的序列,人话讲就是可以用逗号连接多个 using
声明。cppreference上的例子如下:
void f();
namespace A {
void g();
}
namespace X {
using ::f; // 全局 f 现在作为 ::X::f 可见
using A::g; // A::g 现在作为 ::X::g 可见
using A::g, ::f; // (C++17) OK:命名空间作用域允许双重声明
}
void h()
{
X::f(); // 调用 ::f
X::g(); // 调用 A::g
}
但是经过我自己的测试,C++17 我只发现新增了 using
双重定义相关的内容,其余的使用 C++11 标准也可以编译通过。
在c++ 17异常处理规范成为函数类型的一部分。也就是说,下面两个函数现在有两种不同的类型:
void f1();
void f2() noexcept; // different type
在c++ 17之前,这两个函数都具有相同的类型。
因此,编译器现在将检测如果你使用一个函数抛出异常,而一个函数不抛出任何异常的情况:
void (*fp)() noexcept; // pointer to function that doesn’t throw
fp = f2; // OK
fp = f1; // ERROR since C++17
当然,在允许抛出函数的地方使用不抛出的函数仍然是有效的:
void (*fp2)(); // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK
因此,这个新特性不会破坏那些还没有使用noexcept函数指针的程序,但是现在可以确保您不再违反函数指针中的noexcept规范。想深入了解可以看一下这位大佬的文章: 点我跳转
具体目的和影响可以看一下 StackOverflow 上的这个问题:点我跳转
简单来讲就是在对象的初始化中,当初始化器表达式是一个与变量类型相同的类类型的纯右值(忽略 cv 限定)时,T x = T(T(f())); //仅调用一次 T 的默认构造函数以初始化 x
。比如下面这个例子,关掉g++默认的省略优化后,使用不同的标准编译,可以看出以C++17标准编译的话会进行一次复制消除(返回值优化),优化的位置是 main 函数里的无名临时量。
class test
{
public:
test()
{
cout << "constructor" << endl;
}
~test()
{
cout << "destructor" << endl;
}
test(const test& other)
{
cout << "copy constructor" << endl;
}
};
test Foo()
{
test obj;
return obj;
}
int main()
{
test obj = Foo();
}
编译:
g++ -g -fno-elide-constructors -Wall t.cpp -o t -std=c++11
输出:
constructor
copy constructor
destructor
copy constructor
destructor
destructor
编译:
g++ -g -fno-elide-constructors -Wall t.cpp -o t -std=c++17
输出:
constructor
copy constructor
destructor
destructor
想深入了解可以看一下这位大佬的文章: 点我跳转
lambda中可以捕获 *this
辣,可以通过捕获 *this
来将整个当前对象复制一遍。而之前只能捕获 this
,即以引用捕获当前对象。我感觉新增这种捕获方式的一个应用场景就是可以避免悬垂引用问题。
lambda表达式的格式为:[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) { 函数体 }
,而在 C++17 中其说明符部分新增了一位成员 constexpr
。
constexpr
函数。如果没有此说明符但函数调用运算符或任意给定的运算符模板特化恰好满足针对 constexpr
函数的所有要求,那么它也会是 constexpr
的。
简单来说就是简化下面这种情况:
简化前:
void f()
{
[[rpr::kernel, rpr::target(cpu,gpu)]] // 重复
doTask();
}
简化后:
void f()
{
[[using rpr: kernel, target(cpu,gpu)]]
doTask();
}
即一种新的源文件包含语法,其作用为检查一个头或源文件是否可以被包含。cppreference上的例子如下,我感觉适配不同版本头文件时可能有用吧。
#if __has_include()
# include
# define has_optional 1
template<class T> using optional_t = std::optional<T>;
#elif __has_include()
# include
# define has_optional -1
template<class T> using optional_t = std::experimental::optional<T>;
#else
# define has_optional 0
# include
template<class V> class optional_t {
V v_{}; bool has_{false};
public:
optional_t() = default;
optional_t(V&& v) : v_(v), has_{true} {}
V value_or(V&& alt) const& { return has_ ? v_ : alt; }
/*...*/
};
#endif