1. 变量模板
变量模板定义一族变量或静态数据成员。
template < 形参列表 > 变量声明 | (1) | |
template < 形参列表 > requires 约束 变量声明 | (2) | (C++20 起) |
变量声明 | 变量的声明。声明的变量名成为模板名。 |
形参列表 | 非空的模板形参的逗号分隔列表,每项是非类型形参、类型形参、模板形参,或任何上述的形参包之一。 |
约束 | 约束表达式,限制这个变量模板所能接受的模板形参 |
从变量模板实例化的变量被称为被实例化变量,从静态数据成员模板实例化的变量被称为被实例化静态数据成员。变量模板可以通过处于命名空间作用域中的模板声明引入,其中 变量声明 声明一个变量。
template<class T>
constexpr T pi = T(3.1415926535897932385L); // 变量模板
template<class T>
T circular_area(T r) // 函数模板
{
return pi<T> * r * r; // pi<T> 是变量模板实例化
}
在类作用域中使用时,变量模板声明一个静态数据成员模板。
using namespace std::literals;
struct matrix_constants
{
template<class T>
using pauli = hermitian_matrix<T, 2>; // 别名模版
template<class T> // 静态数据成员模板
static constexpr pauli<T> sigmaX = {{0, 1}, {1, 0}};
template<class T>
static constexpr pauli<T> sigmaY = {{0, -1i}, {1i, 0}};
template<class T>
static constexpr pauli<T> sigmaZ = {{1, 0}, {0, -1}};
};
与其他静态成员一样,静态数据成员模板的需要一个定义。这种定义可以在类定义外提供。处于命名空间作用域的静态数据成员的模板声明也可以是类模板的非模板数据成员的定义:
struct limits
{
template<typename T>
static const T min; // 静态数据成员模板的声明
};
template<typename T>
const T limits::min = {}; // 静态数据成员模板的定义
template<class T>
class X
{
static T s; // 类模板的非模板静态数据成员的声明
};
template<class T>
T X<T>::s = 0; // 类模板的非模板静态数据成员的定义
除非变量模板被显式特化或显式实例化,否则在变量模板的特化在要求变量定义存在的语境中被引用,或定义存在与否影响程序语义时,即表达式(可能不使用定义)对常量求值需要该变量时,隐式实例化它。
如果有表达式需要某变量进行常量求值,那么变量定义存在与否会影响程序语义,即使不要求常量求值表达式,或常量表达式求值不使用该定义。
在 C++14 引入变量模板前,参数化变量通常实现为类模板的静态数据成员,或返回所需值的 constexpr 函数模板。变量模板不能用作模板模板实参。
定义一个变量并使用数据转换(类型转换)是一种常见的编程方式,但与变量模板有一些区别:
- 通用性: 变量模板允许你通过模板参数来生成多个不同类型的变量,从而在不同的上下文中使用。这使得代码更具通用性和可扩展性,因为你可以为多个类型生成相应的变量。相比之下,直接定义变量并使用数据转换通常只适用于特定的一种数据类型。
- 模板化: 变量模板是一种模板化的方式来生成变量,它遵循 C++ 的模板机制,这意味着你可以使用模板特化、部分特化等技术来定制化生成的变量,以满足不同的需求。而使用数据转换时,你必须显式地执行类型转换,这可能会在代码中引入不必要的重复。
- 编译时计算: 变量模板通常用于在编译时生成值,因此可以在编译阶段进行类型检查和计算。这有助于提高代码的性能和安全性。而数据转换可能在运行时进行,可能会引入一些运行时开销和类型错误的风险。
- 抽象性: 变量模板可以在更高的抽象层次上操作数据,使代码更具表达力和可读性。它允许你以更自然的方式描述某个值与特定类型之间的关系,而不必显式进行类型转换。
变量模板提供了一种更灵活、通用和模板化的方式来生成变量,适用于需要在不同类型上工作的情况。当需要为多个类型生成特定的变量或值时,变量模板是一种更优雅和强大的选择。
2. 泛型lambda
构造闭包:能够捕获作用域中的变量的无名函数对象。C++14添加了两个新功能:参数推断(auto)、参数初始化后捕获(可以在[]
对某个新参数进行赋值)。
#include <iostream>
void Test() {
int a = 0;
auto F1 = [a = 3](auto y) { return a * y; };
std::cout << "F1: " << F1(10) << std::endl;
int b = 0;
int c = 20;
auto F2 = [=, a = 10, b = 20](auto y) { return a + b + c + y; };
std::cout << "F2: " << F2(10) << std::endl;
int d = 50;
auto F3 = [=, a = std::move(d), b = 20](auto y) { return a + b + c + y; };
std::cout << "F3: " << F3(10) << std::endl;
auto F4 = [](auto y) -> auto{ return y + 10 + 20; };
std::cout << "F4: " << F4(10) << std::endl;
}
int main() {
Test();
return 0;
}
执行输出
F1: 30 F2: 60 F3: 100 F4: 40 |
3. constexpr限制放宽
在c++14前,constexpr基本功能如下:
- constexpr修饰变量,要求变量必须可以在编译器推导出来
- constexpr修饰函数(返回值),函数内除了可以包含using和typedef指令以及
static_asssert
断言外,只能包含一条return
语句 - constexpr同时可以修饰构造函数,但也会要求使用这个构造函数的时候,可以在编译器就把相关的内容全推导出来
在c++14中,允许使用循环、if、switch等语句,最终还是在编译期间获得字面常量。
4. 二进制字面量
二进制字面量 是字符序列 0b
或字符序列 0B
后随一或多个二进制数字(0
、1
)。在c++11中使用也不提示编译错误(和编译器实现有关),正式加入c++标准在c++14中提出。
#include <iostream>
void Test() {
int a = 0b01010101;
int b = 0B01010111;
std::cout << std::hex << "a = 0x" << a << ", b = 0x" << b << std::endl;
}
int main() {
Test();
return 0;
}
执行结果
a = 0x55, b = 0x57 |
5. 数位分隔符
数字之间可插入作为分隔符的可选的单引号(`)。编译器会忽略它们。
#include <iostream>
void Test() {
unsigned long long l1 = 18446744073709550592ull; // C++11
unsigned long long l2 = 18'446'744'073'709'550'592llu; // C++14
unsigned long long l3 = 1844'6744'0737'0955'0592uLL; // C++14
unsigned long long l4 = 184467'440737'0'95505'92LLU; // C++14
std::cout << "l1 = " << l1 << std::endl;
std::cout << "l2 = " << l2 << std::endl;
std::cout << "l3 = " << l3 << std::endl;
std::cout << "l4 = " << l4 << std::endl;
}
int main() {
Test();
return 0;
}
执行结果
l1 = 18446744073709550592 l2 = 18446744073709550592 l3 = 18446744073709550592 l4 = 18446744073709550592 |
6. 函数返回类型推导
函数声明引入函数名和它的类型。函数定义将函数名/类型与函数体关联起来。
如果函数声明的声明说明符序列 包含关键词 auto,那么尾随返回类型可以省略,且编译器将从返回语句中所用的表达式的类型推导出它。如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行:
int x = 1;
auto f() { return x; } // 返回类型是 int
const auto& f() { return x; } // 返回类型是 const int&
如果返回类型是 decltype(auto),那么返回类型是将返回语句中所用的表达式包裹到 decltype 中时所得到的类型:
int x = 1;
decltype(auto) f() { return x; } // 返回类型是 int,同 decltype(x)
decltype(auto) f() { return (x); } // 返回类型是 int&,同 decltype((x))
(注意:“const decltype(auto)&”是错误的,decltype(auto) 必须独自使用)
如果有多条返回语句,那么它们必须推导出相同的类型:
auto f(bool val)
{
if (val) return 123; // 推导出返回类型 int
else return 3.14f; // 错误:推导出返回类型 float
}
如果没有返回语句或返回语句的实参是 void 表达式,那么所声明的返回类型,必须要么是 decltype(auto),此时推导返回类型是 void,要么是(可有 cv 限定的)auto,此时推导的返回类型是(具有相同 cv 限定的)void。
auto f() {} // 返回 void
auto g() { return f(); } // 返回 void
auto* x() {} // 错误: 不能从 void 推导 auto*
一旦在函数中见到一条返回语句,那么从该语句推导的返回类型就可以用于函数的剩余部分,包括其他返回语句:
auto sum(int i)
{
if (i == 1)
return i; // sum 的返回类型是 int
else
return sum(i - 1) + i; // OK,sum 的返回类型已知
}
如果返回语句使用花括号包围的初始化器列表,那么就不能推导:
auto func() { return {1, 2, 3}; } // 错误
struct F
{
virtual auto f() { return 2; } // 错误
};
除了用户定义转换函数以外的函数模板可以使用返回类型推导。即使返回语句中的表达式并非待决,推导也在实例化时发生。这种实例化并不处于 SFINAE 的目的的立即语境中。
template<class T>
auto f(T t) { return t; }
typedef decltype(f(1)) fint_t; // 实例化 f<int> 以推导返回类型
template<class T>
auto f(T* t) { return *t; }
void g() { int (*p)(int*) = &f; } // 实例化两个 f 以确定返回类型,
// 选择第二个模板重载
使用返回类型推导的函数或函数模板的重声明或特化必须使用同一返回类型占位符:
auto f(int num) { return num; }
// int f(int num); // 错误:返回类型未使用占位符
// decltype(auto) f(int num); // 错误:占位符不同
template<typename T>
auto g(T t) { return t; }
template auto g(int); // OK:返回类型是 int
// template char g(char); // 错误:不是主模板 g 的特化
反过来也一样:不使用返回类型推导的函数或函数模板的重声明或特化不能使用返回类型占位符:
int f(int num);
// auto f(int num) { return num; } // 错误:不是 f 的重声明
template<typename T>
T g(T t) { return t; }
template int g(int); // OK:特化 T 为 int
// template auto g(char); // 错误:不是主模板 g 的特化
显式实例化声明本身并不会实例化使用返回类型推导的函数模板:
template<typename T>
auto f(T t) { return t; }
extern template auto f(int); // 不会实例化 f<int>
int (*p)(int) = f; // 实例化 f<int> 以确定它的返回类型,
// 但仍需要在程序的别处出现显式实例化的定义
7. [[deprecated]]
标记
表示已经弃用的函数,编译时会提示警告信息。
#include <iostream>
[[deprecated]] void Test1() { std::cout << "Test1" << std::endl; }
void Test() { Test1(); }
int main() {
Test();
return 0;
}
编译警告
../C11Project/main.cpp: In function ‘void Test()’: ../C11Project/main.cpp:16:20: warning: ‘void Test1()’ is deprecated [-Wdeprecated-declarations] 16 | void Test() { Test1(); } | ~~~~~^~ ../C11Project/main.cpp:14:21: note: declared here 14 | [[deprecated]] void Test1() { std::cout << "Test1" << std::endl; } | ^~~~~ |
8. make_unique
c++14新增std::unique_ptr智能指针包装器接口实现。
#include <iostream>
#include <memory>
void Test() {
std::unique_ptr<int> ptr = std::make_unique<int>(10);
std::cout << *ptr << std::endl; // 10
}
int main() {
Test();
return 0;
}
9. std::shared_timed_mutex & std::shared_lock
shared_timed_mutex
类是一种同步原语,能用于保护数据免受多个线程同时访问。与其他促进独占访问的互斥体类型相反,它拥有两个访问层次:
- 共享 - 多个线程能共享同一互斥体的所有权。
- 独占 - 仅一个线程能占有互斥体。
共享互斥体通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。
#include <mutex>
#include <shared_mutex>
class R {
mutable std::shared_timed_mutex mut;
public:
R &operator=(const R &other) {
// 写入,独占
std::unique_lock<std::shared_timed_mutex> lhs(mut, std::defer_lock);
// 读取,共享
std::shared_lock<std::shared_timed_mutex> rhs(other.mut, std::defer_lock);
std::lock(lhs, rhs);
return *this;
}
};
void Test() { R r; }
int main() {
Test();
return 0;
}
10. 其他
- std::exchange
#include <iostream>
#include <string>
#include <utility>
void Test() {
std::string s1 = "hello";
std::string s2 = "world";
std::exchange(s1, s2); // 将s2值赋值给s1,不改变s2值
std::cout << s1 << ", " << s2 << std::endl; // world, world
}
int main() {
Test();
return 0;
}
std::exchange
可以在实现移动赋值运算符和移动构造函数时使用:
struct S
{
int n;
S(S&& other) noexcept : n{std::exchange(other.n, 0)} {}
S& operator=(S&& other) noexcept
{
n = std::exchange(other.n, 0); // 移动 n,并于 other.n 留下零
// (注意:自我移动赋值时,n 不会改变)
return *this;
}
};
- std::integer_sequeuce
类模板 std::integer_sequence
表示一个编译时的整数序列。在用作函数模板的实参时,能推导参数包 Ints
并将它用于包展开。
#include <iostream>
#include <string>
#include <utility>
template <typename T, T... ints>
void Print(int id, std::integer_sequence<T, ints...> int_seq) {
std::cout << id << ") 大小为 " << int_seq.size() << " 的序列: ";
((std::cout << ints << ' '), ...);
std::cout << '\n';
}
void Test() {
Print(1, std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
Print(2, std::make_integer_sequence<int, 12>{});
Print(3, std::make_index_sequence<10>{});
Print(4, std::index_sequence_for<std::ios, float, signed>{});
}
int main() {
Test();
return 0;
}
执行结果
1) 大小为 7 的序列: 9 2 5 1 9 1 6 2) 大小为 12 的序列: 0 1 2 3 4 5 6 7 8 9 10 11 3) 大小为 10 的序列: 0 1 2 3 4 5 6 7 8 9 4) 大小为 3 的序列: 0 1 2 |
- std::quoted
允许插入或提取带引号字符串,例如在 CSV 或 XML 中出现的那些格式。
#include <iomanip>
#include <iostream>
#include <sstream>
void default_delimiter()
{
const std::string in = "std::quoted() quotes this string and embedded \"quotes\" too";
std::stringstream ss;
ss << std::quoted(in);
std::string out;
ss >> std::quoted(out);
std::cout << "默认分隔符的情形:\n"
"读取为 [" << in << "]\n"
"存储为 [" << ss.str() << "]\n"
"写出为 [" << out << "]\n\n";
}
void custom_delimiter()
{
const char delim{'$'};
const char escape{'%'};
const std::string in = "std::quoted() quotes this string and embedded $quotes$ $too";
std::stringstream ss;
ss << std::quoted(in, delim, escape);
std::string out;
ss >> std::quoted(out, delim, escape);
std::cout << "自定义分隔符的情形:\n"
"读取为 [" << in << "]\n"
"存储为 [" << ss.str() << "]\n"
"写出为 [" << out << "]\n\n";
}
int main()
{
default_delimiter();
custom_delimiter();
}
执行输出
默认分隔符的情形: 读取为 [std::quoted() quotes this string and embedded "quotes" too] 存储为 ["std::quoted() quotes this string and embedded \"quotes\" too"] 写出为 [std::quoted() quotes this string and embedded "quotes" too] 自定义分隔符的情形: 读取为 [std::quoted() quotes this string and embedded $quotes$ $too] 存储为 [$std::quoted() quotes this string and embedded %$quotes%$ %$too$] 写出为 [std::quoted() quotes this string and embedded $quotes$ $too] |