C++ std::optional完全解读

引言

在编写可选择接受或返回对象的函数的时候,通常的做法是选择一个单独的布尔值来确保函数入参或者返回对象的可用性:

//使用is_valid来指示入参value是否有效
void maybe_take_an_int(int value = -1bool is_valid = false) 

//使用bool代表返回的int是否有效
bool maybe_return_an_int(int &value) //or
std::pair<int, bool> maybe_return_an_int()

上面的例子是可行的,但是本来我们只是为了传递或者得到一个value,但为了确保value的可用性需要增加一个布尔作为可用的指示,当调用者忘记bool简单调用maybe_take_an_int(1)时,该函数会默认失败,同样的在读取bool maybe_return_an_int(int &value)中的value值的时候,若忽略返回bool的值的时候,可能会导致读取的value是无效的。虽然在std::pair<int, bool> maybe_return_an_int()中使用了pair看似将两者进行了绑定,但是还是不能避免使用者忘记检查bool,导致使用了不可用的value

C++17中提供了std::optional<T>来解决这类问题,我们可以将optional<T>看作是T类型和bool的一个打包。其与std::pair<T, bool>相比其显示的表达意图,更加可读,而且可以良好地处理构造开销高昂的对象。使用std::optional<T>,上面的问题可变为

void maybe_take_an_int(optional<int> potential_value = nullopt); 

optional<int> maybe_return_an_int();

optional<T> 直接解决传递或存储当前可能或可能不是对象时出现的问题。optional<T>提供接口来确定它是否包含 并 T 查询存储的值。我们可以使用实际T值初始化 ,optional或者默认初始化它(或初始化为 std::nullopt )以将其置于“空”状态。

std::nullopt_t 是空类类型,用于指示optional类型拥有未初始化状态。

std::optional<T>基本用法介绍

std::optional<T>是一个管理一个可选的容纳值(既可以存在,也可以不存在的值)的类模板。任何一个std::optional<T>实例在给定的时间点要么含值,要么不含值。其在 <optional> 定义,函数原型如下:

template< class T >
class optional; //C++17 起
  • T:要为管理状态的值的类型,该校类型需要满足可析构克的要求。(特别是不允许数组类型)

成员函数

构造函数
//构造不含 值的对象。
constexpr optional() noexcept; //C++17 起
constexpr optional( std::nullopt_t ) noexcept; //C++17 起

/*复制构造函数:如果 other 包含值,那么初始化所含值,
如同以表达式 *other 直接初始化(但不是直接列表初始化)T 类型对象。
如果 other 不含值,那么构造一个不含 值的对象。*/
constexpr optional( const optional& other ); //C++17 起

/*移动构造函数:如果 other 含值,那么初始化所含值,
如同以表达式 std::move(*other) 直接初始化(但不是直接列表初始化) T 类型对象,
且不 令 other 为空:被移动的 std::optional 仍然包含 值,但该值自身是被移动的。*/
constexpr optional( optional&& other ) noexcept(); //C++17 起	

/*转换复制构造函数:如果 other 不含值,那么构造不含值的 optional 对象。
否则,构造含值的 optional 对象,如同以表达式 *other 直接初始化
(但不是直接列表初始化) T 类型对象一般初始化。*/
template < class U >
optional( const optional<U>& other ); //C++17 起, C++20 前 (条件性 explicit)
template < class U >
constexpr optional( const optional<U>& other ); //C++20 起 (条件性 explicit)

/*转换移动构造函数:如果 other 不含值,那么构造不含值的 optional 对象。
否则,构造含值的 optional 对象,如同以表达式 std::move(*other) 直接初始化
(但不是直接列表初始化) T 类型对象一般初始化。*/
template < class U >
optional( optional<U>&& other ); //C++17 起, C++20 前 (条件性 explicit)
template < class U >
constexpr optional( optional<U>&& other ); //C++20 起 (条件性 explicit)

/*构造一个包含的对象,如同从参数 std::forward<Args>(args)... 
直接初始化(但不是直接列表初始化) T 类型对象一般初始化。r*/
template< class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args ); //C++17 起

/*构造一个包含的对象,如同从参数 ilist, std::forward<Args>(args)... 
直接初始化(但不是直接列表初始化) T 类型对象一般初始化。*/
template< class U, class... Args >
constexpr explicit optional( std::in_place_t,
                             std::initializer_list<U> ilist,
                             Args&&... args ); //C++17 起

