目录

C++ 标准库

记录常用的 c++特性语法。

在 C++11时期,主要用于指定一个变量或函数是编译时常量。对于条件语句,仍然会在运行时判断,而在 C++17时获得了改变:if constexpr 被引入,它允许在编译期进行条件判断。

template<typename T>
void process(T t) {
    if constexpr (std::is_integral_v<T>) {
        // 处理整数类型的情况
    } else {
        // 处理非整数类型的情况
    }
}

这个例子中,编译器会根据 T 是否是整数类型在编译期决定生成哪段代码。

用于查询表达式或变量的类型。

它不能直接传入多个参数来同时获取多个表达式的类型。但可以使用逗号运算符将多个表达式组合成一个表达式。

例如 decltype(expr1, expr2),它会获取 expr2 的类型,因为逗号运算符会先计算 expr1(但不考虑其结果),然后计算并返回 expr2 的结果,并且整个表达式的类型是 expr2 的类型。

用于在运行时获取对象或类型的类型信息。它返回一个 type_info 类型的对象,该对象包含了关于类型或对象类型的信息。

int a = 42;
std::cout << "Type of a: " << typeid(a).name() << std::endl;
  • 使用 typeid 会引入运行时开销,因此在性能敏感的代码中应谨慎使用。

引入该类型,解决了多值返回问题。传统编程中,函数返回值通常是单一类型的,若返回多值,通常依赖指针或者引用参数间接实现。

#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 变量

建议使用如下形式创建与取值:

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 取值。

模板机制为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;
}

全特化是为特定的模板参数提供完全不同的实现,而无需改变通用模板的代码。

全特化并不影响泛型模板的推导机制,如果存在全特化版本,并且推导出的参数匹配全特化版本的参数,那么编译器会选择全特化版本。否则,将使用通用的模板实现。

// 全特化类模板
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>...

偏特化后的模板,需要进一步的实例化才能形成确定的签名,并且写法上需要给出剩余的"模板形参"和必要的"模板实参",如:

template <class T2>
class A<int, T2>{
    ...
};

注意函数模板是不允许偏特化的,且多数情况下函数模板重载就可以完成函数偏特化的需要。

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;

上述代码按照空行分为三段,下面一次讲解,

  1. 基础模板定义

    首先 template<typename T, typename = void> 定义了带有两个模板参数的模板结构体,第二个模板参数有一个默认值 void。

    随后,指定了 struct HasInit 继承 std::false_type,这是一个标准库中定义的类型,表示布尔常量 false。

    基础模板是一个通用模板,用于处理多数情况,即当类型 T 没有 Init 成员函数时

  2. 特化模板定义

    首先特化模板只接受一个模板参数 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

  3. 变量模板定义

    定义了布尔类型的常量 HasInit_v,它的值是 HasInit<T>::value。由于 HasInitx<T> 继承自 std::true_type 或继承自 std::false_type,所以常量值只能是 truefalse

    变量模板提供一个方便访问的布尔常量,用于判断类型 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通常用于条件编译,即在编译期根据类型的不同特性选择不同的代码路径
  • 模板特化适用场景 :当你需要针对特定类型提供完全不同的实现时,模板特化是更好的选择。模板特化允许你为某个特定类型或一组类型提供专门的代码实现,而无需在通用代码中进行条件判断。