C++ 标准库
记录常用的 c++特性语法。
1 关键字
1.1 constexpr
在 C++11时期,主要用于指定一个变量或函数是编译时常量。对于条件语句,仍然会在运行时判断,而在 C++17时获得了改变:if constexpr 被引入,它允许在编译期进行条件判断。
template<typename T>
void process(T t) {
if constexpr (std::is_integral_v<T>) {
// 处理整数类型的情况
} else {
// 处理非整数类型的情况
}
}
这个例子中,编译器会根据 T 是否是整数类型在编译期决定生成哪段代码。
1.2 decltype
用于查询表达式或变量的类型。
它不能直接传入多个参数来同时获取多个表达式的类型。但可以使用逗号运算符将多个表达式组合成一个表达式。
例如 decltype(expr1, expr2),它会获取 expr2 的类型,因为逗号运算符会先计算 expr1(但不考虑其结果),然后计算并返回 expr2 的结果,并且整个表达式的类型是 expr2 的类型。
1.3 typeid
用于在运行时获取对象或类型的类型信息。它返回一个 type_info 类型的对象,该对象包含了关于类型或对象类型的信息。
int a = 42;
std::cout << "Type of a: " << typeid(a).name() << std::endl;
- 使用
typeid会引入运行时开销,因此在性能敏感的代码中应谨慎使用。
2 Pair 和 Tuple
引入该类型,解决了多值返回问题。传统编程中,函数返回值通常是单一类型的,若返回多值,通常依赖指针或者引用参数间接实现。
2.1 多值返回与解构
#include <tuple>
std::tuple<int, double, std::string> get_object_properties() {
int id = 1;
double value = 3.14;
std::string name = "Object1";
return {id, value, name};
}
auto [id, value, name] = get_object_properties();
// 现在可以分别使用 id、value 和 name 变量
2.2 创建与取值
建议使用如下形式创建与取值:
auto t = make_tuple(22, 44, "nico");
auto v = get<1>(t);
倘若希望改动 tuple内的一个既有值,那么:
std::string s;
auto x = std::make_tuple(s); // x is of type tuple<string>
std::get<0>(x) = "my value"; // modifies x but not s
auto y = std::make_tuple(std::ref(s)); // y is of type tuple<string&>, thus y refers to s
std::get<0>(y) = "my value"; // modifies y
如果想方便的局部提取 tuple中的元素,那么使用 tie与 std::ignore 结合:
std::tuple<int, float, std::string> t(77, 1.1, "more light");
int i;
std::string s;
std::tie(i, std::ignore, s) = t; // assigns first and third value of t to i and s
pair 相比 tuple,只可以存储两个值,利用 first、second 取值。
3 模块特化与偏特化
模板机制为C++提供了泛型编程的方式,在减少代码冗余的同时仍然可以提供类型安全。
特化必须在同一命名空间下进行,可以特化类模板也可以特化函数模板,但类模板可以偏特化和全特化,而函数模板只能全特化。 模板实例化时会优先匹配"模板参数"最相符的那个特化版本。
通过如下模板,举例全特化、偏特化的区别:
// 类模板
template <class T1, class T2>
class A{
T1 data1;
T2 data2;
};
// 函数模板
template <class T>
T max(const T lhs, const T rhs){
return lhs > rhs ? lhs : rhs;
}
3.1 全特化
全特化是为特定的模板参数提供完全不同的实现,而无需改变通用模板的代码。
全特化并不影响泛型模板的推导机制,如果存在全特化版本,并且推导出的参数匹配全特化版本的参数,那么编译器会选择全特化版本。否则,将使用通用的模板实现。
// 全特化类模板
template <>
class A<int, double>{
int data1;
double data2;
};
// 函数模板
template <>
int max<int>(const int lhs, const int rhs){
return lhs > rhs ? lhs : rhs;
}
全特化的模板参数列表应当是空的,并且应当给出"模板实参"列表。倘若不指定"模板实参"列表,编译器会通过函数签名进行推导,但这一过程是有歧义的,那么可能导致错误 error: no function template matches function template specialization 'f'。
因此建议,显式指定"模板实参",如上述的 int max<int>... 和 class A<int, double>...。
3.2 偏特化
偏特化后的模板,需要进一步的实例化才能形成确定的签名,并且写法上需要给出剩余的"模板形参"和必要的"模板实参",如:
template <class T2>
class A<int, T2>{
...
};
注意函数模板是不允许偏特化的,且多数情况下函数模板重载就可以完成函数偏特化的需要。
4 Type Trait 和 Type Utility
Type Trait 在编译期间进行类型推断和类型控制,从而实现更为安全和高效的泛化代码;Type Utilities 简化模板编程中的类型操作过程,提供了便捷的方式,使得代码更加简洁。
下面提供一个自定义案例,用来判断对应的 T 类型是否包含 Init 函数。语法复杂,会给予释义。
以下是 C++17 的写法,依赖 std::void_t 方法:
template<typename T, typename = void>
struct HasInit : std::false_type {};
template<typename T>
struct HasInit<T, std::void_t<decltype(std::declval<T>().Init())>> : std::true_type {};
template<typename T>
constexpr bool HasInit_v = HasInit<T>::value;
上述代码按照空行分为三段,下面一次讲解,
-
基础模板定义
首先
template<typename T, typename = void>定义了带有两个模板参数的模板结构体,第二个模板参数有一个默认值 void。随后,指定了
struct HasInit继承std::false_type,这是一个标准库中定义的类型,表示布尔常量 false。基础模板是一个通用模板,用于处理多数情况,即当类型
T没有Init成员函数时。 -
特化模板定义
首先特化模板只接受一个模板参数
T,而第二个参数依赖于T的类型推导。其中,
std::declval<T>()是一个标准库辅助函数,用于获取类型T的伪对象(不实际构造对象)。decltype(std::declval<T>().Init())用于检测表达式std::declval<T>().Init()的类型。如果能够成功调用Init()成员函数,那么decltype将推导出返回类型。然后std::void_t将这个类型转换为void。表达式的作用总结来说:如果
T类型有Init成员函数,那么std::void_t将生成void类型;否则,将导致 SFINAE(替换失败不是错误)。特化模板的作用是当类型
T确实有Init成员函数时,他继承自std::true_type,即返回 true。 -
变量模板定义
定义了布尔类型的常量
HasInit_v,它的值是HasInit<T>::value。由于HasInitx<T>继承自std::true_type或继承自std::false_type,所以常量值只能是true或false。变量模板提供一个方便访问的布尔常量,用于判断类型
T是否有Init成员函数。
以下是 C++11 的写法:
#include <type_traits>
template<typename T>
struct HasInit {
template<typename U>
static auto test(U*) -> decltype(std::declval<U>().Init(), std::true_type{});
template<typename U>
static std::false_type test(...);
using type = decltype(test<T>(nullptr));
};
template<typename T>
constexpr bool HasInit_v = HasInit<T>::type::value;
使用了较为传统的 SFINAE (Substitution Failure Is Not An Error )技术,通过重载函数模板 test 来实现。
首先,进行了一个模板静态成员函数的声明 auto test(U*) ->,并将函数的返回值通过 decltype 表达式进行指定。即指定类型U 具有 Init 成员函数,返回 std::true_type ,否则触发 SFINAE 特性,这个函数不作为候选函数。
而后,定义另一个成员函数 std::false_type test(...),接受可变参数列表(用…表示),这个函数返回值为 std::false_type ,当上述函数因为 SFINAE 失败被排除后,这个函数就会被选中。
最后,定义了变量type,它表示调用 test<T>(nullptr) 的返回类型。这里通过传递 nullptr(它被隐式转换为目标类型 T*)来调用前面声明的两个 test 函数中的一个。那么type 的值要么是 std::true_type ,要么是 std::false_type。
函数 auto test(U*) 的入参使用 U*,是方便后续 test
(nullptr) 传入 nullptr 方便校验。
场景小结:
- Type Traits适用场景 :当需要在通用代码中根据类型的某些特性进行小的调整时,Type Traits是合适的。例如,判断类型是否支持某种操作,据此决定使用何种算法或数据结构。Type Traits通常用于条件编译,即在编译期根据类型的不同特性选择不同的代码路径。
- 模板特化适用场景 :当你需要针对特定类型提供完全不同的实现时,模板特化是更好的选择。模板特化允许你为某个特定类型或一组类型提供专门的代码实现,而无需在通用代码中进行条件判断。