c++ template-3

第 7 章 按值传递还是按引用传递

        从一开始,C++就提供了按值传递(call-by-value)和按引用传递(call-by-reference)两种参数传递方式,但是具体该怎么选择,有时并不容易确定:通常对复杂类型用按引用传递的成本更低,但是也更复杂。C++11 又引入了移动语义(move semantics),也就是说又多了一种按引用传递的方式:
1. X const &(const 左值引用)
参数引用了被传递的对象,并且参数不能被更改。
2. X &(非 const 左值引用)
参数引用了被传递的对象,但是参数可以被更改。
3. X &&(右值引用)
参数通过移动语义引用了被传递的对象,并且参数值可以被更改或者被“窃取”。仅仅对已知的具体类型,决定参数的方式就已经很复杂了。在参数类型未知的模板中,就更难选择合适的传递方式了。
不过在 1.6.1 节中,我们曾经建议在函数模板中应该优先使用按值传递,除非遇到以下情况: 对象不允许被 copy。

  • 参数被用于返回数据。
  • 参数以及其所有属性需要被模板转发到别的地方。
  • 可以获得明显的性能提升。

        本章将讨论模板中传递参数的几种方式,并将证明为何应该优先使用按值传递,也列举了不该使用按值传递的情况。同时讨论了在处理字符串常量和裸指针时遇到的问题。在阅读本章的过程中,最好先够熟悉下附录 B 中和数值分类有关的一些术语(lvalue,rvalue,prvalue,xvalue)。

7.1 按值传递

        当按值传递参数时,原则上所有的参数都会被拷贝。因此每一个参数都会是被传递实参的一份拷贝。对于 class 的对象,参数会通过 class 的拷贝构造函数来做初始化。调用拷贝构造函数的成本可能很高。

        但是有多种方法可以避免按值传递的高昂成本:事实上编译器可以通过移动语义(move semantics)来优化掉对象的拷贝,这样即使是对复杂类型的拷贝,其成本也不会很高。
        比如下面这个简单的按值传递参数的函数模板: 

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void printV(T arg) {


}

int main() {
  std::string returnString();
  std::string s = "hi";
  printV(s); //copy constructor
  printV(std::string("hi")); //copying usually optimized away (if not,move constructor)
  printV(returnString()); // copying usually optimized away (if not, moveconstructor)
  printV(std::move(s)); // move constructor
  return 0;
}

        在第一次调用中,被传递的参数是左值(lvalue),因此拷贝构造函数会被调用。

        但是在第二和第三次调用中,被传递的参数是纯右值(prvalue,pure right value,临时对象或者某个函数的返回值,参见附录 B),此时编译器会优化参数传递,使得拷贝构造函数不会被调用。从 C++17 开始,C++标准要求这一优化方案必须被实现。在 C++17 之前,如果编译器没有优化掉这一类拷贝,它至少应该先尝试使用移动语义,这通常也会使拷贝成本变得比较低廉。

        在最后一次调用中,被传递参数是 xvalue(一个使用了 std::move()的已经存在的非const 对象),这会通过告知编译器我们不在需要 s 的值来强制调用移动构造函数(move constructor)。

        综上所述,在调用 printV()(参数是按值传递的)的时候,只有在被传递的参数是lvalue(对象在函数调用之前创建,并且通常在之后还会被用到,而且没有对其使用std::move())时, 调用成本才会比较高。不幸的是,这唯一的情况也是最常见的情况,因为我们几乎总是先创建一个对象,然后在将其传递给其它函数