/*构造一个包含的对象,如同从参数 std::forward<U>(value) 
直接初始化(但不是直接列表初始化) T 类型对象一般初始化。*/                             
template < class U = T >
constexpr optional( U&& value ); //C++17 起 (条件性 explicit)

示例:

#include <iostream>
#include <optional>
#include <string>
 
int main()
{
    std::optional<int> o1, // 空
                       o2 = 1, // 从右值初始化
                       o3 = o2; // 复制构造函数
 
    // 调用 std::string( initializer_list<CharT> ) 构造函数
    std::optional<std::string> o4(std::in_place, {'a', 'b', 'c'});
 
    // 调用 std::string( size_type count, CharT ch ) 构造函数
    std::optional<std::string> o5(std::in_place, 3, 'A');
 
    // 从 std::string 移动构造,用推导指引拾取类型
 
    std::optional o6(std::string{"deduction"});
 
    std::cout << *o2 << ' ' << *o3 << ' ' << *o4 << ' ' << *o5  << ' ' << *o6 << '\n';
}

std::in_place是消除歧义的标签,其传递给ystd::optional的构造函数,用来指示原位构造对象。

输出:

1 1 abc AAA deduction
析构函数
~optional(); //C++17 起, C++20 前
constexpr ~optional(); //C++20 起
operator=
//若 *this 在调用前含值,则通过调用其析构函数销毁所含值,如同用 value().T::~T() 。此调用后 *this 不含值。
optional& operator=( std::nullopt_t ) noexcept; //C++17 起, C++20 前
constexpr optional& operator=( std::nullopt_t ) noexcept; //C++20 起

//赋值 other 的状态
constexpr optional& operator=( const optional& other ); //C++17 起

//赋值 other 的状态
constexpr optional& operator=( optional&& other ) noexcept(); //C++17 起

/*完美转发赋值:取决于 *this 在调用前是否含值,从 std::forward<U>(value) 直接初始化,
或从 std::forward<U>(value) 赋值被含有值。*/
template< class U = T >
optional& operator=( U&& value ); //C++17 起, C++20 前
template< class U = T >
constexpr optional& operator=( U&& value ); //C++20 起

//赋值 other 的状态
template< class U >
optional& operator=( const optional<U>& other ); //C++17 起, C++20 前
template< class U >
constexpr optional& operator=( const optional<U>& other ); //C++20 起

//赋值 other 的状态
template< class U >
optional& operator=( optional<U>&& other ); //C++17 起, C++20 前
template< class U >
constexpr optional& operator=( optional<U>&& other ); //C++20 起

示例:

#include <optional>
#include <iostream>
int main()
{
    std::optional<const char*> s1 = "abc", s2; // 构造函数
    s2 = s1; // 赋值
    s1 = "def"; // 衰变赋值( U = char[4], T = const char* )
    std::cout << *s2 << ' ' << *s1 << '\n';
}

输出:

abc def

观察器

访问所含值

optionalstd::optional<T>::operator->, std::optional<T>::operator*函数主要的作用就是用来访问所含值,其原型如下:

//返回指向所含值的指针。
constexpr const T* operator->() const noexcept; //C++17 起
constexpr T* operator->() noexcept; //C++17 起

//返回到所含值的引用。
constexpr const T& operator*() const& noexcept; //C++17 起
constexpr T& operator*() & noexcept; //C++17 起
constexpr const T&& operator*() const&& noexcept; //C++17 起
constexpr T&& operator*() && noexcept; //C++17 起

注:若*this不含值则行为未定义。

示例:

#include <optional>
#include <iostream>
#include <string>
 
int main()
{
    using namespace std::string_literals;
 
    std::optional<int> opt1 = 1;
    std::cout<< "opt1: "  << *opt1 << '\n';
 
    *opt1 = 2;
    std::cout<< "opt1: "  << *opt1 << '\n';
 
    std::optional<std::string> opt2 = "abc"s;
    std::cout<< "opt2: " << *opt2 << " size: " << opt2->size() << '\n';
 
    // 你能通过在到 optional 的右值上调用 operator* “取”其所含值
 
    auto taken = *std::move(opt2);
    std::cout << "taken: " << taken << " opt2: " << *opt2 << "size: " << opt2->size()  << '\n';
}

