CPP-Templates-2nd--第二十章 基 于 类 型 属 性 的 重 载(Overloading on Type Properties)

目录

20.1 算法特化

20.2 标记派发(Tag Dispatching)

20.3 Enable/Disable 函数模板

20.3.1 提供多种特化版本

20.3.2 EnableIf 所之何处(where does the EnableIf Go)?

20.3.3 编译期 if

20.3.4 Concepts C++20

20.4 类的特化(Class Specialization)

20.4.1 启用/禁用类模板

20.4.2 类模板的标记派发

20.5 实例化安全的模板(Instantiation-Safe Templates)

参考:GitHub - Walton1128/CPP-Templates-2nd--: 《C++ Templates 第二版》中文翻译,和原书排版一致,第一部分(1至11章)以及第18,19,20,21、22、23、24、25章已完成,其余内容逐步更新中。 个人爱好,发现错误请指正函数重载使得相同的函数名能够被多个函数使用,只要能够通过这些函数的参数类型区分它 们就行。比如:

void f (int);
void f (char const*);

对于函数模板,可以在类型模式上进行重载,比如针对指向 T 的指针或者 Array<T>

template<typename T> void f(T*);
template<typename T> void f(Array<T>);

在类型萃取(参考第 19 章)的概念流行起来之后,很自然地会想到基于模板参数对函数模 板进行重载。比如:

template<typename Number> void f(Number); // only for numbers
template<typename Container> void f(Container);// only for containers

但是,目前 C++还没有提供任何可以直接基于类型属性进行重载的方法。事实上,上面的两 个模板声明的是完全相同的函数模板,而不是进行了重载,因为在比较两个函数模板的时候 不会比较模板参数的名字。

有比较多的基于类型特性的技术,可以被用来实现类似于函数模板重载的功能。

20.1 算法特化

不是函数模板偏特化。函数模板不支持偏特例化,主要是为了避免与重载冲突。

考虑 一个交换两个数值的 swap()操作:

template<typename T>
void swap(T& x, T& y)
{
T tmp(x);
x = y;
y = tmp;
}template<typename T>
void swap(Array<T>& x, Array<T>& y)
{
swap(x.ptr, y.ptr);
swap(x.len, y.len);
}

在有更为特化的版本(也更高效)可用的时候,编译器会优先选择该版本,在 其不适用的时候,会退回到更为泛化的版本

在一个泛型算法中引入更为特化的变体,这一设计和优化方式被称为算法特化(algorithm specialization)。

并不是所有的概念上更为特化的算法变体,都可以被直接转换成提供了正确的部分排序行为 (partial ordering behavior)的函数模板。比如我们下面的这个例子。

函数模板 advanceIter()(类似于 C++标准库中的 std::advance())会将迭代器 x 向前迭代 n 步。 这一算法可以用于输入的任意类型的迭代器: 

template<typename InputIterator, typename Distance>
void advanceIter(InputIterator& x, Distance n)
{
while (n > 0) { //linear time
++x;
--n;
}
}template<typename RandomAccessIterator, typename Distance>
void advanceIter(RandomAccessIterator& x, Distance n) {
x += n; // constant time
}

但是不幸的是,同时定义以上两种函数模板会导致编译错误,正如我们在序言中介绍的那样, 这是因为只有模板参数名字不同的函数模板是不可以被重载的。

20.2 标记派发(Tag Dispatching)

算法特化的一个方式是,用一个唯一的、可以区分特定变体的类型来标记(tag)不同算法 变体的实现。比如为了解决上述 advanceIter()中的问题,可以用标准库中的迭代器种类标记 类型,来区分 advanceIter()算法的两个变体实现:

template<typename Iterator, typename Distance>
void advanceIterImpl(Iterator& x, Distance n,
std::input_iterator_tag)
{
while (n > 0) { //linear time
++x;
--n;
}
}
template<typename Iterator, typename Distance>
void advanceIterImpl(Iterator& x, Distance n,
std::random_access_iterator_tag)
{
x += n; // constant time
}