按值传递会导致类型退化(decay)

        关于按值传递,还有一个必须被讲到的特性:当按值传递参数时,参数类型会退化(decay)。也就是说,裸数组会退化成指针,const 和 volatile 等限制符会被删除(就像用一个值去初始化一个用 auto

        

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void printV(T arg) {


}

int main() {
  std::string const c = "hi";
  printV(c); // c decays so that arg has type std::string
  printV("hi"); //decays to pointer so that arg has type char const*int arr[4];
  int arr[4];
  printV(arr); // decays to pointer so that arg has type int *
  return 0;
}

        当传递字符串常量“hi”的时候,其类型 char const[3]退化成 char const *,这也就是模板参数 T 被推断出来的类型。此时模板会被实例化成: 

void printV (char const* arg)
{ …
}

        这一行为继承自 C 语言,既有优点也有缺点。通常它会简化对被传递字符串常量的处理,但是缺点是在 printV()内部无法区分被传递的是一个对象的指针还是一个存储一组对象的数组。在 7.4 节将专门讨论如何应对字符串常量和裸数组的问题。 

 7.2 按引用传递

        现在来讨论按引用传递。按引用传递不会拷贝对象(因为形参将引用被传递的实参)。而且,按引用传递时参数类型也不会退化(decay)。不过,并不是在所有情况下都能使用按引用传递,即使在能使用的地方,有时候被推断出来的模板参数类型也会带来不少问题。

7.2.1 按 const 引用传递

        为了避免(不必要的)拷贝,在传递非临时对象作为参数时,可以使用const 引用传递。

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void printR(T const& arg) {
}

int main() {
  std::string returnString();
  std::string s = "hi";
  printR(s); // no copy
  printR(std::string("hi")); // no copy
  printR(std::move(s)); // no copy

  int i = 42;
  printR(i); // passes reference instead of just copying i
  return 0;
}

         这个模板永远不会拷贝被传递对象(不管拷贝成本是高还是低)

        即使是按引用传递一个 int 类型的变量,虽然这样可能会事与愿违(不会提高性能,见下段中的解释),也依然不会拷贝。

        这样做之所以不能提高性能,是因为在底层实现上,按引用传递还是通过传递参数的地址实现的。地址会被简单编码,这样可以提高从调用者向被调用者传递地址的效率。不过按地址传递可能会使编译器在编译调用者的代码时有一些困惑:被调用者会怎么处理这个地址?理论上被调用者可以随意更改该地址指向的内容。这样编译器就要假设在这次调用之后,所有缓存在寄存器中的值可能都会变为无效。而重新载入这些变量的值可能会很耗时(可能比拷贝对象的成本高很多)。你或许会问在按 const 引用传递参数时:为什么编译器不能推断出被调用者不会改变参数的值?不幸的是,确实不能,因为调用者可能会通过它自己的非const 引用修改被引用对象的值(这个解释太好,另一种情况是被调用者可以通过const_cast 移除参数中的 const)。 

        不过对可以 inline 的函数,情况可能会好一些:如果编译器可以展开inline 函数,那么它就可以基于调用者和被调用者的信息,推断出被传递地址中的值是否会被更改。函数模板通常总是很短,因此很可能会被做 inline 展开。但是如果模板中有复杂的算法逻辑,那么它大概率就不会被做 inline 展开了。

按引用传递不会做类型退化(decay)  

        按引用传递参数时,其类型不会退化(decay)。也就是说不会把裸数组转换为指针,也不会移除 const 和 volatile 等限制符。而且由于调用参数被声明为 T const &,被推断出来的模板参数 T 的类型将不包含 const。比如:

std::string const c = "hi";
printR(c); // T deduced as std::string, arg is std::string const&printR("hi"); // T deduced as char[3], arg is char const(&)[3]
int arr[4];
printR(arr); // T deduced as int[4], arg is int const(&)[4]

因此对于在 printR()中用 T 声明的变量,它们的类型中也不会包含 const。

7.2.2 按非 const 引用传递

        如果想通过调用参数来返回变量值(比如修改被传递变量的值),就需要使用非const 引用(要么就使用指针)。同样这时候也不会拷贝被传递的参数。被调用的函数模板可以直接访问被传递的参数。 考虑如下情况:

template<typename T>
void outR(T& arg) {
}

        注意对于 outR(),通常不允许将临时变量(prvalue)或者通过 std::move()处理过的已存在的变量(xvalue)用作其参数:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void outR(T& arg) {
}

int main() {
  std::string returnString();
  std::string s = "hi";
  outR(s); //OK: T deduced as std::string, arg is std::string&
  outR(std::string("hi")); //ERROR: not allowed to pass a temporary(prvalue)
  outR(returnString()); // ERROR: not allowed to pass a temporary(prvalue)
  outR(std::move(s)); // ERROR: not allowed to pass an xvalue
  return 0;
}

同样可以传递非 const 类型的裸数组,其类型也不会 decay:

int arr[4];
outR(arr); // OK: T deduced as int[4], arg is int(&)[4]

这样就可以修改数组中元素的值,也可以处理数组的长度。比如:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void outR(T& arg) {
  if (std::is_array<T>::value) {
    std::cout << "got array of " << std::extent<T>::value << " elems\n";
  }
}

int main() {
  int arr[4];
  outR(arr); // OK: T deduced as int[4], arg is int(&)[4]
  return 0;
}

        但是在这里情况有一些复杂。此时如果传递的参数是 const 的,arg 的类型就有可能被推断为 const 引用,也就是说这时可以传递一个右值(rvalue)作为参数,但是模板所期望的参数类型却是左值(lvalue):

std::string const c = "hi";
outR(c); // OK: T deduced as std::string const
outR(returnConstString()); // OK: same if returnConstString() returnsconst string
outR(std::move(c)); // OK: T deduced as std::string const6
outR("hi"); // OK: T deduced as char const[3]

        在这种情况下,在函数模板内部,任何试图更改被传递参数的值的行为都是错误的。在调用表达式中也可以传递一个 const 对象,但是当函数被充分实例化之后(可能发生在接接下来的编译过程中),任何试图更改参数值的行为都会触发错误(但是这有可能发生在被调用模板的很深层次逻辑中,具体细节请参见 9.4 节)。

         如果想禁止想非 const 应用传递 const 对象,有如下选择:

        可以将任意类型的参数传递给转发引用,而且和往常的按引用传递一样,都不会创建被传递参数的备份:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void passR(T&& arg) {
  if (std::is_array<T>::value) {
    std::cout << "got array of " << std::extent<T>::value << " elems\n";
  }
}

int main() {
  std::string s = "hi";
  passR(s); // OK: T deduced as std::string& (also the type of arg)
  passR(std::string("hi")); // OK: T deduced as std::string, arg is std::string&&
  passR(std::string()); // OK: T deduced as std::string, arg is std::string&&
  passR(std::move(s)); // OK: T deduced as std::string, arg is std::string&&
  int arr[4];
  passR(arr); // OK: T deduced as int(&)[4] (also
  return 0;
}

         但是,这种情况下类型推断的特殊规则可能会导致意想不到的结果:

        看上去将一个参数声明为转发引用总是完美的。但是,没有免费的午餐。比如,由于转发引用是唯一一种可以将模板参数 T 隐式推断为引用的情况,此时如果在模板内部直接用 T 声明一个未初始化的局部变量,就会触发一个错误(引用对象在创建的时候必须被初始化):

template<typename T>
void passR(T&& arg) {
  T x;
}
int main() {
  passR(42); // OK: T deduced as int
  int i;
  passR(i); // ERROR: T deduced as int&, which makes the declaration ofxin passR() invalid
  return 0;
}

??? 没看懂问题

7.3 使用 std::ref()和 std::cref() (限于模板)

        从 C++11 开始,可以让调用者自行决定向函数模板传递参数的方式。如果模板参数被声明成按值传递的,调用者可以使用定义在头文件中的 std::ref()和std::cref()将参数按引用传递给函数模板。比如

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

template<typename T>
void printT(T arg) {
}

int main() {
  std::string s = "hello";
  printT(s); //pass s By value
  printT(std::cref(s)); // pass s “as if by reference”
  return 0;
}

7.4 处理字符串常量和裸数组

到目前为止,我们看到了将字符串常量和裸数组用作模板参数时的不同效果:

  •  按值传递时参数类型会 decay,参数类型会退化成指向其元素类型的指针。
  • 按引用传递是参数类型不会 decay,参数类型是指向数组的引用。

        两种情况各有其优缺点。将数组退化成指针,就不能区分它是指向对象的指针还是一个被传递进来的数组。另一方面,如果传递进来的是字符串常量,那么类型不退化的话就会带来问题,因为不同长度的字符串的类型是不同的。比如:

        这里 foo(“hi”, “guy”)不能通过编译,因为”hi”的类型是 char const [3],而”guy”的类型是char const [4],但是函数模板要求两个参数的类型必须相同。

        这种 code 只有在两个字符串常量的长度相同时才能通过编译。因此,强烈建议在测试代码中使用长度不同的字符串。

        如果将 foo()声明成按值传递的,这种调用可能可以正常运行: 

        但是这样并不能解决所有的问题。反而可能会更糟,编译期间的问题可能会变为运行期间的问题

7.4.1 关于字符串常量和裸数组的特殊实现

        有时候可能必须要对数组参数和指针参数做不同的实现。此时当然不能退化数组的类型。

        为了区分这两种情况,必须要检测到被传递进来的参数是不是数组。

        通常有两种方法:

 可以将模板定义成只能接受数组作为参数:

template<typename T, std::size_t L1, std::size_t L2>
void foo(T (&arg1)[L1], T (&arg2)[L2])
{
T* pa = arg1; // decay arg1
T* pb = arg2; // decay arg2
if (compareArrays(pa, L1, pb, L2)) { …
}
}

参数 arg1 和 arg2 必须是元素类型相同、长度可以不同的两个数组。但是为了支持多种不同类型的裸数组,可能需要更多实现方式(参见 5.4 节)。

可以使用类型萃取来检测参数是不是一个数组:

template<typename T, typename =
std::enable_if_t<std::is_array_v<T>>>
void foo (T&& arg1, T&& arg2)
{ …
}

        由于这些特殊的处理方式过于复杂,最好还是使用一个不同的函数名来专门处理数组参数。或者更近一步,让模板调用者使用 std::vector 或者 std::array 作为参数。但是只要字符串还是裸数组,就必须对它们进行单独考虑

7.5 处理返回值

将返回类型声明为 auto,从而让编译器去推断返回类型,这是因为auto 也会导致类型退化

template<typename T>
auto retV(T p) // by-value return type deduced by compiler
{
return T{…}; // always returns by value
}

7.6 关于模板参数声明的推荐方法

正如前几节介绍的那样,函数模板有多种传递参数的方式:

将参数声明成按值传递:
        这一方法很简单,它会对字符串常量和裸数组的类型进行退化,但是对比较大的对象可能会受影响性能。在这种情况下,调用者仍然可以通过 std::cref()和 std::ref()按引用传递参数,但是要确保这一用法是有效的。
将参数声明成按引用传递:
        对于比较大的对象这一方法能够提供比较好的性能。尤其是在下面几种情况下:

  • 将已经存在的对象(lvalue)按照左值引用传递, 
  • 将临时对象(prvalue)或者被 std::move()转换为可移动的对象(xvalue)按右值引用传递,
  • 或者是将以上几种类型的对象按照转发引用传递。

由于这几种情况下参数类型都不会退化,因此在传递字符串常量和裸数组时要格外小心。对于转发引用,需要意识到模板参数可能会被隐式推断为引用类型(引用折叠)。

一般性建议

        基于以上介绍,对于函数模板有如下建议: 1. 默认情况下,将参数声明为按值传递。这样做比较简单,即使对字符串常量也可以正常工作。对于比较小的对象、临时对象以及可移动对象,其性能也还不错。对于比较大的对象,为了避免成本高昂的拷贝,可以使用 std::ref()和 std::cref()

        2. 如果有充分的理由,也可以不这么做:

  • 如果需要一个参数用于输出,或者即用于输入也用于输出,那么就将这个参数按非const 引用传递。但是需要按照 7.2.2 节介绍的方法禁止其接受 const 对象。
  • 如果使用模板是为了转发它的参数,那么就使用完美转发(perfect forwarding)。也就是将参数声明为转发引用并在合适的地方使用 std::forward<>()。考虑使用std::decay<>或者 std::common_type<>来处理不同的字符串常量类型以及裸数组类型的情况。
  • 如果重点考虑程序性能,而参数拷贝的成本又很高,那么就使用const 引用。不过如果最终还是要对对象进行局部拷贝的话,这一条建议不适用。

3. 如果你更了解程序的情况,可以不遵循这些建议。但是请不要仅凭直觉对性能做评估。在这方面即使是程序专家也会犯错。真正可靠的是:测试结果

不要过分泛型化

        值得注意的是,在实际应用中,函数模板通常并不是为了所有可能的类型定义的。而是有一定的限制。比如你可能已经知道函数模板的参数只会是某些类型的 vector。这时候最好不要将该函数模板定义的过于泛型化,否则,可能会有一些令人意外的副作用。针对这种情况应该使用如下的方式定义模板:

template<typename T>
void printVector (std::vector<T> const& v)
{ …
}

        这里通过的参数 v,可以确保 T 不会是引用类型,因为 vector 不能用引用作为其元素类型。而且将 vector 类型的参数声明为按值传递不会有什么好处,因为按值传递一个vector 的成本明显会比较高昂(vector 的拷贝构造函数会拷贝 vector 中的所有元素)。此处如果直接将参数 v 的类型声明为 T,就不容易从函数模板的声明上看出该使用那种传递方式了。

以 std::make_pair<>为例

        std::make_pair<>()是一个很好的介绍参数传递机制相关陷阱的例子。使用它可以很方便的通过类型推断创建 std::pair<>对象。它的定义在各个版本的 C++中都不一样:

         在第一版 C++标准 C++98 中,std::make_pair<>被定义在 std 命名空间中,并且使用按引用传递来避免不必要的拷贝:

template<typename T1, typename T2>
pair<T1,T2> make_pair (T1 const& a, T2 const& b)
{
return pair<T1,T2>(a,b);
}

        但是当使用 std::pair<>存储不同长度的字符串常量或者裸数组时,这样做会导致严重的问题。

        因此在 C++03 中,该函数模板被定义成按值传递参数

template<typename T1, typename T2>
pair<T1,T2> make_pair (T1 a, T2 b)
{
return pair<T1,T2>(a,b);
}

        不过在 C++11 中,由于 make_pair<>()需要支持移动语义,就必须使用转发引用。因此,其定义大体上是这样:

template<typename T1, typename T2>
constexpr pair<typename decay<T1>::type, typename
decay<T2>::type>
make_pair (T1&& a, T2&& b)
{
return pair<typename decay<T1>::type, typename
decay<T2>::type>(forward<T1>(a), forward<T2>(b));
}

        完 整 的 实 现 还 要 复 杂 的 多 : 为 了 支 持 std::ref() 和 std::cref() ,该函数会将std::reference_wrapper 展开成真正的引用。 目前 C++标准库在很多地方都使用了类似的方法对参数进行完美转发,而且通常都会结合std::decay<>使用

7.7 总结

 最好使用不同长度的字符串常量对模板进行测试。
 模板参数的类型在按值传递时会退化,按引用传递则不会。
 可以使用 std::decay<>对按引用传递的模板参数的类型进行退化。 在某些情况下,对被声明成按值传递的函数模板,可以使用 std::cref()和std::ref()将参数按引用进行传递。
 按值传递模板参数的优点是简单,但是可能不会带来最好的性能。 除非有更好的理由,否则就将模板参数按值传递。
 对于返回值,请确保按值返回(这也意味着某些情况下不能直接将模板参数直接用于返回类型)。
 在比较关注性能时,做决定之前最好进行实际测试。不要相信直觉,它通常都不准确

第 8 章 编译期编程

8.1 模板元编程

        模板的实例化发生在编译期间(而动态语言的泛型是在程序运行期间决定的)。事实证明C++模板的某些特性可以和实例化过程相结合,这样就产生了一种 C++自己内部的原始递归的“编程语言”。因此模板可以用来“计算一个程序的结果”。第 23 章会对这些特性进行全面介绍,这里通过一个简单的例子来展示它们的用处。

判断一个数是不是质数

下面的代码在编译期间就能判断一个数是不是质数:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

// p: number to check, d: current divisor
template<unsigned p, unsigned d>
struct DoIsPrime {
  static constexpr bool value = (p % d != 0) && DoIsPrime < p, d - 1 >::value;
};

// end recursion if divisor is 2
template<unsigned p>
struct DoIsPrime<p, 2> {
  static constexpr bool value = (p % 2 != 0);
};

// primary template
template<unsigned  n>
struct IsPrime {
  // start recursion with divisor from p/2:
  static constexpr bool value = DoIsPrime < n, n / 2 >::value;
};

// special cases (to avoid endless recursion with template instantiation):
template<>
struct IsPrime<0> { static constexpr bool value = false; };
template<>
struct IsPrime<1> { static constexpr bool value = false; };
template<>
struct IsPrime<2> { static constexpr bool value = true; };
template<>
struct IsPrime<3> { static constexpr bool value = true; };

int main() {
  std::cout << IsPrime<9>::value;
  return 0;
}

        IsPrime<>模板将结果存储在其成员 value 中。为了计算出模板参数是不是质数,它实例化了DoIsPrime<>模板,这个模板会被递归展开,以计算 p 除以 p/2 和 2 之间的数之后是否会有余数。

        正如以上实例化过程展现的那样:

  • 我们通过递归地展开 DoIsPrime<>来遍历所有介于 p/2 和 2 之间的数,以检查是否有某个数可以被 p 整除。
  • 用 d 等于 2 偏特例化出来的 DoIsPrime<>被用于终止递归调用。但是以上过程都是在编译期间进行的。 

8.2 通过 constexpr 进行计算

        在 C++14 中,constexpr 函数可以使用常规 C++代码中大部分的控制结构。因此为了判断一个数是不是质数,可以不再使用笨拙的模板方式(C++11 之前)以及略显神秘的单行代码方式

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

constexpr bool IsPrime(unsigned int n) {
  for (unsigned int d = 2; d <= n / 2; ++d) {
    if (n % d == 0) {
      return false;
    }
  }

  return n > 1;
}

int main() {
  constexpr bool b1 = IsPrime(9);
  return 0;
}

         但是上面所说的“可以”在编译期执行,并不是一定会在编译期执行。在需要编译期数值的上下文中(比如数组的长度和非类型模板参数),编译器会尝试在编译期对被调用的 constexpr 函数进行计算,此时如果无法在编译期进行计算,就会报错(因为此处必须要产生一个常量)。

        在其他上下文中,编译期可能会也可能不会尝试进行编译期计算,如果在编译期尝试了,但是现有条件不满足编译期计算的要求,那么也不会报错,相应的函数调用被推迟到运行期间执行。         比如:

constexpr bool b1 = isPrime(9); // evaluated at compile time

        会在编译期进行计算(因为 b1 被 constexpr 修饰)。而对

const bool b2 = isPrime(9); // evaluated at compile time if in namespacescope

        如果 b2 被定义于全局作用域或者 namespace 作用域,也会在编译期进行计算。如果b2 被定义于块作用域({}内),那么将由编译器决定是否在编译期间进行计算。下面这个例子就属于这种情况:

bool fiftySevenIsPrime() {
return isPrime(57); // evaluated at compile or running time
}

此时是否进行编译期计算将由编译期决定。

另一方面,在如下调用中:

int x;

std::cout << isPrime(x); // evaluated at run time

不管 x 是不是质数,调用都只会在运行期间执行

8.3 通过部分特例化进行路径选择

诸如 isPrime()这种在编译期进行相关测试的功能,有一个有意思的应用场景:可以在编译期间通过部分特例化在不同的实现方案之间做选择。

比如,可以以一个非类型模板参数是不是质数为条件,在不同的模板之间做选择:

#include <utility>
#include <string>
#include <iostream>
#include <type_traits>

constexpr bool IsPrime(unsigned int n) {
  for (unsigned int d = 2; d <= n / 2; ++d) {
    if (n % d == 0) {
      return false;
    }
  }

  return n > 1;
}


// primary helper template:
template<int SZ, bool = IsPrime(SZ)>
struct Helper;
// implementation if SZ is not a prime number:
template<int SZ>
struct Helper<SZ, false> {

};
// implementation if SZ is a prime number:
template<int SZ>
struct Helper<SZ, true> {

};


int main() {
  Helper<9> h;
  return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用springAI实现图片相识度搜索

类似的功能&#xff1a;淘宝拍照识别商品。图片相识度匹配 实现方式&#xff1a;其实很简单&#xff0c;用springai 将图片转换为向量数据&#xff0c;然后搜索就是先把需要搜索的图片转位向量再用向量数据去向量数据库搜索 但是springai现在不支持多模态嵌入数据库。做了一些…

私有化部署DeepSeek并SpringBoot集成使用(附UI界面使用教程-支持语音、图片)

私有化部署DeepSeek并SpringBoot集成使用&#xff08;附UI界面使用教程-支持语音、图片&#xff09; windows部署ollama Ollama 是一个开源框架&#xff0c;专为在本地机器上便捷部署和运行大型语言模型&#xff08;LLM&#xff09;而设计 下载ollama 下载地址&#xff08;…

半导体制造工艺讲解

目录 一、半导体制造工艺的概述 二、单晶硅片的制造 1.单晶硅的制造 2.晶棒的切割、研磨 3.晶棒的切片、倒角和打磨 4.晶圆的检测和清洗 三、晶圆制造 1.氧化与涂胶 2.光刻与显影 3.刻蚀与脱胶 4.掺杂与退火 5.薄膜沉积、金属化和晶圆减薄 6.MOSFET在晶圆表面的形…

正则表达式的简单介绍 + regex_match使用

正则表达式 正则表达式&#xff08;Regular Expression&#xff0c;简称 regex&#xff09;是一种用于匹配字符串的模式。它由一系列字符和特殊符号组成&#xff0c;用于描述、匹配一系列符合某个句法规则的字符串。正则表达式广泛应用于文本搜索、替换、验证等场景。 它的主…

AnythingLLM开发者接口API测试

《Win10OllamaAnythingLLMDeepSeek构建本地多人访问知识库》见上一篇文章&#xff0c;本文在上篇基础上进行。 1.生成本地API 密钥 2.打开API测试页面&#xff08;http://localhost:3001/api/docs/&#xff09; 就可以在页面测试API了 2.测试获取用户接口(/v1/admin/users) 3…

TypeScript 中的类:面向对象编程的基础

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

二级C语言题解:矩阵主、反对角线元素之和,二分法求方程根,处理字符串中 * 号

目录 一、程序填空&#x1f4dd; --- 矩阵主、反对角线元素之和 题目&#x1f4c3; 分析&#x1f9d0; 二、程序修改&#x1f6e0;️ --- 二分法求方程根 题目&#x1f4c3; 分析&#x1f9d0; 三、程序设计&#x1f4bb; --- 处理字符串中 * 号 题目&#x1f…

Qt 支持的动画格式对比,Lottie/APNG/GIF/WEBP

Qt版本&#xff1a;6.7.2 &#xff0c; QML 一&#xff0c;Lottie 在qml中使用LottieAnimation即可&#xff0c;但有三个问题&#xff1a; 1.动画加载中报错&#xff1a; 如果图片&#xff08;.json)本身存在不支持的effect 或shape type等&#xff0c;效果并不好&#xff1a…

SpringCloud - Nacos注册/配置中心

前言 该博客为Nacos学习笔记&#xff0c;主要目的是为了帮助后期快速复习使用 学习视频&#xff1a;7小快速通关SpringCloud 辅助文档&#xff1a;SpringCloud快速通关 一、简介 Nacos官网&#xff1a;https://nacos.io/docs/next/quickstart/quick-start/ Nacos /nɑ:kəʊ…

老游戏回顾:TL2

TL2是一部ARPG游戏&#xff0c;是TL的续作游戏&#xff0c;由位于美国西雅图的Runic Games开发&#xff0c;游戏于2012年9月20日上市&#xff0c;简体中文版于2013年4月10日在国内上市。 2有非常独特的艺术风格&#xff0c;这些在1中就已经形成&#xff0c;经过升级将使这款游…

DeepSeek-R1 云环境搭建部署流程

DeepSeek横空出世&#xff0c;在国际AI圈备受关注&#xff0c;作为个人开发者&#xff0c;AI的应用可以有效地提高个人开发效率。除此之外&#xff0c;DeepSeek的思考过程、思考能力是开放的&#xff0c;这对我们对结果调优有很好的帮助效果。 DeepSeek是一个基于人工智能技术…

利用ETL工具进行数据挖掘

ETL的基本概念 数据抽取&#xff08;Extraction&#xff09;&#xff1a;从不同源头系统中获取所需数据的步骤。比如从mysql中拿取数据就是一种简单的抽取动作&#xff0c;从API接口拿取数据也是。 数据转换&#xff08;Transformation&#xff09;&#xff1a;清洗、整合和转…

k8s网络插件及基础命令

一、k8s的cni网络插件 1.k8s的内部网络模式 pod内的容器与容器之间的通信。一个节点上的pod之间的通信&#xff0c;docker0网桥直接通信。不同节点上的pod之间的通信&#xff1a;通过物理网卡的ip地址和其他节点上的物理网卡的设备进行通信&#xff0c;然后把流量转发到指定的…

保姆级教程Docker部署KRaft模式的Kafka官方镜像

目录 一、安装Docker及可视化工具 二、单节点部署 1、创建挂载目录 2、运行Kafka容器 3、Compose运行Kafka容器 4、查看Kafka运行状态 三、集群部署 四、部署可视化工具 1、创建挂载目录 2、运行Kafka-ui容器 3、Compose运行Kafka-ui容器 4、查看Kafka-ui运行状态 …

【C语言】传值调用与传址调用详解

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 &#x1f4af;前言&#x1f4af;传值调用1. 什么是传值调用&#xff1f;2. 示例代码&#xff1a;传值调用失败的情况执行结果&#xff1a; 3. 为什么传值调用无法修改外部变量&#xff1f; &#x1f4…

HarmonyOS 5.0应用开发——ContentSlot的使用

【高心星出品】 文章目录 ContentSlot的使用使用方法案例运行结果 完整代码 ContentSlot的使用 用于渲染并管理Native层使用C-API创建的组件同时也支持ArkTS创建的NodeContent对象。 支持混合模式开发&#xff0c;当容器是ArkTS组件&#xff0c;子组件在Native侧创建时&#…

Golang:Go 1.23 版本新特性介绍

流行的编程语言Go已经发布了1.23版本&#xff0c;带来了许多改进、优化和新特性。在Go 1.22发布六个月后&#xff0c;这次更新增强了工具链、运行时和库&#xff0c;同时保持了向后兼容性。 Go 1.23 的新增特性主要包括语言特性、工具链改进、标准库更新等方面&#xff0c;以下…

11.PPT:世界动物日【25】

目录 NO12​ NO34 NO56​ NO789视频音频​ NO10/11/12​ NO12 设计→幻灯片大小→ →全屏显示&#xff08;16&#xff1a;9&#xff09;确定调整标题占位符置于图片右侧&#xff1a;内容占位符与标题占位符左对齐单击右键“世界动物日1”→复制版式→大小→对齐 幻灯片大小…

力扣.623. 在二叉树中增加一行(链式结构的插入操作)

Problem: 623. 在二叉树中增加一行 文章目录 题目描述思路复杂度Code 题目描述 思路 1.首先要说明&#xff0c;对于数据结构无非两大类结构&#xff1a;顺序结构、链式结构&#xff0c;而二叉树实质上就可以等效看作为一个二叉链表&#xff0c;而对于链表插入一个节点的操作是应…

Office/WPS接入DS等多个AI工具,开启办公新模式!

在现代职场中&#xff0c;Office办公套件已成为工作和学习的必备工具&#xff0c;其功能强大但复杂&#xff0c;熟练掌握需要系统的学习。为了简化操作&#xff0c;使每个人都能轻松使用各种功能&#xff0c;市场上涌现出各类办公插件。这些插件不仅提升了用户体验&#xff0c;…