输出:

opt1: 1
opt1: 2
opt2: abc size: 3
taken: abc opt2: size: 0
检查对象是否含值

std::optional<T>::operator bool, std::optional<T>::has_value函数用来检查 *this 是否含值。若 *this 含值则为true,若*this不含值则 false 。 其函数原型为:

constexpr explicit operator bool() const noexcept; //C++17 起
constexpr bool has_value() const noexcept; //C++17 起
返回所含值

std::optional<T>::value函数返回所含值,若 *this 含值,则返回到所含值引用。否则,抛出 std::bad_optional_access 异常。 其函数原型如下:

constexpr T& value() &;
constexpr const T& value() const&; //C++17 起
constexpr T&& value() &&;
constexpr const T&& value() const&&; //C++17 起

:解引用运算符 operator*() 不检查此optional是否含值,它可能比value()更有效率。

示例:

#include <optional>
#include <iostream>
int main()
{
  std::optional<int> opt = {};

  try {
      int n = opt.value();
  } catch(const std::exception& e) {
      std::cout << e.what() << '\n';
  }
}

输出:

bad optional access
在所含值可用时返回它,否则返回默认值

std::optional<T>::value_or函数在*this 拥有值则返回其所含的值,否则返回 default_value 。其函数原型如下:

template< class U >
constexpr T value_or( U&& default_value ) const&; //C++17 起
template< class U >
constexpr T value_or( U&& default_value ) &&; //C++17 起

示例:

#include <optional>
#include <iostream>
#include <cstdlib>
 
std::optional<const char*> maybe_getenv(const char* n)
{
    if(const char* x = std::getenv(n))
       return x;
    else
       return {};
}
int main()
{
     std::cout << maybe_getenv("MYPWD").value_or("(none)") << '\n';
}

输出:

(none)

单子操作

and_then

std::optional<T>::and_then在所含值存在时返回对其应用给定的函数的结果,否则返回空的optional,其函数原型为:

template< class F >
constexpr auto and_then( F&& f ) &; //C++23 起

template< class F >
constexpr auto and_then( F&& f ) const&; //C++23 起

template< class F >
constexpr auto and_then( F&& f ) &&; //C++23 起

template< class F >
constexpr auto and_then( F&& f ) const&&; //C++23 起

若所含值存在则返回在其上调用 f 的结果。否则,返回返回类型的空值。
示例:

#include <iostream>
#include <optional>
std::optional<int> add5(int x)
{
  return x + 5;
}
std::optional<int> multiply2(int x)
{
  return x * 2;
}
int main()
{
  std::optional<int> x = 10;
  auto y = x.and_then(add5).and_then(multiply2);
  if (y)
  {
    std::cout << "Result: " << *y << std::endl; 
  }
  else
  {
    std::cout << "Result is empty" << std::endl;
  }
  std::optional<int> z;
  auto w = z.and_then(add5).and_then(multiply2);
  if (w)
  {
    std::cout << "Result: " << *w << std::endl;
  }
  else
  {
    std::cout << "Result is empty" << std::endl; // Output: Result is empty
  }
  return 0;
}

输出:

Result: 30
Result is empty
transform

std::optional<T>::transform函数在所含值存在时返回含有变换后的所含值的 optional,否则返回空的 optional,其函数原型如下:

template< class F >
constexpr auto transform( F&& f ) &; //C++23 起

template< class F >
constexpr auto transform( F&& f ) const&; //C++23 起

template< class F >
constexpr auto transform( F&& f ) &&; //C++23 起

template< class F >
constexpr auto transform( F&& f ) const&&; //C++23 起
		

*this含值则返回含有f在所含值上调用结果的 std::optional 。否则返回这种类型的空 std::optional

示例:

#include <iostream>
#include <optional>
 
struct A { /* ... */ };
struct B { /* ... */ };
struct C { /* ... */ };
struct D { /* ... */ };
 
auto A_to_B(A) -> B { /* ... */ std::cout << "A => B \n"; return {}; }
auto B_to_C(B) -> C { /* ... */ std::cout << "B => C \n"; return {}; }
auto C_to_D(C) -> D { /* ... */ std::cout << "C => D \n"; return {}; }
 
