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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
c templates是一种在C++中使用的要特,它允许我们编写通用的代码,可以在不同的数据上进行操作。《c templates - the complete guide, 2nd edition》是一本介绍c++模板完整指南的书籍,可以帮助读者理解并掌握该特的使用。 这本书的第二版可以在GitHub上以PDF形式获取。GitHub是一个以源代码为中心的开发平台,用户可以在上面共享和存储代码。通过在GitHub上发布这本书的PDF版本,可以让更多的人免费获取和阅读。这为那些对C++模板感兴趣的开发者提供了一个要的资料。 《c templates - the complete guide, 2nd edition》这本书的第二版相比第一版进行了更新和修订,增加了更多的内容和例子,以帮助读者更好地理解和应用c++模板。它涵盖了模板本概念、使用方法和高级技术,包括模板元编程和模板特化等内容。这本书还提供了丰富的示例代码和实践建议,以帮助读者深入学习和探索C++模板的强大功能。 通过阅读该书,读者可以获得对C++模板的全面了解,并学会如何正确地使用它们来编写更高效、更灵活的代码。无论是初学者还是有经验的开发者,都可以从这本书中获得知识和技巧,提高他们的编程水平。 总之,《c templates - the complete guide, 2nd edition》是一本介绍C++模板要参考书,它的PDF版本可以在GitHub上获取。通过阅读该书,开发者可以更好地理解和应用C++模板,提高他们的编程技能和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值