unique_ptr 作为函数入参的使用方法

【C++】unique_ptr作为函数入参的使用方法

1. 各种情况举例

1.1 作为值传递,实参为左值(错误)

void func(std::unique_ptr<int> in_ptr) { // 值传递
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(ptr); // 通过左值传递
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 编译出错

std::unique_ptr不可复制,只能移动。func通过值传递接收一个std::unique_ptr,会尝试使用拷贝构造函数,但std::unique_ptr的拷贝构造函数是被删除的,所以会导致编译错误。

所以需要使用std::moveptr转换为右值引用,从而触发std::unqiue_ptr的移动构造函数。

1.2 作为值传递,实参为右值引用

void func(std::unique_ptr<int> in_ptr) { // 值传递
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(std::move(ptr); // 右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 ptr is nullptr

函数调用时,会用传进来的实参ptr构造参数in_ptr的副本,因为std::uniptr_ptr不可复制,只能移动,所以调用std::unique_ptr的移动构造函数,将

所有权从实参ptr转移给了临时副本in_ptr,只要函数返回,资源的生命周期就结束了,即使func中存在auto local_ptr = std::move(in_ptr),因为将所有权转移给了local_ptr局部变量,在函数返回时,local_ptr会被销毁,指向的资源会被释放。

1.3 作为右值引用传递,实参为右值引用

void func(std::unique_ptr<int> &&in_ptr) {
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(std::move(ptr)); // 右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20

右值引用不会触发std::unique_ptr的移动构造函数或移动赋值函数,不会转移ptr的所有权,可以认为in_ptr只是ptr的一个别名。只要在func中没有转移所有权,func返回后,ptr仍然拥有资源的所有权。

但是如果func中有资源所有权的转移,则ptr会失去所有权。

void func(std::unique_ptr<int> &&in_ptr) {
    *in_ptr = 20;
    auto local_ptr = std::move(in_ptr); // 移动构造
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(std::move(ptr)); // 右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 ptr is nullptr

因为在auto local_ptr = std::move(in_ptr)中,会触发unique_ptr的移动构造函数,所有权会被转移。当func返回后,local_ptr生命周期结束,所有用的资源会被释放。

1.4 作为引用传递,实参为左值

void func(std::unique_ptr<int> &in_ptr) {
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(ptr); //左值
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20

ptr本身被传递,而不是它的副本,所以参数传递的时候不会触发std::unique_ptr的移动构造。只要在func中没有转移所有权,func返回后,ptr仍然拥有资源的所有权。

但是如果func中有资源所有权的转移,则ptr会失去所有权。

void func(std::unique_ptr<int> &in_ptr) {
    *in_ptr = 20;
    auto local_ptr = std::move(in_ptr); // 移动构造
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(ptr); //左值
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 ptr is nullptr

因为在auto local_ptr = std::move(in_ptr)中,会触发unique_ptr的移动构造函数,所有权会被转移。当func返回后,local_ptr生命周期结束,所拥有的资源会被释放。

1.5 作为传递指针

void func(int *in_ptr) {
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(ptr.get()); // 原始指针
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20

使用原始指针可以访问ptr管理的内存,并且不会影响ptr的所有权。但这种方式存在潜在的危险,违反智能指针设计初衷。

2. 有const修饰的情况

2.1 const左值引用

const修饰的是引用本身,即in_ptr本身不能被修改,但指向的内容能否修改需要看in_ptr指向的资源是否是const的。

void func(const std::unique_ptr<int> &in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(ptr); //左值
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20
void func(const std::unique_ptr<const int> &in_ptr) { // in_ptr本身不能修改,并且指向的资源也不可以修改
    *in_ptr = 20; // ❌ 错误: 指向的内容不允许修改
}
int main() {
    std::unique_ptr<const int> ptr(new int(10));
    func(ptr); //左值
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 编译错误

在C++中,任何值可以绑定到const左值引用,因为const保证了不会修改原来对象。

void func(const std::unique_ptr<int> &in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(std::move(ptr)); // 右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20

在这里,std::move其实有点多余,因为并不会触发移动构造,而且容易误解为意图转移所有权。

2.2 const右值引用

const左值引用相同, const修饰的是引用本身,即in_ptr本身不能被修改,但指向的内容能否修改需要看in_ptr指向的资源是否是const的。

void func(const std::unique_ptr<int> &&in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
    *in_ptr = 20;
}
int main() {
    std::unique_ptr<int> ptr(new int(10));
    func(std::move(ptr)); //右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 终端输出 *ptr = 20
void func(const std::unique_ptr<const int> &&in_ptr) { // in_ptr本身不能修改,指向的资源也不可以修改
    *in_ptr = 20; // ❌ 错误: 指向的内容不允许修改
}
int main() {
    std::unique_ptr<const int> ptr(new int(10));
    func(std::move(ptr)); //右值引用
    if (!ptr)
        std::cout << "ptr is nullptr" << std::endl;
    else
        std::cout << "*ptr = " << *ptr << std::endl;
    return 0;
}
// 编译错误

不推荐使用,没有任何意义。使用右值引用主要意义是用来支持移动语义,当使用右值引用时意图是可能需要转移对象的所有权了,但加了const就不允许修改对象本身了,这就造成了矛盾。

3 总结

  1. 入参形式为左值引用或右值引用,从结果表现上来看,完全一致。但一般:
  • 当需要直接操作std::unique_ptr对象,但不改变其所有权的情况下,函数入参使用左值引用,建议使用const std::unique_ptr<T> &类型作为函数入参;
  • 当可能会改变std::unique_ptr对象的所有权的情况下,对象在函数调用后可能会变成nullptr,函数入参使用右值引用,建议使用std::unique_ptr<T> &&类型作为函数入参。【注意:转移所有权后,原对象相当于一个空壳,可以重新赋值,但不能再使用它。】 【不止针对std::unique_ptr,其他类型同样是这样的,但对应没有资源转移的场景,右值引用没有实际意义。】
  1. 是否转移了资源的所有权,只需要抓住一个点:是否触发了std::unique_ptr的移动构造或移动赋值

std::move只是生成一个右值表达式,其类型是右值引用类型(结果是临时的),从而允许资源被转移,和资源转移没有实质上的关系。

比如:std::move(val)本身是不会影响val的,真正转移val的资源所有权的是移动构造或移动赋值操作。

std::string a = "hello";
std::string b = std::move(a);  // 触发移动构造,a 的资源转移给了 b
// a 本身还是 std::string类型,仍然存在,但资源被转移了
// std::move(a) 是 std::string &&类型
std::sting a = "hello";
std::string &&b = std::move(a); // 右值引用变量 b 的初始化绑定,不触发移动构造
// a 本身还是 std::string类型,仍然存在,资源还没有被转移
// std::move(a) 是 std::string &&类型
  1. 【混淆点】左值和左值引用,右值和右值引用,引用(引用变量)和引用类型

左值和右值是值的分类,与值的类型没有关系。

广义上的引用包括左值引用和右值引用,指的是具体的变量别名,所以引用(不管是左值引用,还是右值引用)都是左值。引用本身不是一个对象,也不占用独立的内存。

比如std::unique_ptr<int> &&ptr2 = std::move(ptr1),这里ptr2是一个右值引用,但本身是一个左值。

在 C++ 中,引用(包括右值引用)必须在初始化时就绑定一个对象,不能像普通变量那样先声明,再定义。引用一旦绑定,永远不能重新绑定到别的对象上。

引用类型是一种变量类型,比如int&std::unique_ptr<T> &&等,可以类比于intdouble变量类型。

右值只能在右值引用变量初始化时绑定到右值引用变量。

std::string&& r = std::string("hello");  // ✅ 这是定义并初始化 r,用右值绑定
// ===========================================================================
std::string&& r;                // ❌ 只定义,未初始化(非法,其实不能这么写)
r = std::string("world");       // ❌ 错误:不是初始化,不能用右值赋值给右值引用本身

右值引用变量之间可以移动赋值,这不是重新绑定右值,而是对引用的对象进行赋值,绑定的对象是不变的,只是对象的内容发生了变化。

std::string&& a = std::string("hello");
std::string&& b = std::string("world");
a = std::move(b);  // 移动赋值 ✅ 合法,但注意:这是把 b 所引用的对象移动赋值给 a 所引用的对象,a所引用的对象本身(那块内存)位置是不变的
a = b; // 拷贝赋值 ✅ 合法,但注意:这是把 b 所引用的对象移动赋值给 a 所引用的对象,a所引用的对象本身(那块内存)位置是不变的
  1. 【混淆点】绑定到绑定
int a = 2;
int &b = a;

✅【推荐说法】a绑定到(bind to)b

❌【不推荐说法】b绑定a

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