void try_transform_A_to_D(std::optional<A> o_A)
{
    std::cout << (o_A ? "o_A has a value\n" : "o_A is empty\n");
 
    std::optional<D> o_D = o_A.transform(A_to_B)
                              .transform(B_to_C)
                              .transform(C_to_D);
 
    std::cout << (o_D ? "o_D has a value\n\n" : "o_D is empty\n\n");
};
 
int main()
{
    try_transform_A_to_D( A{} );
    try_transform_A_to_D( {} );
}

输出:

o_A has a value
A => B
B => C
C => D
o_D has a value
 
o_A is empty
o_D is empty
or_else

std::optional<T>::or_else函数在 optional 含值时返回自身,否则返回给定函数的结果。其函数原型如下:

template< class F >
constexpr optional or_else( F&& f ) const&; //C++23 起
template< class F >
constexpr optional or_else( F&& f ) &&; //C++23 起

*this 含值则返回它。否则返回 f 的结果。

修改器

swap
void swap( optional& other ) noexcept(/* see below */); //C++17 起, C++20 前
constexpr void swap( optional& other ) noexcept(/* see below */); //C++20 起

std::optional<T>::swap用来与 other 交换内容。

示例:

#include <iostream>
#include <string>
#include <optional>
 
int main()
{
    std::optional<std::string> opt1("First example text");
    std::optional<std::string> opt2("2nd text");
 
    enum Swap { Before, After };
    auto print_opts = [&](Swap e) {
        std::cout << (e == Before ? "Before swap:\n" : "After swap:\n");
        std::cout << "opt1 contains '" << opt1.value_or("") << "'\n";
        std::cout << "opt2 contains '" << opt2.value_or("") << "'\n";
        std::cout << (e == Before ? "---SWAP---\n": "\n");
    };
 
    print_opts(Before);
    opt1.swap(opt2);
    print_opts(After);
 
    // 在仅一者含值时交换
    opt1 = "Lorem ipsum dolor sit amet, consectetur tincidunt.";
    opt2.reset();
 
    print_opts(Before);
    opt1.swap(opt2);
    print_opts(After);
}

输出:

Before swap:
opt1 contains 'First example text'
opt2 contains '2nd text'
---SWAP---
After swap:
opt1 contains '2nd text'
opt2 contains 'First example text'
 
Before swap:
opt1 contains 'Lorem ipsum dolor sit amet, consectetur tincidunt.'
opt2 contains ''
---SWAP---
After swap:
opt1 contains ''
opt2 contains 'Lorem ipsum dolor sit amet, consectetur tincidunt.'
reset
void reset() noexcept; //C++17 起, C++20 前
constexpr void reset() noexcept; //C++20 起

std::optional<T>::reset用来销毁任何所含值,若 *this 含值,则如同用value().T::~T()销毁此值。否则无效果。
*this 在此调用后不含值。

示例:

#include <optional>
#include <iostream>
 
struct A {
    std::string s;
    A(std::string str) : s(std::move(str))  { std::cout << " constructed\n"; }
    ~A() { std::cout << " destructed\n"; }
    A(const A& o) : s(o.s) { std::cout << " copy constructed\n"; }
    A(A&& o) : s(std::move(o.s)) { std::cout << " move constructed\n"; }
    A& operator=(const A& other) {
        s = other.s;
        std::cout << " copy assigned\n";
        return *this;
    }
    A& operator=(A&& other) {
        s = std::move(other.s);
        std::cout << " move assigned\n";
        return *this;
    }
};
 
int main()
{
    std::cout << "Create empty optional:\n";
    std::optional<A> opt;
 
    std::cout << "Construct and assign value:\n";
    opt = A("Lorem ipsum dolor sit amet, consectetur adipiscing elit nec.");
 
    std::cout << "Reset optional:\n";
    opt.reset();
    std::cout << "End example\n";
}

输出:

Create empty optional:
Construct and assign value:
 constructed
 move constructed
 destructed
Reset optional:
 destructed
End example
emplace

std::optional<T>::emplace用来原位构造所含值 ,其函数原型如下:

