Что такое Rust-эквивалент Shared_ptr C++?

Почему этот синтаксис не разрешен в Rust:

fn main() {
    let a = String::from("ping");
    let b = a;

    println!("{{{}, {}}}", a, b);
}

Когда я попытался скомпилировать этот код, я получил:

error[E0382]: use of moved value: `a`
 --> src/main.rs:5:28
  |
3 |     let b = a;
  |         - value moved here
4 | 
5 |     println!("{{{}, {}}}", a, b);
  |                            ^ value used here after move
  |
  = note: move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait

На самом деле мы можем просто сделать ссылку, которая не будет работать во время выполнения:

fn main() {
    let a = String::from("ping");
    let b = &a;

    println!("{{{}, {}}}", a, b);
}

И это работает:

{ping, ping}

Согласно книге Rust это сделано для того, чтобы избежать двойных бесплатных ошибок, потому что переменные Rust копируются по ссылке, а не по значению. Rust просто аннулирует первый объект и сделает его непригодным для использования...

введите описание изображения здесь

Мы должны сделать что-то вроде этого:

введите описание изображения здесь

Мне нравится идея копирования по ссылке, но зачем автоматически аннулировать первую?

Должна быть возможность избежать двойного бесплатного использования другим методом. Например, в C++ уже есть отличный инструмент для разрешения нескольких бесплатных вызовов... shared_ptr вызывает free только тогда, когда никакой другой указатель не указывает на объект — похоже, это очень похоже на то, что мы делаем на самом деле, с той разницей, что shared_ptr имеет счетчик.

Например, мы можем подсчитать количество ссылок на каждый объект во время компиляции и вызывать free только тогда, когда последняя ссылка выходит из области видимости.

Но Rust — молодой язык; может быть, у них не было времени реализовать что-то подобное? Планирует ли Rust разрешать вторую ссылку на объект, не аннулируя первую, или нам следует взять за привычку работать только со ссылкой на ссылку?


person tirz    schedule 14.04.2018    source источник


Ответы (2)


Либо Rc, либо Arc заменяет shared_ptr. Что вы выберете, зависит от того, какой уровень потокобезопасности вам нужен для общих данных; Rc — для случаев без потоков, а Arc — когда вам нужны потоки:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));
    let b = a.clone();

    println!("{{{}, {}}}", a, b);
}

Как и shared_ptr, это не копирует сам String. Он только увеличивает счетчик ссылок во время выполнения, когда вызывается clone, и уменьшает счетчик, когда каждая копия выходит за пределы области действия.

В отличие от shared_ptr, Rc и Arc имеют лучшую семантику потока. shared_ptr является полупотокобезопасным. Счетчик ссылок shared_ptr сам по себе потокобезопасен, но общие данные не "волшебным образом" сделаны потокобезопасными.

Если вы используете shared_ptr в многопоточной программе, вам еще предстоит проделать большую работу, чтобы обеспечить ее безопасность. В программе без потоков вы платите за некоторую безопасность потоков, которая вам не нужна.

Если вы хотите разрешить изменение общего значения, вам также необходимо переключиться на проверку заимствования во время выполнения. Это обеспечивается такими типами, как Cell, RefCell, Mutex и т. д. RefCell подходит для String и Rc:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let a = Rc::new(RefCell::new(String::from("ping")));
    let b = a.clone();

    println!("{{{}, {}}}", a.borrow(), b.borrow());

    a.borrow_mut().push_str("pong");
    println!("{{{}, {}}}", a.borrow(), b.borrow());
}

мы можем подсчитать количество ссылок на каждый объект во время компиляции и вызывать free только тогда, когда последняя ссылка выходит из области видимости.

Это почти то же самое, что Rust делает со ссылками. Он на самом деле не использует счетчик, а только позволяет вам использовать ссылки на значение, в то время как это значение гарантированно останется по тому же адресу памяти.

shared_ptr в C++ не делает это во время компиляции. shared_ptr, Rc и Arc — это конструкции времени выполнения, поддерживающие счетчик.

Можно ли сделать ссылку на объект, не аннулируя первую ссылку?

Это именно то, что Rust делает со ссылками, и то, что вы уже сделали:

fn main() {
    let a = String::from("ping");
    let b = &a;

    println!("{{{}, {}}}", a, b);
}

Более того, компилятор запретит вам использовать b, как только a станет недействительным.

потому что переменные Rust копируются по ссылке, а не по значению

Это неправда. Когда вы присваиваете значение, право собственности на значение передается новой переменной. Семантически адрес памяти переменной изменился, и поэтому чтение этого адреса может привести к небезопасности памяти.

мы должны принять привычку работать только со ссылкой

Да, использование ссылок, когда это возможно, является наиболее идиоматичным выбором. Они требуют нулевых накладных расходов во время выполнения, и компилятор сообщит вам об ошибках, а не обнаружит их во время выполнения.

Конечно, бывают случаи, когда Rc или Arc полезны. Часто они нужны для циклических структур данных. Вы не должны расстраиваться из-за их использования, если вы не можете получить простые ссылки для работы.

со ссылкой на ссылку?

Это немного обратная сторона, так как дополнительная косвенность вызывает сожаление. Если вам действительно нужно, вы можете уменьшить его. Если вам не нужно изменять строку, вы можете вместо этого переключиться на Rc<str>:

use std::rc::Rc;

fn main() {
    let a: Rc<str> = Rc::from("ping");
    let b = a.clone();

    println!("{{{}, {}}}", a, b);
}

Если вам нужно сохранить возможность иногда изменять String, вы также можете явно преобразовать &Rc<T> в &T:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));
    let b = a.clone();

    let a_s: &str = &*a;
    let b_s: &str = &*b;

    println!("{{{}, {}}}", a_s, b_s);
}

Смотрите также:

person Shepmaster    schedule 14.04.2018

Может быть, мы можем просто подсчитать количество ссылок на каждый объект во время компиляции и вызывать free только тогда, когда последняя ссылка выходит за пределы области видимости.

Вы на правильном пути! Вот для чего нужен Rc. Это тип интеллектуального указателя, очень похожий на std::shared_ptr в C++. Он освобождает память только после того, как последний экземпляр указателя вышел за пределы области видимости:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));

    // clone() here does not copy the string; it creates another pointer
    // and increments the reference count
    let b = a.clone();

    println!("{{{}, {}}}", *a, *b);
}

Поскольку вы получаете неизменяемый доступ только к содержимому Rc (в конце концов, оно является общим, а совместное изменение запрещено в Rust), вам нужна внутренняя изменяемость, чтобы иметь возможность изменять его содержимое, реализованное через Cell или RefCell:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let a = Rc::new(RefCell::new(String::from("Hello")));
    let b = a.clone();

    a.borrow_mut() += ", World!";

    println!("{}", *b); // Prints "Hello, World!"
}

Но в большинстве случаев вам не нужно использовать Rc (или его потокобезопасного брата Arc). Модель владения Rust в основном позволяет вам избежать накладных расходов на подсчет ссылок, объявляя экземпляр String в одном месте и используя ссылки на него повсюду, как вы сделали во втором фрагменте. Попробуйте сосредоточиться на этом и используйте Rc только в случае крайней необходимости, например, когда вы реализуете графоподобную структуру.

person Fabian Knorr    schedule 14.04.2018
comment
Спасибо за комментарий о clone() :) - person tirz; 14.04.2018
comment
Да, это анти-шаблон, но отличный пример того, что a все еще можно использовать. - person tirz; 14.04.2018