然后,通过 advanceIter()函数模板将其参数连同与之对应的 tag 一起转发出去:

template<typename Iterator, typename Distance>
void advanceIter(Iterator& x, Distance n)
{
advanceIterImpl(x, n, typename
std::iterator_traits<Iterator>::iterator_category())
}

有效使用标记派发(tag dispatching)的关键在于理解 tags 之间的内在关系。我们用来标记 两个 advanceIterImpl 变体的标记是 std::input_iterator_tag 和 std::random_access_iterator_tag, 而由于 std::random_access_iterator_tag 继承自 std::input_iterator_tag,对于随机访问迭代器, 会优先选择更为特化的 advanceIterImpl()变体(使用了 std::random_access_iterator_tag 的那 一个)。因此,标记派发依赖于将单一的主函数模板的功能委托给一组_impl 变体,这些变 体都被进行了标记,因此正常的函数重载机制会选择适用于特定模板参数的最为特化的版 本。

当被算法用到的特性具有天然的层次结构,并且存在一组为这些标记提供了值的萃取机制的 时候,标记派发可以很好的工作。而如果算法特化依赖于专有(ad hoc)类型属性的话(比 如依赖于类型 T 是否含有拷贝赋值运算符),标记派发就没那么方便了。对于这种情况,我 们需要一个更强大的技术。

20.3 Enable/Disable 函数模板

和 std::enable_if 一样,EnableIf 模板别名也可以被用来基于特定的条件 enable(或 disable)特 定的函数模板。比如,随机访问版本的 advanceIter()算法可以被实现成这样:

template<typename Iterator>
constexpr bool IsRandomAccessIterator =
IsConvertible< typename
std::iterator_traits<Iterator>::iterator_category,
std::random_access_iterator_tag>;
template<typename Iterator, typename Distance>
EnableIf<IsRandomAccessIterator<Iterator>>
advanceIter(Iterator& x, Distance n){
x += n; // constant time
}

EnableIf 包含两个参数,一个是标示着该模板是否应该被启用的 bool 型 条件参数,另一个是在第一个参数为 true 时,EnableIf 应该包含的类型。

template<bool, typename T = void>
struct EnableIfT {
};
template< typename T>
struct EnableIfT<true, T> {
using Type = T;
};
template<bool Cond, typename T = void>
using EnableIf = typename EnableIfT<Cond, T>::Type;

EnableIf 会扩展成一个类型,因此它被实现成了一个别名模板(alias template)。我们希望 为之使用偏特化(参见第 16 章),但是别名模板(alias template)并不能被偏特化。幸运 的是,我们可以引入一个辅助类模板(helper class template)EnableIfT,并将真正要做的工 作委托给它,而别名模板 EnableIf 所要做的只是简单的从辅助模板中选择结果类型。

当条件 是 true 的时候,EnableIfT::Type(也就是 EnableIf)的计算结果将是第二个模板参数 T。当条件是 false 的时候,EnableIf 不会生成有效的类型,因为主模板 EnableIfT 没有名为 Type 的成员。

通常这应该是一个错误,但是在 SFINAE(参见第 15.7 节)上下文中(比如函数模 板的返回类型),它只会导致模板参数推断失败,并将函数模板从待选项中移除。

我们可以将 EnableIf 理解成一种在模板参数不满足特定需 求的时候,防止模板被实例化的防卫手段。

现在我们已经可以显式的为特定的类型激活其所适用的更为特化的模板了。但是这还不够: 我们还需要“去激活(de-activate)”不够特化的模板,因为在两个模板都适用的时候,编 译期没有办法在两者之间做决断(order),从而会报出一个模板歧义错误。

幸运的是,实 现这一目的方法并不复杂:我们为不够特化的模板使用相同模式的 EnableIf,只是适用相反 的判断条件。这样,就可以确保对于任意 Iterator 类型,都只有一个模板会被激活。因此, 适用于非随机访问迭代器的 advanceIter()会变成下面这样:

template<typename Iterator, typename Distance>
EnableIf<!IsRandomAccessIterator<Iterator>>
advanceIter(Iterator& x, Distance n)
{
while (n > 0) {//linear time
++x;
--n;
}
}

20.3.1 提供多种特化版本

上述模式可以被继续泛化以满足有两种以上待选项的情况:可以为每一个待选项都配备一个 EnableIf,并且让它们的条件部分,对于特定的模板参数彼此互斥。这些条件部分通常会用 到多种可以用类型萃取(type traits)表达的属性。

比如,考虑另外一种情况,第三种 advanceIter()算法的变体:允许指定一个负的距离参数, 以让迭代器向“后”移动。很显然这对一个“输入迭代器(input itertor)”是不适用的,对 一个随机访问迭代器却是适用的。但是,标准库也包含一种双向迭代器(bidirectional iterator) 的概念,这一类迭代器可以向后移动,但却不要求必须同时是随机访问迭代器。实现这一情 况需要稍微复杂一些的逻辑:

每个函数模板都必须使用一个包含了在所有函数模板间彼此互 斥 EnableIf 条件,这些函数模板代表了同一个算法的不同变体。这样就会有下面一组条件:

 随机访问迭代器:适用于随机访问的情况(常数时间复杂度,可以向前或向后移动)

 双向迭代器但又不是随机访问迭代器:适用于双向情况(线性时间复杂度,可以向前或 向后移动)

 输入迭代器但又不是双向迭代器:适用于一般情况(线性时间复杂度,只能向前移动)

相关函数模板的具体实现如下:

#include <iterator>
// implementation for random access iterators:
template<typename Iterator, typename Distance>
EnableIf<IsRandomAccessIterator<Iterator>>
advanceIter(Iterator& x, Distance n) {
x += n; // constant time
}
template<typename Iterator>
constexpr bool IsBidirectionalIterator =
IsConvertible< typename
std::iterator_traits<Iterator>::iterator_category,
std::bidirectional_iterator_tag>;
// implementation for bidirectional iterators:
template<typename Iterator, typename Distance>
EnableIf<IsBidirectionalIterator<Iterator>
&& !IsRandomAccessIterator<Iterator>>
advanceIter(Iterator& x, Distance n) {
if (n > 0) {
for ( ; n > 0; ++x, --n) { //linear time
}
} else {
for ( ; n < 0; --x, ++n) { //linear time
}
}
}
// implementation for all other iterators:
template<typename Iterator, typename Distance>
EnableIf<!IsBidirectionalIterator<Iterator>>
advanceIter(Iterator& x, Distance n) {
if (n < 0) {
throw "advanceIter(): invalid iterator category for negative n";
}
while (n > 0) { //linear time
++x;
--n;
}
}

通过让每一个函数模板的 EnableIf 条件与其它所有函数模板的条件互相排斥,可以保证对于 一组参数,最多只有一个函数模板可以在模板参数推断中胜出。

上述例子已体现出通过 EnableIf 实现算法特化的一个缺点:每当一个新的算法变体被加入进 来,就需要调整所有算法变体的 EnableIf 条件,以使得它们之间彼此互斥。作为对比,当通 过标记派发(tag dispatching)引入一个双向迭代器的算法变体时,则只需要使用标记 std::bidirectional_iterator_tag 重载一个 advanceIterImpl()即可。

标记派发(tag dispatching)和 EnableIf 两种技术所适用的场景有所不同:一般而言,标记派 发可以基于分层的 tags 支持简单的派发,而 EnableIf 则可以基于通过使用类型萃取(type trait)获得的任意一组属性来支持更为复杂的派发。

20.3.2 EnableIf 所之何处(where does the EnableIf Go)?