//以 std::forward<Args>(args)... 为参数直接初始化(但不是直接列表初始化)所含值。
template< class... Args >
T& emplace( Args&&... args ); //C++17 起, C++20 前
template< class... Args >
constexpr T& emplace( Args&&... args ); //C++20 起

//以 ilist, std::forward<Args>(args)... 为参数直接初始化(但不是直接列表初始化)所含值。
template< class U, class... Args >
T& emplace( std::initializer_list<U> ilist, Args&&... args ); //C++17 起, C++20 前
template< class U, class... Args >
constexpr T& emplace( std::initializer_list<U> ilist, Args&&... args ); //C++20 起

*this已在此调用前含值,则调用其析构函数销毁所含值。

示例:

#include <optional>
#include <iostream>
 
struct A {
    std::string s;
    A(std::string str) : s(std::move(str))  { std::cout << " constructed\n"; }
    ~A() { std::cout << " destructed\n"; }
    A(const A& o) : s(o.s) { std::cout << " copy constructed\n"; }
    A(A&& o) : s(std::move(o.s)) { std::cout << " move constructed\n"; }
    A& operator=(const A& other) {
        s = other.s;
        std::cout << " copy assigned\n";
        return *this;
    }
    A& operator=(A&& other) {
        s = std::move(other.s);
        std::cout << " move assigned\n";
        return *this;
    }
};
 
int main()
{
    std::optional<A> opt;
 
    std::cout << "Assign:\n";
    opt = A("Lorem ipsum dolor sit amet, consectetur adipiscing elit nec.");
 
    std::cout << "Emplace:\n";
    // 由于 opt 含值,这亦将销毁该值
    opt.emplace("Lorem ipsum dolor sit amet, consectetur efficitur. ");
 
    std::cout << "End example\n";
}

输出:

Assign:
 constructed
 move constructed
 destructed
Emplace:
 destructed
 constructed
End example
 destructed

非成员函数

比较 optional 对象

operator==, !=, <, <=, >, >=, <=>(std::optional)函数用来比较optional对象。

示例:

#include <optional>
#include <iostream>

int main()
{
    std::optional<int> oEmpty;
    std::optional<int> oTwo(2);
    std::optional<int> oTen(10);

    std::cout << std::boolalpha;
    std::cout << (oTen > oTwo) << "\n";
    std::cout << (oTen < oTwo) << "\n";
    std::cout << (oEmpty < oTwo) << "\n";
    std::cout << (oEmpty == std::nullopt) << "\n";
    std::cout << (oTen == 10) << "\n";
}

输出:

true  
false 
true 
true  
true  
make_optional

std::make_optional函数用来创建一个optional对象 。其函数原型如下:

//从value 创建 optional 对象。
template< class T >
constexpr std::optional<std::decay_t<T>> make_optional( T&& value ); //C++17 起

//从 args... 创建原位构造的 optional 对象。
template< class T, class... Args >
constexpr std::optional<T> make_optional( Args&&... args ); //C++17 起

//从 il 和 args... 创建原位构造的 optional 对象。
template< class T, class U, class... Args >
constexpr std::optional<T> make_optional( std::initializer_list<U> il, Args&&... args ); //C++17 起

示例:

#include <optional>
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
 
int main()
{
    auto op1 = std::make_optional<std::vector<char>>({'a','b','c'});
    std::cout << "op1: ";
    for (char c: op1.value()){
        std::cout << c << ",";
    }
    auto op2 = std::make_optional<std::vector<int>>(5, 2);
    std::cout << "\nop2: ";
    for (int i: *op2){
        std::cout << i << ",";
    }
    std::string str{"hello world"};
    auto op3 = std::make_optional<std::string>(std::move(str));
    std::cout << "\nop3: " << quoted(op3.value_or("empty value")) << '\n';
    std::cout << "str: " << std::quoted(str) << '\n';
}

输出:

op1: a,b,c,
op2: 2,2,2,2,2,
op3: "hello world"
str:
std::swap(std::optional)

std::swap(std::optional)是特化 std::swap 算法 ,其函数原型如下:

template< class T >

void swap( std::optional<T>& lhs,
           std::optional<T>& rhs ) noexcept(/* see below */); //C++17 起, C++20 前
template< class T >

constexpr void swap( std::optional<T>& lhs,
                     std::optional<T>& rhs ) noexcept(); //C++20 起

