C++17完整导引-模板特性之类模板参数推导
模板特性之类模板参数推导
- 使用类模板参数推导
- 默认以拷贝方式推导
- 推导`lambda`的类型
- 没有类模板部分参数推导
- 使用类模板参数推导代替快捷函数
- 推导指引
- 使用推导指引强制类型退化
- 非模板推导指引
- 推导指引VS构造函数
- 显式推导指引
- 聚合体的推导指引
- 标准推导指引
- `pair`和`tuple`的推导指引
- 从迭代器推导
- `std::array<>`推导
- (Unordered) Map推导
- 智能指针没有推导指引
在
C++17
之前,你必须明确指出类模板的所有参数。例如,你不可以省略下面的
double
:
std::complex<double> c{5.1, 3.3};
也不可以省略下面代码中的第二个std::mutex
:
std::mutex mx;
std::lock_guard<std::mutex> lg(mx);
自从C++17
起必须指明类模板参数的限制被放宽了。通过使用 类模板参数推导(class template argument deduction) (CTAD),只要编译器能根据初始值 推导出 所有模板参数,那么就可以不指明参数。
例如:
- 你现在可以这么声明:
std::complex c{5.1, 3.3}; // OK:推导出std::complex<double>
但是在c++11
会报如下错误
error: missing template arguments before 'c'7 | std::complex c{5.1, 3.3};
- 你现在可以这么写:
std::mutex mx;
std::lock_guard lg{mx}; // OK:推导出std::lock_guard<std::mutex>
在c++11
会报如下错误
<source>:9:21: error: missing template arguments before 'lg'9 | std::lock_guard lg(mx);|
- 你现在甚至可以让容器来推导元素类型:
std::vector v1 {1, 2, 3}; // OK:推导出std::vector<int>
std::vector v2 {"hello", "world"}; // OK:推导出std::vector<const char*>
上述三个示例的预处理代码如下:
std::complex<double> c = std::complex<double>{5.0999999999999996, 3.2999999999999998};std::mutex mx = std::mutex();std::lock_guard<std::mutex> lg = std::lock_guard<std::mutex>(mx);std::vector<int, std::allocator<int> > v1 = std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3}, std::allocator<int>()};std::vector<const char *, std::allocator<const char *> > v2 = std::vector<const char *, std::allocator<const char *> >{std::initializer_list<const char *>{"hello", "world"}, std::allocator<const char *>()};
使用类模板参数推导
只要能根据初始值推导出所有模板参数就可以使用类模板参数推导。推导过程支持所有方式的初始化(只要保证初始化是有效的):
std::complex c1{1.1, 2.2}; // 推导出std::complex<double>
std::complex c2(2.2, 3.3); // 推导出std::complex<double>
std::complex c3 = 3.3; // 推导出std::complex<double>
std::complex c4 = {4.4}; // 推导出std::complex<double>
c++17
的预处理代码如下:
std::complex<double> c1 = std::complex<double>{1.1000000000000001, 2.2000000000000002};std::complex<double> c2 = std::complex<double>(2.2000000000000002, 3.2999999999999998);std::complex<double> c3 = std::complex<double>(3.2999999999999998, 0.0);std::complex<double> c4 = std::complex<double>{4.4000000000000004, 0.0};
因为std::complex
只需要一个参数就可以初始化并推导出模板参数T
:
namespace std {template<typename T>class complex {constexpr complex(const T&re = T(), const T& im = T());...}
};
所以c3
和c4
可以正确初始化。对于如下声明:
std::complex c1{1.1, 2.2};
编译器会查找到构造函数:constexpr complex(const T& re = T(), const T& im = T());
并调用。因为两个参数都是double
类型,所以编译器会推导出T
就是double
,并生成如下代码:
complex<double>::complex(const double& re = double(), const double& im = double());
注意推导的过程中模板参数必须没有歧义。也就是说,如下初始化代码不能通过编译:
std::complex c5{5, 3.3}; // ERROR:尝试将T推导为int和double
像通常的模板一样,推导模板参数时不会使用隐式类型转换。也可以对可变参数模板使用类模板参数推导。例如,对于一个如下定义的std::tuple
:
namespace std {template<typename... Types>class tuple {public:constexpr tuple(const Types&...);...};
};
如下声明:
std::tuple t{42, 'x', nullptr};
将推导出类型std::tuple<int, char, std::nullptr_t>
,预处理代码如下:
std::tuple<int, char, std::nullptr_t> t = std::tuple<int, char, std::nullptr_t>{42, 'x', nullptr};
我们也可以推导非类型模板参数。例如,我们可以根据传入的参数同时推导数组的元素类型和元素数量:
#include <iostream>template <typename T, int SZ>
class MyClass {public:MyClass(T (&)[SZ]) {}
};
int main() { MyClass mc("hello"); /* // 推导出T为const char,SZ为6*/}
这里我们推导出SZ
为6
,因为传入的字符串字面量有6个字符。预处理代码如下:
#include <iostream>template<typename T, int SZ>
class MyClass
{public: inline MyClass(T (&)[SZ]){}
};
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class MyClass<const char, 6>
{public: inline MyClass(const char (&)[6]){}};
#endif
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
MyClass(const char (&)[6]) -> MyClass<const char, 6>;
#endif
int main()
{MyClass<const char, 6> mc = MyClass<const char, 6>("hello");return 0;
}
你甚至可以推导用作基类的lambda
来实现重载或者推导auto
模板参数。
默认以拷贝方式推导
类模板参数推导过程中会首先尝试以拷贝的方式初始化。例如,首先初始化一个只有一个元素的std::vector
:
std::vector v1{42}; // 一个元素的vector<int>
预处理代码如下:
std::vector<int, std::allocator<int> > v1 = std::vector<int, std::allocator<int> >{std::initializer_list<int>{42}, std::allocator<int>()};
然后使用这个vector初始化另一个vector,推导时会解释为创建一个拷贝:std::vector v2{v1}; // v2也是一个std::vector<int>
而不是创建一个只有一个元素的vector<vector<int>>
。
这个规则适用于所有形式的初始化:
std::vector v2{v1}; // v2也是vector<int>
std::vector v3(v1); // v3也是vector<int>
std::vector v4 = {v1}; // v4也是vector<int>
auto v5 = std::vector{v1}; // v5也是vector<int>
预处理代码如下:
std::vector<int, std::allocator<int> > v2 = std::vector<int, std::allocator<int> >{v1};std::vector<int, std::allocator<int> > v3 = std::vector<int, std::allocator<int> >(v1);std::vector<int, std::allocator<int> > v4 = std::vector<int, std::allocator<int> >{v1};std::vector<int, std::allocator<int> > v5 = std::vector<int, std::allocator<int> >{v1};
注意 这是花括号初始化总是把列表中的参数作为元素这一规则的一个例外。如果你传递一个只有一个vector
的初值列来初始化另一个vector
,你将得到一个传入的vector
的拷贝。然而,如果用多于一个元素的初值列来初始化的话就会把传入的参数作为元素并推导出其类型作为模板参数(因为这种情况下无法解释为创建拷贝):
std::vector vv{v1, v2}; // vv是一个vector<vector<int>>
预处理代码如下:
std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > > vv = std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > >{std::initializer_list<std::vector<int, std::allocator<int> > >{std::vector<int, std::allocator<int> >(v1), std::vector<int, std::allocator<int> >(v2)}, std::allocator<std::vector<int, std::allocator<int> > >()};
这引出了一个问题就是对可变参数模板使用类模板参数推导时会发生什么:
template<typename... Args>
auto make_vector(const Args&... elems) {return std::vector{elem...};
}
std::vector<int> v{1, 2, 3};
auto x1 = make_vector(v, v); // vector<vector<int>>
auto x2 = make_vector(v); // vector<int>还是vector<vector<int>>?
目前不同的编译器会有不同的行为,这个问题还在讨论之中。
推导lambda
的类型
通过使用类模板参数推导,我们可以用lambda
的类型(确切的说是lambda
生成的 闭包类型 )作为模板参数来实例化类模板。例如我们可以提供一个泛型类,对一个任意回调函数进行包装并统计调用次数:
#include <utility> // for std::forward()template<typename CB>
class CountCalls
{
private:CB callback; // 要调用的回调函数long calls = 0; // 调用的次数
public:CountCalls(CB cb) : callback(cb) {}template<typename... Args>decltype(auto) operator() (Args&&... args) {++calls;return callback(std::forward<Args>(args)...);}long count() const {return calls;}
};
这里构造函数获取一个回调函数并进行包装,这样在初始化时会把参数的类型推导为CB
。例如,我们可以使用一个lambda
作为参数来初始化一个对象:
CountCalls sc{[](auto x, auto y) { return x > y; }};
这意味着排序准则sc
的类型将被推导为CountCalls<TypeOfTheLambda>
。预处理内容为 CountCalls<__lambda_26_19> sc = CountCalls<__lambda_26_19>{__lambda_26_19{}};
。这样,我们可以统计出排序准则被调用的次数:
std::vector v{5,4,3,2,1};std::sort(v.begin(), v.end(), // 排序区间std::ref(sc)); // 排序准则std::cout << "sorted with " << sc.count() << " calls\n" <<v.at(0);
预处理代码如下:
{public: template<class type_parameter_0_0, class type_parameter_0_1>inline /*constexpr */ auto operator()(type_parameter_0_0 x, type_parameter_0_1 y) const{return x < y;}#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline /*constexpr */ bool operator()<int, int>(int x, int y) const{return x < y;}#endifprivate: template<class type_parameter_0_0, class type_parameter_0_1>static inline /*constexpr */ auto __invoke(type_parameter_0_0 x, type_parameter_0_1 y){return __lambda_26_19{}.operator()<type_parameter_0_0, type_parameter_0_1>(x, y);}public: // inline /*constexpr */ __lambda_26_19(const __lambda_26_19 &) noexcept = default;// /*constexpr */ __lambda_26_19() = default;};CountCalls<__lambda_26_19> sc = CountCalls<__lambda_26_19>{__lambda_26_19{}};std::vector<int, std::allocator<int> > v = std::vector<int, std::allocator<int> >{std::initializer_list<int>{5, 4, 3, 2, 1}, std::allocator<int>()};std::sort(v.begin(), v.end(), std::ref(sc));
输出
sorted with 4 calls
1
这里包装过后的lambda
被用作排序准则。注意这里必须要传递引用,否则std::sort()
将会获取sc
的拷贝作为参数,计数时只会修改该拷贝内的计数器。然而,我们可以直接把包装后的lambda
传递给std::for_each()
,因为该算法(非并行版本)最后会返回传入的回调函数,以便于获取回调函数最终的状态:
std::vector v{5,4,3,2,1};auto fo = std::for_each(v.begin(), v.end(), CountCalls{[](auto i) {std::cout << "elem: " << i << '\n';}});std::cout << "output with " << fo.count() << " calls\n";
预处理代码如下:
class __lambda_32_60{public: template<class type_parameter_0_0>inline /*constexpr */ auto operator()(type_parameter_0_0 i) const{(std::operator<<(std::cout, "elem: ") << i) << '\n';}#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline /*constexpr */ void operator()<int>(int i) const{std::operator<<(std::operator<<(std::cout, "elem: ").operator<<(i), '\n');}#endifprivate: template<class type_parameter_0_0>static inline /*constexpr */ auto __invoke(type_parameter_0_0 i){return __lambda_32_60{}.operator()<type_parameter_0_0>(i);}public: // inline /*constexpr */ __lambda_32_60(const __lambda_32_60 &) noexcept = default;// inline /*constexpr */ __lambda_32_60(__lambda_32_60 &&) noexcept = default;// /*constexpr */ __lambda_32_60() = default;};CountCalls<__lambda_32_60> fo = std::for_each(v.begin(), v.end(), CountCalls<__lambda_32_60>{__lambda_32_60{}});
输出将会如下(排序准则调用次数可能会不同,因为sort()
的实现可能会不同):
elem: 1
elem: 2
elem: 3
elem: 4
elem: 5
output with 5 calls
如果计数器是原子的,你也可以使用并行算法:
std::sort(std::execution::par, v.begin(), v.end(), std::ref(sc));
没有类模板部分参数推导
注意,不像函数模板,类模板不能只指明一部分模板参数,然后指望编译器去推导剩余的部分参数。甚至使用<>
指明空模板参数列表也是不允许的。例如:
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
#include <string>using namespace std;template <typename T1, typename T2, typename T3 = T2>
class C {public:C(T1 x = {}, T2 y = {}, T3 z = {}) {}
};// 推导所有参数
C c1(22, 44.3, "hi"); // OK:T1是int,T2是double,T3是const char*
C c2(22, 44.3); // OK:T1是int,T2和3是double
C c3("hi", "guy"); // OK:T1、T2、T3都是const char*// 推导部分参数
C<string> c4("hi", "my"); // ERROR:只有T1显式指明 error: too few template arguments for class template 'C'
C<> c5(22, 44.3); // ERROR:T1和T2都没有指明 error: too few template arguments for class template 'C'
C<> c6(22, 44.3, 42); // ERROR:T1和T2都没有指明 error: too few template arguments for class template 'C'// 指明所有参数
C<string, string, int> c7; // OK:T1、T2是string,T3是int
C<int, string> c8(52, "my"); // OK:T1是int,T2、T3是string
C<string, string> c9("a", "b", "c"); // OK:T1、T2、T3都是string
预处理代码如下:
// 推导所有参数C<int, double, const char *> c1 = C<int, double, const char *>(22, 44.299999999999997, "hi");C<int, double, double> c2 = C<int, double, double>(22, 44.299999999999997, {});C<const char *, const char *, const char *> c3 = C<const char *, const char *, const char *>("hi", "guy", {});// 指明所有参数C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> c7 = C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{}, std::basic_string<char, std::char_traits<char>, std::allocator<char> >{}, {});C<int, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > c8 = C<int, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(52, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("my", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >{});C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > c9 = C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >("b", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >("c", std::allocator<char>()));
注意第三个模板参数有默认值,因此只要指明了第二个参数就不需要再指明第三个参数。如果你想知道为什么不支持部分参数推导,这里有一个导致这个决定的例子:
std::tuple<int> t(42, 43); // 仍然ERROR
std::tuple
是一个可变参数模板,因此你可以指明任意数量的模板参数。在这个例子中,并不能判断出只指明一个参数是一个错误还是故意的。不幸的是,不支持部分参数推导意味着一个常见的编码需求并没有得到解决。我们仍然不能简单的使用一个lambda
作为关联容器的排序准则或者无序容器的hash函数:
std::set<Cust> coll([] (const Cust& x, const Cust& y) { // 仍然ERRORreturn x.getName() > y.getName();});
我们仍然必须指明lambda
的类型。例如:
auto sortcrit = [] (const Cust& x, const Cust& y) {return x.getName() > y.getName();};
std::set<Cust, decltype(sortcrit)> coll(sortcrit); // OK
仅仅指明类型是不行的,因为容器初始化时会尝试用给出的lambda
类型创建一个lambda
。但这在C++17
中是不允许的,因为默认构造函数只有编译器才能调用。
使用类模板参数推导代替快捷函数
原则上讲,通过使用类模板参数推导,我们可以摆脱已有的几个快捷函数模板,这些快捷函数的作用其实就是根据传入的参数实例化相应的类模板。一个明显的例子是std::make_pair()
,它可以帮助我们避免指明传入参数的类型。
例如,在如下声明之后:
std::vector<int> v;
我们可以这样:
auto p = std::make_pair(v.begin(), v.end());
而不需要写:
std::pair<typename std::vector<int>::iterator, typename std::vector<int>::iterator>p(v.begin(), v.end());
现在这种场景已经不再需要std::make_pair()
了,我们可以简单的写为std::pair p(v.begin(), v.end());
或者std::pair p{v.begin(), v.end()};
然而,从另一个角度来看std::make_pair()
也是一个很好的例子,它演示了有时便捷函数的作用不仅仅是推导模板参数。事实上std::make_pair()
会使传入的参数退化(在C++03
中以值传递,自从C++11
起使用特征)。这样会导致字符串字面量的类型(字符数组)被推导为const char*
:
auto q = std::make_pair("hi", "world"); // 推导为指针的pair
预处理代码如下,可以看到,q
的类型为std::pair<const char*, const char*>
。
std::pair<const char *, const char *> q = std::make_pair("hi", "world");
使用类模板参数推导可能会让事情变得更加复杂。考虑如下这个类似于std::pair
的简单的类的声明:
template<typename T1, typename T2>
struct Pair1 {T1 first;T2 second;Pair1(const T1& x, const T2& y) : first{x}, second{y} {}
};
这里元素以引用传入,根据语言规则,当以引用传递参数时模板参数的类型不会退化。因此,当调用:
Pair1 p1{"hi", "world"}; // 推导为不同大小的数组的pair
T1
被推导为char[3]
,T2
被推导为char[6]
。原则上讲这样的推导是有效的。然而,我们使用了T1
和T2
来声明成员first
和second
,因此它们被声明为:
char first[3];
char second[6];
然而使用一个左值数组来初始化另一个数组是不允许的。它类似于尝试编译如下代码:
const char x[3] = "hi";
const char y[6] = "world";
char first[3] {x}; // ERROR
char second[6] {y}; // ERROR
所以代码Pair1 p1{"hi", "world"};
报错如下:
<source>:12:45: error: cannot initialize an array element of type 'char' with an lvalue of type 'const char[3]'Pair1(const T1& x, const T2& y) : first{x}, second{y} {^
<source>:17:11: note: in instantiation of member function 'Pair1<char[3], char[6]>::Pair1' requested herePair1 p1{"hi", "world"}; ^
<source>:12:56: error: cannot initialize an array element of type 'char' with an lvalue of type 'const char[6]'Pair1(const T1& x, const T2& y) : first{x}, second{y} {
注意如果我们声明参数时以值传参就不会再有这个问题:
tempalte<typename T1, typename T2>
struct Pair2 {T1 first;T2 second;Pair2(T1 x, T2 y) : first{x}, second{y} {}
};
如果我们像下面这样创建新对象:
Pair2 p2{"hi", "world"}; // 推导为指针的pair
T1
和T2
都会被推导为const char*
。
预处理代码如下:
template<typename T1, typename T2>
struct Pair1
{T1 first;T2 second;inline Pair1(T1 x, T2 y): first{x}, second{y}{}
};/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Pair1<const char *, const char *>
{const char * first;const char * second;inline Pair1(const char * x, const char * y): first{x}, second{y}{}};
#endif/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair1(const char * x, const char * y) -> Pair1<const char *, const char *>;
#endifint main()
{Pair1<const char *, const char *> p1 = Pair1<const char *, const char *>{"hi", "world"};return 0;
}
然而,因为std::pair<>
的构造函数以引用传参,所以下面的初始化正常情况下应该不能通过编译:
std::pair p{"hi", "world"}; // 看似会推导出不同大小的数组的pair,但是……
然而事实上它能通过编译,因为std::pair<>
有 推导指引 ,
推导指引
你可以定义特定的 推导指引 来给类模板参数添加新的推导或者修正构造函数定义的推导。
例如,你可以定义无论何时推导Pair3
的模板参数,推导的行为都好像参数是以值传递的:
#include <iostream>
#include <string>
#include <utility>template <typename T1, typename T2>
struct Pair3 {T1 first;T2 second;Pair3(const T1& x, const T2& y) : first{x}, second{y} {}
};
//为构造函数定义的推到指引
template <typename T1, typename T2>
Pair3(T1, T2) -> Pair3<T1, T2>;int main() { Pair3 p1{"hi", "world"}; }
在->
的左侧我们声明了我们 想要推导什么 。这里我们声明的是使用两个以值传递且类型分别为T1
和T2
的对象创建一个Pair3
对象。在->
的右侧,我们定义了推导的结果。在这个例子中,Pair3
以类型T1
和T2
实例化。
预处理代码如下
template<typename T1, typename T2>
struct Pair3
{T1 first;T2 second;inline Pair3(const T1 & x, const T2 & y): first{x}, second{y}{}};/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Pair3<const char *, const char *>
{const char * first;const char * second;inline Pair3(const char *const & x, const char *const & y): first{x}, second{y}{}};
#endif#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair3(const char (&x)[3], const char (&y)[6]) -> Pair3<char[3], char[6]>;
#endiftemplate <typename T1, typename T2>
Pair3(T1, T2) -> Pair3<T1, T2>;/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair3(const char *, const char *) -> Pair3<const char *, const char *>;
#endifint main()
{Pair3<const char *, const char *> p1 = Pair3<const char *, const char *>{"hi", "world"};return 0;
}
你可能会说这是构造函数已经做到的事情。然而,构造函数是以引用传参,两者是不同的。一般来说,不仅是模板,所有以值传递的参数都会 退化 ,而以引用传递的参数不会退化。 退化 意味着原生数组会转换为指针,并且顶层的修饰符例如const
或者引用将会被忽略。
如果没有推导指引,对于如下声明:
Pair3 p3{"hi", "world"};
参数x
的类型是const char(&)[3]
,因此T1
被推导为char[3]
,参数y
的类型是const char(&)[6]
,因此T2
被推导为char[6]
。有了推导指引后,模板参数就会退化。这意味着传入的数组或者字符串字面量会退化为相应的指针类型。现在,如下声明:
Pair3 p3{"hi", "world"};
推导指引会发挥作用,因此会以值传参。因此,两个类型都会退化为const char*
,然后被用作模板参数推导的结果。上面的声明和下述声明是等价:
Pair3<const char*, const char*> p3{"hi", "world"};
注意构造函数仍然以引用传参。推导指引只和模板参数的推导相关,它与推导出T1
和T2
之后实际调用的构造函数无关。
使用推导指引强制类型退化
就像上一个例子展示的那样,重载推导规则的一个非常重要的用途就是确保模板参数T
在推导时发生 退化 。考虑如下的一个经典的类模板:
template<typename T>
struct C {C(const T&) {}...
};
这里,如果我们传递一个字符串字面量"hello"
,传递的类型将是const char(&)[6]
,因此T
被推导为char[6]
:
C x{"hello"}; // T被推导为char[6]
原因是当参数以引用传递时模板参数不会 退化 为相应的指针类型。
通过使用一个简单的推导指引:
template<typename T> C(T) -> C<T>;
我们就可以修正这个问题:
C x{"hello"}; // T被推导为const char*
推导指引以值传递参数因此"hello"
的类型T
会退化为const char*
。因为这一点,任何构造函数里传递引用作为参数的模板类都需要一个相应的推导指引。C++
标准库中为pair
和tuple
提供了相应的推导指引。
非模板推导指引
推导指引并不一定是模板,也不一定应用于构造函数。例如,为下面的结构体添加的推导指引也是有效的:
template<typename T>
struct S {T val;
};
S(const char*) -> S<std::string>; // 把S<字符串字面量>映射为S<std::string>
这里我们创建了一个没有相应构造函数的推导指引。推导指引被用来推导参数T
,然后结构体的模板参数就相当于已经被指明了。
因此,下面所有初始化代码都是正确的,并且都会把模板参数T
推导为std::string
:
S s1{"hello"}; // OK,等同于S<std::string> s1{"hello"};
S s2 = {"hello"}; // OK,等同于S<std::string> s2 = {"hello"};
S s3 = S{"hello"}; // OK,两个S都被推导为S<std::string>
因为传入的字符串字面量能隐式转换为std::string
,所以上面的初始化都是有效的。
注意聚合体需要列表初始化。下面的代码中参数推导能正常工作,但会因为没有使用花括号导致初始化错误:
S s4 = "hello"; // ERROR:不能不使用花括号初始化聚合体
S s5("hello"); // ERROR:不能不使用花括号初始化聚合体
推导指引VS构造函数
推导指引会和类的构造函数产生竞争。类模板参数推导时会根据重载情况选择最佳匹配的构造函数/推导指引。如果一个构造函数和一个推导指引匹配优先级相同,那么将会优先使用推导指引。
考虑如下定义:
template<typename T>
struct C1 {C1(const T&) {}
};
C1(int)->C1<long>;
当传递一个int
时将会使用推导指引,因为根据重载规则它的匹配度更高。
因此,T
被推导为long
:
C1 x1{42}; // T被推导为long
然而,如果我们传递一个char
,那么构造函数的匹配度更高(因为不需要类型转换),这意味着T
会被推导为char
:
C1 x3{'x'}; // T被推导为char
在重载规则中,以值传参和以引用传参的匹配度相同的。然而在相同匹配度的情况下将优先使用推导指引。因此,通常会把推导指引定义为以值传参(这样做还有类型退化的优点)。
显式推导指引
推导指引可以用explicit
声明。当出现explicit
不允许的初始化或转换时这一条推导指引就会被忽略。例如:
template<typename T>
struct S {T val;
};explicit S(const char*) -> S<std::string>;
如果用拷贝初始化(使用=
)将会忽略这一条推导指引。这意味着下面的初始化是无效的:
S s1 = {"hello"}; // ERROR(推导指引被忽略,因此是无效的)
报错如下:
<source>:12:29: error: class template argument deduction for 'S<T>' failed: explicit deduction guide selected in copy-list-initialization12 | int main() { S s1 = {"hello"}; }| ^
<source>:10:10: note: explicit deduction guide declared here10 | explicit S(const char*) -> S<std::string>;| ^
直接初始化或者右侧显式推导的方式仍然有效:
S s2{"hello"}; // OK,等同于S<std::string> s2{"hello"};
S s3 = S{"hello"}; // OK
S s4 = {S{"hello"}}; // OK
预处理代码如下:
S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s2 = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())};S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s3 = S{std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())};S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s4 = {S{std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())}};
另一个例子如下:
#include <iostream>
#include <string>
#include <utility>template <typename T>
struct Ptr {Ptr(T) { std::cout << "Ptr(T)\n"; }template <typename U>Ptr(U) {std::cout << "Ptr(U)\n";}
};template <typename T>
explicit Ptr(T) -> Ptr<T*>;int main() {Ptr p1{42}; // 根据推导指引推导出Ptr<int*>Ptr p2 = 42; // 根据构造函数推导出Ptr<int>int i = 42;Ptr p3{&i}; // 根据推导指引推导出Ptr<int**>Ptr p4 = &i; // 根据构造函数推导出Ptr<int*>
}
预处理代码如下:
#include <iostream>
#include <string>
#include <utility>template<typename T>
struct Ptr
{inline Ptr(T){std::operator<<(std::cout, "Ptr(T)\n");}template<typename U>inline Ptr(U){std::operator<<(std::cout, "Ptr(U)\n");}};/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int *>
{inline Ptr(int *){std::operator<<(std::cout, "Ptr(T)\n");}template<typename U>inline Ptr(U);/* First instantiated from: insights.cpp:18 */#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline Ptr<int>(int){std::operator<<(std::cout, "Ptr(U)\n");}#endif#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline Ptr<int *>(int *);#endif};#endif
/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int>
{inline Ptr(int){std::operator<<(std::cout, "Ptr(T)\n");}template<typename U>inline Ptr(U);#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline Ptr<int>(int);#endif};#endif
/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int **>
{inline Ptr(int **);template<typename U>inline Ptr(U);/* First instantiated from: insights.cpp:21 */#ifdef INSIGHTS_USE_TEMPLATEtemplate<>inline Ptr<int *>(int *){std::operator<<(std::cout, "Ptr(U)\n");}#endif};#endif/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int) -> Ptr<int>;
#endif/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int *) -> Ptr<int *>;
#endiftemplate <typename T>
explicit Ptr(T) -> Ptr<T*>;/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int) -> Ptr<int *>;
#endif/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int *) -> Ptr<int **>;
#endifint main()
{Ptr<int *> p1 = Ptr<int *>{42};Ptr<int> p2 = Ptr<int>(42);int i = 42;Ptr<int **> p3 = Ptr<int **>{&i};Ptr<int *> p4 = Ptr<int *>(&i);return 0;
}
聚合体的推导指引
泛型聚合体中也可以通过使用推导指引来支持类模板参数推导。例如,对于:
template<typename T>
struct A {T val;
};
在没有推导指引的情况下尝试使用类模板参数推导会导致错误:
A i1{42}; // ERROR
A s1("hi"); // ERROR
A s2{"hi"}; // ERROR
A s3 = "hi"; // ERROR
A s4 = {"hi"}; // ERROR
你必须显式指明参数的类型T
:
A<int> i2{42};
A<std::string> s5 = {"hi"};
然而,如果有推导指引A(const char*) -> A<std::string>; A(int) -> A<int>;
的话
你就可以像下面这样初始化聚合体:
A i1{42}; //ok
A s2{"hi"}; // OK
A s4 = {"hi"}; // OK
预处理代码如下
A<int> i1 = {42};A<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s2 = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hi", std::allocator<char>())};A<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s4 = {std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hi", std::allocator<char>())};
注意你仍然需要使用花括号(像通常的聚合体初始化一样)。否则,类型T
能成功推导出来,但初始化会错误:
A s1("hi"); // ERROR:T是string,但聚合体不能初始化
A s3 = "hi"; // ERROR:T是string,但聚合体不能初始化
报错如下:
<source>: In function 'int main()':
<source>:15:14: error: no matching function for call to 'A<std::__cxx11::basic_string<char> >::A(const char [3])'15 | A s1("hi"); // ERROR| ^
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A()'6 | struct A {| ^
<source>:6:8: note: candidate expects 0 arguments, 1 provided
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A(const A<std::__cxx11::basic_string<char> >&)'
<source>:6:8: note: no known conversion for argument 1 from 'const char [3]' to 'const A<std::__cxx11::basic_string<char> >&'
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A(A<std::__cxx11::basic_string<char> >&&)'
<source>:6:8: note: no known conversion for argument 1 from 'const char [3]' to 'A<std::__cxx11::basic_string<char> >&&'
<source>:17:12: error: conversion from 'const char [3]' to non-scalar type 'A<std::__cxx11::basic_string<char> >' requested17 | A s3 = "hi"; // ERROR
std::array
的推导指引是一个有关聚合体推导指引的进一步的例子。
标准推导指引
C++17
标准在标准库中引入了很多推导指引。
pair
和tuple
的推导指引
std::pair
需要推导指引来确保类模板参数推导时会推导出参数的退化类型:
namespace std {template<typename T1, typename T2>struct pair {...constexpr pair(const T1& x, const T2& y); // 以引用传参...};template<typename T1, typename T2>pair(T1, T2) -> pair<T1, T2>; // 以值推导类型
}
因此,如下声明:
std::pair p{"hi", "wrold"}; // 参数类型分别为const char[3]和const char[6]
等价于:
std::pair<const char*, const char*> p{"hi", "world"};
可变参数类模板std::tuple
也使用了相同的方法:
namespace std {template<typename... Types>class tuple {public:constexpr tuple(const Types&...); // 以引用传参template<typename... UTypes> constexpr tuple(UTypes&&...);...};template<typename... Types>tuple(Types...) -> tuple<Types...>; // 以值推导类型
}
因此,如下声明:
std::tuple t{42, "hello", nullptr};
将会推导出t
的类型为std::tuple<int, const char*, std::nullptr_t>
。
从迭代器推导
为了能够从表示范围的两个迭代器推导出元素的类型,所有的容器类例如std::vector<>
都有类似于如下的推导指引:
// 使std::vector<>能根据初始的迭代器推导出元素类型
namespace std {template<typename Iterator>vector(Iterator, Iterator) -> vector<typename iterator_traits<Iterator>::value_type>;
}
下面的例子展示了它的作用:
std::set<float> s;
std::vector v1(s.begin(), s.end()); // OK,推导出std::vector<float>
**注意这里必须使用圆括号来初始化。**如果你使用花括号:
std::vector v2{s.begin(), s.end()}; // 注意:并不会推导出std::vector<float>
那么这两个参数将会被看作一个初值列的两个元素(根据重载规则初值列的优先级更高)。因此,它等价于:
std::vector<std::set<float>::iterator> v2{s.begin(), s.end()};
这意味着我们初始化的vector
有两个元素,第一个元素是一个指向首元素的迭代器,第二个元素是指向尾后元素的迭代器。
另一方面,考虑:
std::vector v3{"hi", "world"}; // OK,推导为std::vector<const char*>
std::vector v4("hi", "world"); // OOPS:运行时错误
v3
的声明会初始化一个拥有两个元素的vector(两个元素都是字符串字面量),v4
的初始化会导致运行时错误,很可能会导致core dump。
**问题在于字符串字面量被转换成为字符指针,也算是有效的迭代器。**因此,我们传递了两个 不是 指向同一个对象的迭代器。换句话说,我们指定了一个无效的区间。我们推导出了一个std::vector<const char>
,但是根据这两个字符串字面量在内存中的位置关系,我们可能会得到一个bad_alloc
异常,也可能会因为没有距离而得到一个core dump,还有可能得到两个位置之间的未定义范围内的字符。
总而言之,使用花括号是最佳的初始化vector
的 元素 的方法。唯一的例外是传递单独一个vector
(这时会优先进行拷贝)。当传递别的含义的参数时,使用圆括号会更好。
在任何情况下,对于像std::vector<>
或其他STL容器一样拥有复杂的构造函数的类模板, 强烈建议不要使用类模板参数推导 ,而是显式指明类型。
std::array<>
推导
有一个更有趣的例子是关于std::array<>
的。为了能够同时推导出元素的类型和数量:
std::array a{42, 45, 77}; // OK,推导出std::array<int, 3>
而定义了下面的推导指引(间接的):
// 让std::array<>推导出元素的数量(元素的类型必须相同):
namespace std {template<typename T, typename... U>array(T, U...) -> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}
这个推导指引使用了折叠表达式
(is_same_v<T, U> && ...)
来确保所有参数的类型相同。因此,下面的代码是错误的:
std::array a{42, 45, 77.7}; // ERROR:元素类型不同
注意类模板参数推导的初始化甚至可以在编译期上下文中生效:
constexpr std::array arr{0, 8, 15}; // OK,推导出std::array<int, 3>
(Unordered) Map推导
想让推导指引正常工作是非常困难的。可以通过给关联容器(map
、multimap
、unordered_map
、unordered_multimap
)定义推导指引来展示其复杂程度。
这些容器里元素的类型是std::pair<const keytype, valuetype>
。这里const
是必需的,因为元素的位置取决于key的值,这意味着如果能修改key的值的话会导致容器内部陷入不一致的状态。
在C++17
标准中为std::map
:
namespace std {template<typename Key, typename T, typename Compare = less<Key>,typename Allocator = allocator<pair<const Key, T>>>class map {...};
}
想出的第一个解决方案是,为如下构造函数:
map(initializer_list<pair<const Key, T>>, const Compare& = Compare(),const Allocator& = Allocator());
定义了如下的推导指引:
namespace std {template<typename Key, typename T, typename Compare = less<Key>,typename Allocator = allocator<pair<const Key, T>>>map(initializer_list<pair<const Key, T>>, Compare = Compare(), Allocator = Allocator())-> map<Key, T, Compare, Allocator>;
}
所有的参数都以值传递,因此这个推导指引允许传递的比较器和分配器像之前讨论的一样发生退化。然而,我们在推导指引中直接使用了和构造函数中完全相同的元素类型,这意味着初值列的key的类型必须是const
的。因此,下面的代码不能工作:
std::pair elem1{1, 2};
std::pair elem2{3, 4};
...
std::map m1{elem1, elem2}; // 原来的C++17推导指引会ERROR
这是因为elem1
和elem2
会被推导为std::pair<int, int>
,而推导指引需要pair
中的第一个元素是const
的类型,所以不能成功匹配。因此,你仍然要像下面这么写:
std::map<int, int> m1{elem1, elem2}; // OK
因此,推导指引中的const
必须被删掉:
namespace std {template<typename Key, typename T, typename Compare = less<Key>,typename Allocator = allocator<pair<const Key, T>>>map(initializer_list<pair<Key, T>>, Compare = Compare(), Allocator = Allocator())-> map<Key, T, Compare, Allocator>;
}
然而,为了继续支持比较器和分配器的退化,我们还需要为const
key类型的pair定义一个重载版本。否则当传递一个const
key类型的参数时将会使用构造函数来推导类型,这样会导致传递const
key和非const
key参数时推导的结果会有细微的不同。
智能指针没有推导指引
注意C++
标准库中某些你觉得应该有推导指引的地方实际上没有推导指引。你可能会希望共享指针和独占指针有推导指引,这样你就不用写:
std::shared_ptr<int> sp{new int(7)};
而是直接写:
std::shared_ptr sp{new int(7)}; // 不支持
上边的写法是错误的,因为相应的构造函数是一个模板,这意味着没有隐式的推导指引:
namespace std {template<typename T> class shared_ptr {public:...template<typename Y> explicit shared_ptr(Y* p);...};
}
这里Y
和T
是不同的模板参数,这意味着虽然能从构造函数推导出Y
,但不能推导出T
。这是一个为了支持如下写法的特性:
std::shared_ptr<Base> sp{new Derived(...)};
假如我们要提供推导指引的话,那么相应的推导指引可以简单的写为:
namespace std {template<typename Y> shared_ptr(Y*) -> shared_ptr<Y>;
}
然而,这可能导致当分配数组时也会应用这个推导指引:
std::shared_ptr sp{new int[10]}; // OOPS:推导出shared_ptr<int>
就像经常在C++
遇到的一样,我们陷入了一个讨厌的C问题:就是一个对象的指针和一个对象的数组拥有或者退化以后拥有相同的类型。
这个问题看起来很危险,因此C++
标准委员会决定不支持这么写。对于单个对象,你仍然必须这样调用:
std::shared_ptr<int> sp1{new int}; // OK
auto sp2 = std::make_shared<int>(); // OK
对于数组则要:
std::shared_ptr<std::string> p(new std::string[10],[] (std::string* p) {delete[] p;});
或者,使用实例化原生数组的智能指针的新特性,只需要:
std::shared_ptr<std::string[]> p{new std::string[10]};
相关文章:
C++17完整导引-模板特性之类模板参数推导
模板特性之类模板参数推导 使用类模板参数推导默认以拷贝方式推导推导lambda的类型没有类模板部分参数推导使用类模板参数推导代替快捷函数 推导指引使用推导指引强制类型退化非模板推导指引推导指引VS构造函数显式推导指引聚合体的推导指引标准推导指引pair和tuple的推导指引从…...

CSS3小可爱亲吻表白特效,给你的五一假期增添点小乐趣
马上五一假期了,小伙伴们是不是都准备出去旅游呢,或者回老家陪陪父母。今天我用CSS3制作一个小可爱亲吻表白的特效,来给你即将到来的五一假期增添点小小的乐趣。 目录 实现思路 左边小可爱的实现 右边小可爱的实现 左右摇摆动效的实现 右…...
Samba CentOS 7 安装
安装步骤 Samba是在Linux与Windows系统间共享文件和打印机的标准协议。要在CentOS上安装Samba,可以按以下步骤操作: 安装Samba相关包: yum install samba samba-client samba-common创建Samba配置文件/etc/samba/smb.conf: vim /etc/samba/smb.conf添加如下配置: [global]…...

Mac电脑 Vscode : Flutter 开发环境搭建(最细节教程)
参考链接: MacVSCode安装flutter环境_mac vscode配置flutter_GalenWu的博客-CSDN博客 mac搭建Flutter环境以及初始化项目 - 简书 注意: *下载xcode 就包含git了, *苹果芯片和intel 芯片需要的环境不同,苹果芯片需要安装: Im…...

BEVFormer 论文学习
1. 解决了什么问题? 3D 视觉感知任务,包括基于多相机图像的 3D 目标检测和分割,对于自动驾驶系统非常重要。与基于 LiDAR 的方法相比,基于相机图像的方法能够检测到更远距离的目标,识别交通信号灯、交通标识等信息。有…...

现在的00后,实在是太卷了,我们这些老油条都想辞职了......
现在的小年轻真的卷得过分了。前段时间我们公司来了个00年的,工作没两年,跳槽到我们公司起薪20K,都快要超过我了。 后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了。 最近和他聊了一次天,原来这位小老弟家…...
shell 数组定义与使用
一维数组 数组定义 array_name(value1 value2 ... value)也可以使用数字下表来定义数组 array_name[0]value0 array_name[1]value1 array_name[2]value2读取数组 ${array_name[index]}实例1 [rootiZj6c3slqbp8xuu2w3i4roZ devops]# cat array_name.sh #!/usr/bin/bashmy_…...

24 KVM管理虚拟机-配置VNC-TLS登录
文章目录 24 KVM管理虚拟机-配置VNC-TLS登录24.1 概述24.2 操作步骤 24 KVM管理虚拟机-配置VNC-TLS登录 24.1 概述 VNC服务端和客户端默认采用明文方式进行数据传输,因此通信内容可能被第三方截获。为了提升安全性,openEuler支持VNC服务端配置TLS模式进…...
C++基础讲解第六期(多态、虚函数、虚析构函数、dynamic_cast、typeid纯虚函数)
C基础讲解第六期 代码中也有对应知识注释,别忘看,一起学习! 一、多态1. 问题引出2. 多态的概念和使用3. 多态的原理4. 虚析构函数5. 动态类型识别(dynamic_cast)(1) 自定义类型(2). dynamic_cast(3). typeid 6. 纯虚函数 纯虚函数需要补充 一…...

防火墙之iptables(二)
防火墙之iptables(二) 一.SNAT原理与应用 1.应用环境 局域网主机共享单个公网IP地址接入Internet(私网不能被Internet中正常路由)2.SNAT原理 修改数据包的源地址内网访问外网 将从内网发送到外网的数据包的源IP由私网IP转换成…...
亚马逊销量暴跌该如何查找原因?
很多卖家经常遇到一个棘手的问题,就是突然会遇到链接销量暴跌的问题。 比如之前链接可以稳定出单10多单的,突然连续几天只有两三单,这到底是什么原因呢? 1.查看链接的类目是否被修改 这个类目修改不一定是卖家自己修改,更多的时…...
Vue中的脚手架和路由
私人博客 许小墨のBlog —— 菜鸡博客直通车 系列文章完整版,配图更多,CSDN博文图片需要手动上传,因此文章配图较少,看不懂的可以去菜鸡博客参考一下配图! 系列文章目录 前端系列文章——传送门 后端系列文章——传送…...

Convolutional Neural network(卷积神经网络)
目录 Why CNN for Image? The whole CNN structure Convolution(卷积) Max Pooling Flatten CNN in Keras What does CNN learn? what does filter do what does neuron do what about output Deep Dream Application Pla…...

【资料分享】高边、低边晶体管开关及电路解析
高边和低边晶体管开关 电路中,晶体管常常被用来当做开关使用。晶体管用作开关时有两种不同的接线方式:高边(high side)和低边(low side)。 高边和低边是由晶体管在电路中的位置决定的。晶体管可以是双极性晶体管(BJT…...

六级备考28天|CET-6|听力第二讲|长对话满分技巧|听写技巧|2022年6月考题|14:30~16:00
目录 1. 听力策略 2. 第一二讲笔记 3. 听力原文复现 (5)第五小题 (6)第六小题 (7)第七小题 (8)第八小题 扩展业务 expand business 4. 重点词汇 1. 听力策略 2. 第一二讲笔记 3. 听力原文复现 (5)第五小题 our guest is Molly Sundas, a university stud…...

计算机图形学 | 实验九:纹理贴图和天空盒
计算机图形学 | 实验九:纹理贴图和天空盒 计算机图形学 | 实验九:纹理贴图和天空盒实验概述顶点数据立方体顶点数据天空盒顶点数组 纹理载入创建纹理纹理读取纹理绑定 使用纹理立方体着色器顶点着色器片元着色器 天空盒着色器顶点着色器片元着色器 立方体…...

Unity A* Pathfinding Project
先下载免费版 https://arongranberg.com/astar/download# 教程首页 https://arongranberg.com/astar/docs/getstarted.html 创建一个plane 当地面 创建一个gameobject 添加组件 PathFinder 长这样 调整每个格子大小的 创建两个layer 一个是阻挡物的 一个是地面的 这里填入阻…...

SpringBoot ( 一 ) 搭建项目环境
1.搭建环境 1.1.创建项目向导 使用idea中的向导创建SpringBoot项目 1.1.1.建立新的项目 位置 : 菜单 > File > New > Project… 1.1.2.选择向导 默认的向导URL 是 https://start.spring.io 建议使用 https://start.aliyun.com 1.1.3.配置项目信息 Group : 组织…...

idea中关联Git
注意:未安装和配置Git软件,请先跳转到 Git宝典_没办法,我就是这么菜的博客-CSDN博客 idea关联git 关联git.exe 选择你的Version Control 下的Git 选择你的Git安装目录bin下的git.exe,点击ok 点击Test,显示版本号…...

Java面试知识点(全)-分布式微服务-zookeeper面试知识点
Java面试知识点(全) 导航: https://nanxiang.blog.csdn.net/article/details/130640392 注:随时更新 ZooKeeper是什么? ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现&…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...