EnableIf 通常被用于函数模板的返回类型。但是,该方法不适用于构造函数模板以及类型转 换模板,因为它们都没有被指定返回类型。而且,使用 EnableIf 也会使得返回类型很难被读 懂。对于这一问题,我们可以通过将 EnableIf 嵌入一个默认的模板参数来解决,比如:

#include <iterator>
#include "enableif.hpp"
#include "isconvertible.hpp"
template<typename Iterator>
constexpr bool IsInputIterator = IsConvertible< typename
std::iterator_traits<Iterator>::iterator_category,
std::input_iterator_tag>;
template<typename T>
class Container {
public:
// construct from an input iterator sequence:
template<typename Iterator, typename =
EnableIf<IsInputIterator<Iterator>>>
Container(Iterator first, Iterator last);
// convert to a container so long as the value types are convertible:
template<typename U, typename = EnableIf<IsConvertible<T, U>>>
operator Container<U>() const;
};

但是,这样做也有一个问题。如果我们尝试再添加一个版本的重载的话,会导致错误:

问题在于这两个模板唯一的区别是默认模板参数,但是在判断两个模板是否相同的时候却又 不会考虑默认模板参数。

  该问题可以通过引入另外一个模板参数来解决,这样两个构造函数模板就有数量不同的模板 参数了:

// construct from an input iterator sequence:
template<typename Iterator, typename =
EnableIf<IsInputIterator<Iterator>
&& !IsRandomAccessIterator<Iterator>>>
Container(Iterator first, Iterator last);
template<typename Iterator, typename =
EnableIf<IsRandomAccessIterator<Iterator>>, typename = int> // extra
dummy parameter to enable both constructors
Container(Iterator first, Iterator last); //OK now

20.3.3 编译期 if

值得注意的是,C++17 的 constexpr if 特性(参见第 8.5 节)使得某些情况下可以不再使用 EnableIf。

比如在 C++17 中可以像下面这样重写 advanceIter():

template<typename Iterator, typename Distance>
void advanceIter(Iterator& x, Distance n) {
if constexpr(IsRandomAccessIterator<Iterator>) {
// implementation for random access iterators:
x += n; // constant time
}else if constexpr(IsBidirectionalIterator<Iterator>) {
// implementation for bidirectional iterators:
if (n > 0)
{for ( ; n > 0; ++x, --n) { //linear time for positive n
}
} else {
for ( ; n < 0; --x, ++n) { //linear time for negative n
}
}
}else {
// implementation for all other iterators that are at least input iterators:
if (n < 0) {
throw "advanceIter(): invalid iterator category for negative n";
}
while (n > 0) { //linear time for positive n only
++x;
--n;
}
}
}

这样会更好一些。更为特化的代码分支只会被那些支持它们的类型实例化。因此,对于使用 了不被所有的迭代器都支持的代码的情况,只要它们被放在合适的 constexpr if 分支中,就 是安全的。

但是,该方法也有其缺点。只有在泛型代码组件可以被在一个函数模板中完整的表述时,这 一使用 constexpr if 的方法才是可能的。

在下面这些情况下,我们依然需要 EnableIf:

 需要满足不同的“接口”需求

 需要不同的 class 定义

 对于某些模板参数列表,不应该存在有效的实例化。

对于最后一种情况,下面这种做法看上去很有吸引力:

template<typename T>
void f(T p) {
if constexpr (condition<T>::value) {
// do something here…
}
else {
// not a T for which f() makes sense:
static_assert(condition<T>::value, "can’t call f() for such a T");
}
}

但是我们并不建议这样做,因为它对 SFINAE 不太友好:函数 f()并不会被从待选项列表中 移除,因此它有可能会屏蔽掉另一种重载解析结果。作为对比,使用 EnableIf f()则会在 EnableIf替换失败的时候将该函数从待选项列表中移除。

20.3.4 Concepts C++20

比如,我们可能希望被重载的 container 的构造函数可以像下面这样