std::optional重载 std::swap 算法。交换lhsrhs 的状态。等效地调用 lhs.swap(rhs)
示例:


#include <iostream>
#include <optional>
#include <string>
 
int main()
{
    std::optional<std::string> a{"██████"}, b{"▒▒▒▒▒▒"}; 
 
    auto print = [&](auto const& s) {
        std::cout 
            << s << "\t"
            << "a = " << a.value_or("(null)") << "  "
            << "b = " << b.value_or("(null)") << '\n';
    };
 
    print("Initially:");
    std::swap(a, b);
    print("swap(a, b):");
    a.reset();
    print("\n""a.reset():");



    std::swap(a, b);
    print("swap(a, b):");
}

输出:

Initially:	a = ██████  b = ▒▒▒▒▒▒
swap(a, b):	a = ▒▒▒▒▒▒  b = ██████
a.reset():	a = (null)  b = ██████
swap(a, b):	a = ██████  b = (null)

何时使用

通常在以下情况下会用到std::optional

  • 当我们想很好地表示一个可能会为空的类型的时候。
  • 函数返回一些处理结果,该结果无法生成值,但该结果并不是错误。
  • 执行资源的延时加载。
  • 将可选参数传递到函数中。

使用示例

函数返回 std::optional

如果从函数返回可选值,则仅 std::nullopt 返回或计算值非常方便。

std::optional<std::string> TryParse(Input input)
{
    if (input.valid())
        return input.asString();

    return std::nullopt;
}
延时初始化

在某个类初始化的时候,由于某种原因,其某个成员还不能被初始化,也就是说该类初始化的时候需要选择性的初始化它的成员,其某个成员需要在稍晚时间或者在发生某个动作后才能够被初始化。如果我们要实现这种功能,使用optional会非常方便:

using T = /* some type */;
 
 struct S {
   optional<T> maybe_T;
   
   void construct_the_T(int arg) {
     // 我们无需处理重复构造所带来的问题,因为
     // optional的emplace member会自动销毁所
     // 之前存在的对象并构建一个新对象。
     maybe_T.emplace(arg);
   }
   
   T& get_the_T() {
     assert(maybe_T);
     return maybe_T.value();
   }
   
   // ... 接下来的拷贝和构造函数就能保证问题不会处在optional这里啦!...
 };

optional 特别适合延迟初始化问题,因为它本身就是延迟初始化的实例。所包含 T 的内容可以在构造时初始化,也可以在以后的某个时间初始化,或者永远不会初始化。任何包含 T 的内容都必须在销毁时 optional 销毁。

作为可选参数
#include <optional>
#include <iostream>

class UserRecord
{
public:
    UserRecord (const std::string& name, std::optional<std::string> nick, std::optional<int> age)
    : mName{name}, mNick{nick}, mAge{age}
    {
    }

    friend std::ostream& operator << (std::ostream& stream, const UserRecord& user);

private:
    std::string mName;
    std::optional<std::string> mNick;
    std::optional<int> mAge;

};

std::ostream& operator << (std::ostream& os, const UserRecord& user) 
{
    os << user.mName << ' ';
    if (user.mNick) {
        os << *user.mNick << ' ';
    }
    if (user.mAge)
        os << "age of " << *user.mAge;

    return os;
}

int main()
{
    UserRecord tim { "Tim", "SuperTim", 16 };
    UserRecord nano { "Nathan", std::nullopt, std::nullopt };

    std::cout << tim << "\n";
    std::cout << nano << "\n";
}

总结

对于std::optional我们注意以下几点:

  • std::optional 是表示“可为空”类型的包装器类型。
  • std::optional不会使用任何动态分配。
  • std::optional包含值或为空。
    • 使用 operator * operator-> value() value_or() 访问基础值。
  • std::optional 隐式转换为bool ,以便我们可以轻松检查它是否包含值。

当我们需要一个具有延迟初始化的对象、或者用来表达valueno value的时候,我们可以使用std::optional来将使用其类型提高抽象的级别,使其他人更加容易理解我们大代码在做什么,因为声明 optional<T> f(); void g(optional<T>);表达意图比做 pair<T, bool> f(); void g(T t, bool is_valid); 更清晰简洁。

文章首发公众号:iDoitnow如果喜欢话,可以关注一下

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艰默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值