template<typename T>
class Container {
public:
//construct from an input iterator sequence:
template<typename Iterator>
requires IsInputIterator<Iterator>
Container(Iterator first, Iterator last);
// construct from a random access iterator sequence:
template<typename Iterator>
requires IsRandomAccessIterator<Iterator>
Container(Iterator first, Iterator last);
// convert to a container so long as the value types are convertible:
template<typename U>
requires IsConvertible<T, U>
operator Container<U>() const;
};

20.4 类的特化(Class Specialization)

类模板的偏特化可以被用来提供一个可选的、为特定模板参数进行了特化的实现,这一点和 函数模板的重载很相像。而且,和函数模板的重载类似,如果能够基于模板参数的属性对各 种偏特化版本进行区分,也会很有意义。

20.4.1 启用/禁用类模板

启用/禁用类模板的不同实现方式的方法是使用类模板的偏特化。为了将 EnableIf 用于类模 板的偏特化,需要先为 Dictionary 引入一个未命名的、默认的模板参数:

template<typename Key, typename Value, typename = void>
class Dictionary
{ … //vector implementation as above
};

这个新的模板参数将是我们使用 EnableIf 的入口,现在它可以被嵌入到基于 map 的偏特化 Dictionary的模板参数例表中:

template<typename Key, typename Value>
class Dictionary<Key, Value, EnableIf<HasLess<Key>>>
{
private:
map<Key, Value> data;
public:value& operator[](Key const& key) {
return data[key];
}…
};

和函数模板的重载不同,我们不需要对主模板的任意条件进行禁用,因为对于类模板,任意 偏特化版本的优先级都比主模板高。但是,当我们针对支持哈希操作的另一组 keys 进行特 化时,则需要保证不同偏特化版本间的条件是互斥的:

template<typename Key, typename Value, typename = void>
class Dictionary
{ … // vector implementation as above
};
template<typename Key, typename Value>
class Dictionary<Key, Value, EnableIf<HasLess<Key> && !HasHash<Key>>> {
{ … // map implementation as above
};
template typename Key, typename Value>
class Dictionary Key, Value, EnableIf HasHash Key>>>
{
private:
unordered_map Key, Value> data;
public:
value& operator[](Key const& key) {
return data[key];
}…
};

20.4.2 类模板的标记派发

同样地,标记派发也可以被用于在不同的模板特化版本之间做选择。

,我 们定义一个类似于之前章节中介绍的 advanceIter()算法的函数对象类型 Advance, 它同样会以一定的步数移动迭代器。会同时提供基本实现(用于 input iterators)和适用于双 向迭代器和随机访问迭代器的特化版本,并基于辅助萃取 BestMatchInSet(下面会讲到)为 相应的迭代器种类选择最合适的实现版本:

// primary template (intentionally undefined):
template<typename Iterator,
typename Tag = BestMatchInSet< typename
std::iterator_traits<Iterator> ::iterator_category,
std::input_iterator_tag,
std::bidirectional_iterator_tag,
std::random_access_iterator_tag>>
class Advance;
// general, linear-time implementation for input iterators:
template<typename Iterator>
class Advance<Iterator, std::input_iterator_tag>
{
public:
using DifferenceType = typename
std::iterator_traits<Iterator>::difference_type;
void operator() (Iterator& x, DifferenceType n) const
{
while (n > 0) {
++x;
-n;
}
}
};
// bidirectional, linear-time algorithm for bidirectional iterators:
template<typename Iterator>
class Advance<Iterator, std::bidirectional_iterator_tag>
{
public:
using DifferenceType =typename
std::iterator_traits<Iterator>::difference_type;
void operator() (Iterator& x, DifferenceType n) const
{
if (n > 0) {
while (n > 0) {
++x;
--n;
}
} else {
while (n < 0) {
--x;
++n;
}
}
}
};
// bidirectional, constant-time algorithm for random access iterators:
template<typename Iterator>
class Advance<Iterator, std::random_access_iterator_tag>
{
public:
using DifferenceType =
typename std::iterator_traits<Iterator>::difference_type;
void operator() (Iterator& x, DifferenceType n) const
{
x += n;
}
}

这一实现形式和函数模板中的标记派发很相像。但是,比较困难的是 BestMatchInSet 的实现, 它主要被用来为一个给定的迭代器选择选择最匹配 tag。

本质上,这个类型萃取所做的是, 当给定一个迭代器种类标记的值之后,要判断出该从以下重载函数中选择哪一个,并返回其 参数类型:

void f(std::input_iterator_tag);
void f(std::bidirectional_iterator_tag);
void f(std::random_access_iterator_tag);

模拟重载解析最简单的方式就是使用重载解析,就像下面这样:

// construct a set of match() overloads for the types in Types…:
template<typename… Types>
struct MatchOverloads;
// basis case: nothing matched:
template<>
struct MatchOverloads<> {
static void match(…);
};
// recursive case: introduce a new match() overload:
template<typename T1, typename… Rest>
struct MatchOverloads<T1, Rest…> : public MatchOverloads<Rest…>
{
static T1 match(T1); // introduce overload for T1
using MatchOverloads<Rest…>::match;// collect overloads from bases
};
// find the best match for T in Types…
template<typename T, typename… Types>
struct BestMatchInSetT {
using Type = decltype(MatchOverloads<Types…>::match(declval<T> ()));
};
template<typename T, typename… Types>
using BestMatchInSet = typename BestMatchInSetT<T, Types…>::Type;

20.5 实例化安全的模板(Instantiation-Safe Templates)

EnableIf 技术的本质是:只有在模板参数满足某些条件的情况下才允许使用某个模板或者某 个偏特化模板。比如,最为高效的 advanceIter()算法会检查迭代器的参数种类是否可以被转 化成 std::random_access_iterator_tag,也就意味着各种各样的随机访问迭代器都适用于该算 法。

如果我们将这一概念发挥到极致,将所有模板用到的模板参数的操作都编码进 EnableIf 的条 件,会怎样呢?这样一个模板的实例化永远都不会失败,因为那些没有提供 EnableIf 所需操 作的模板参数会导致一个推断错误,而不是任由可能会出错的实例化继续进行。我们称这一 类模板为“实例化安全(instantiation-safe )”的模板,

先从一个计算两个数之间的最小值的简单模板 min()开始。我们可能会将其实现成下面这样:

template<typename T>
T const& min(T const& x, T const& y)
{
if (y < x) {
return y;
}
return x;
}

这个模板要求类型为 T 的两个值可以通过<运算符进行比较,并将比较结果转换成 bool 类型 给 if 语句使用。可以检查类型是否支持<操作符,并计算其返回值类型的类型萃取,在形式 上和我们第 19.4.4 节介绍的 SFINAE 友好的 PlusResultT 萃取类似。为了方便,我们此处依然 列出 LessResultT 的实现:

#include <utility> // for declval()
#include <type_traits> // for true_type and false_type
template<typename T1, typename T2>
class HasLess {
template<typename T> struct Identity;
template<typename U1, typename U2>
static std::true_type
test(Identity<decltype(std::declval<U1>() < std::declval<U2>())>*);
template<typename U1, typename U2>
static std::false_type
test(…);
public:
static constexpr bool value = decltype(test<T1, T2> (nullptr))::value;
};
template<typename T1, typename T2, bool HasLess>
class LessResultImpl {
public:
using Type = decltype(std::declval<T1>() < std::declval<T2>());
};
template<typename T1, typename T2>
class LessResultImpl<T1, T2, false> {
};
template<typename T1, typename T2>
class LessResultT
: public LessResultImpl<T1, T2, HasLess<T1, T2>::value> {
};
template<typename T1, typename T2>
using LessResult = typename LessResultT<T1, T2>::Type;

现在就可以通过将该萃取和 IsConvertible 一起使用,使 min()变成实例化安全的:

#include "isconvertible.hpp"
#include "lessresult.hpp"
template<typename T>
EnableIf<IsConvertible<LessResult<T const&, T const&>, bool>, T const&>
min(T const& x, T const& y)
{
if (y < x) {
return y;
}
return x;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/81133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

档案管理系统设计与实现

摘 要 近年来&#xff0c;随着企业彼此间的竞争日趋激烈&#xff0c;信息技术在企业的发展中占据着越来越重要的地位。在企业的运输生产中&#xff0c;档案已成为企业运输经营中不可或缺的一部分&#xff0c;为管理者进行管理决策和进行各种经营活动提供了重要的依据&#xf…

程序地址空间

✅<1>主页&#xff1a;&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;Linux——程序地址空间 ☂️<3>开发环境&#xff1a;Centos7 &#x1f4ac;<4>前言&#xff1a;我们一直随口就能说出来的栈区&#xff0c;堆区&#xff0c;常量…

2023-简单点-树莓派安装ncnn框架

not python 按照下面的步骤进行就可以了&#xff1a; 参考 tips: 其中有一步要用下面方法: 如果你的git clone不得行&#xff0c;可以按照以下操作方法&#xff1a; git clone --depth1 https://ghproxy.com/ https://github.com/Tencent/ncnn.git python 直接 pip install …

如何构建 Protocol Buffers(protobuf)并解决常见问题

简介 Protocol Buffers&#xff0c;通常称为protobuf&#xff0c;是一种用于序列化结构化数据的开源工具。它广泛用于数据交换&#xff0c;通常用于RPC&#xff08;远程过程调用&#xff09;和持久化数据存储。在本文中&#xff0c;我们将介绍如何构建protobuf&#xff0c;并解…

基于Java的大学生选修选课系统设计与实现(亮点:多角色、贴近现实的选课流程、好看的系统外观)

大学生选修选课系统 一、前言二、我的优势2.1 自己的网站2.2 自己的小程序&#xff08;小蔡coding&#xff09;2.3 有保障的售后2.4 福利 三、开发环境与技术3.1 MySQL数据库3.2 Vue前端技术3.3 Spring Boot框架3.4 微信小程序 四、功能设计4.1 主要功能描述 五、系统实现5.1 管…

【基础篇】ClickHouse 表引擎详解

文章目录 0. 引言1. 什么是表引擎2. 不同表引擎使用场景1. MergeTree:2. Log:3. Memory:4. Distributed:5. Kafka:6. MaterializedView:7. File和URL: 3. MergeTree 家族3.1. MergeTree:3.2. ReplacingMergeTree:3.3. SummingMergeTree:3.4. AggregatingMergeTree:3.5. Collaps…

理解HTTPS/TLS/SSL(二)可视化TLS握手过程并解密加密数据

文章目录 WireShark抓包TLS握手过程Client HelloServer HelloEncryped Extenstions, Certificate, Certificate VerifyChange Ciper Spec, FinshedTLS 1.2和TLS 1.3的区别能不能在进一步&#xff1f; 解密WireShark中抓到的TLS包参考资料 上一篇文章已经在本地使用了生成自签名…

[npm] npx 介绍与使用说明

[npm] npx 介绍与使用说明 npm 的由来npx 是什么&#xff1f;npx 特点npx 的特点项目安装包的使用全局安装包的避免指定工具包版本--no-install 参数和--ignore-existing 参数使用不同版本的 node-p 参数-c 参数实战应用 执行 GitHub 源码 npm 的由来 说到 npm 就离不开社区文…

【Linux操作系统】信号的产生捕获

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ Linux       &#x1f6f0;️社区 :✈️ 进步学堂       &#x1f6f0…

css经典面试题(二)

文章目录 1、清除浮动2、opacity: 0、visibility: hidden、display: none 的区别3、css画一个三角形4、常见的主流浏览器前缀5、重绘与重排的区别&#xff1f;6、如何优化图片7、CSS3 中 transition 和 animation 的属性分别有哪些8、居中为什么要使用 transform&#xff08;为…

The driver has not received any packets from the server

在测试数据迁移时遇到的错误。 目录 一、错误 二、解决 三、数据迁移测试 3.1 环境 3.2 源码及测试 3.2.1 源码 3.2.2 测试结果&#xff08;太慢&#xff09; 3.2.3 源码修改 3.2.4 异常及解决 一、错误 The driver has not received any packets from the server. 二…

数学建模——微分方程介绍

一、基础知识 1、一阶微分方程 称为一阶微分方程。y(x0)y0为定解条件。 其常规求解方法&#xff1a; &#xff08;1&#xff09;变量分离 再两边积分就可以求出通解。 &#xff08;2&#xff09;一阶线性求解公式 通解公式&#xff1a; 有些一阶微分方程需要通过整体代换…

四种常用的自动化测试框架

一直想仔细研究框架&#xff0c;写个流水账似的测试程序不难&#xff0c;写个低维护成本的测试框架就很难了&#xff0c;所以研究多种测试框架还是很有必要的&#xff0c;知道孰优孰劣&#xff0c;才能在开始编写框架的时候打好基础&#xff0c;今天读到了KiKi Zhao的翻译文章&…

Java实现Ip地址获取

Java实现Ip地址获取 一、两种实现方式二、测试结果 一、两种实现方式 package com.lyp;import org.apache.commons.lang3.ObjectUtils;import java.net.*; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Optional;/***…

Linux Ubuntu20.04深度学习环境快速配置命令记录

一、驱动安装 1、更新系统包 sudo apt-get updatesudo apt-get upgrade 2、安装显卡驱动 使用apt方式安装驱动&#xff0c;多数情况不容易成功&#xff0c; 使用一下方法更佳&#xff1a; 1.查看合适显卡的驱动版本 ubuntu-drivers devices NVIDIA GeForce 驱动程序 - …

git压缩仓库

git 压缩仓库 git gc命令压缩增量存储单元,节省磁盘空间 du -sh 查看当前文件夹占用多少K 快照的存储: 对于修改的内容,做快照处理并保存. 对于未修改的文件,做引用处理.

SOLIDWORKS Composer位置关键帧的使用

SOLIDWORKS Composer是专业的SOLIDWORKS及3D文件处理的动画制作软件&#xff0c;作为SOLIDWORKS 产品线下的一个明星存在。 SOLIDWORKS Composer几乎可以处理任何SOLIDWORKS的模型文件并将之转化成可以动作的机械动画&#xff0c;可以引用在企业的网站、产品说明书以及工作指导…

MySQL 面试题——MySQL 基础

目录 1.什么是 MySQL&#xff1f;有什么优点&#xff1f;2.MySQL 中的 DDL 与 DML 是分别指什么&#xff1f;3.✨数据类型 varchar 与 char 有什么区别&#xff1f;4.数据类型 BLOB 与 TEXT 有什么区别&#xff1f;5.DATETIME 和 TIMESTAMP 的异同&#xff1f;6.✨MySQL 中 IN …

Golang开发--计时器(Timer)和定时器(Ticker)

计时器&#xff08;Timer&#xff09; 在 Go 中&#xff0c;可以使用 time 包提供的计时器&#xff08;Timer&#xff09;来执行定时任务。计时器允许你在指定的时间间隔后执行某个操作。 time.Timer结构表示一个计时器&#xff0c;它会在指定的时间段后触发单次操作。 创建计…

STM32F4X SPI W25Q128

STM32F4X SPI W25Q128 什么是SPISPI的特点SPI通信引脚SPI接线方式SPI速率SPI通信方式SPI时钟相位和时钟极性 STM32F4X SPISTM32F4X SPI配置STM32F4X SPI频率 W25Q128W25Q128存储结构W25Q128读写操作W25Q128常用指令读取ID命令(0x90)写使能命令(0x06)禁止写使能命令(0x04)读取W2…