И зачем этому учиться

Как и любой программист на C++, я трачу не менее половины своего времени на решение (и беспокойство) проблем управления памятью.

Несмотря на то, что со времен C++11 был достигнут большой прогресс (подумайте только об интеллектуальных указателях или принципе RAII), большинство разработчиков C++, даже самые наиболее опытным приходится смириться с мыслью, что любое их программное обеспечение, особенно если оно большое и написано несколькими людьми, может страдать от таких проблем, как утечки памяти, висячие указатели, переполнение буфера и т. д.

Подход Rust направлен на решение проблемы в корне: с набором правил Ownership компилятор просто не примет код, который может привести к одной из проблем управления памятью, которую вместо этого выявляет C++.

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

Управление памятью C++ подвержено ошибкам

«Просто позвоните delete, когда закончите выделение».

Просто не правда ли? Нет.
Подход C++ требует, чтобы программист вручную выделял и освобождал память с помощью таких функций, как new и delete.
Это может привести к различным проблемам, таким как утечки памяти, двойное освобождение и другие ошибки во время выполнения.
Например, утечки памяти, возникают, когда программа выделяет память, но никогда не освобождает ее, что приводит к постепенному накоплению неиспользуемой памяти, что в конечном итоге может привести к нехватке памяти для программы.

И это происходит:

  • Когда программисты просто забывают написать delete, и это не сразу ломается.
  • Когда несколько разработчиков работают вместе, потому что они могут делать разные предположения о том, кто должен освобождать ресурс.
  • Когда разные части кода должны использовать одни и те же данные.

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

Хотя C++ предоставляет некоторые инструменты для управления памятью, такие как интеллектуальные указатели и общий подход RAII, эти инструменты могут быть сложными для правильного использования, а также могут привести к снижению производительности. В результате программист должен тщательно управлять выделением и освобождением памяти, чтобы избежать подобных проблем.

Так почему бы не использовать сборщик мусора?

Потому что он медленный.
Языки программирования более высокого уровня, такие как Java или Python, используют сборщики мусора для упрощения управление памятью и повышение производительности: GC автоматически отслеживает, какая память используется, и автоматически освобождает память, которая больше не нужна.
Это может снизить вероятность ошибок, связанных с памятью, но:

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

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

Понимание модели владения Rust

Правил владения три (из официальной документации):
1. У каждого значения в Rust есть владелец.
2. Одновременно может быть только один владелец.
3. Когда владелец выходит за пределы области действия, значение будет удалено.

Таким образом, в Rust у каждого значения есть уникальный владелец, который отвечает за выделение и освобождение памяти:

fn main() {
    let s = String::from("hello"); // s is the owner of the String "hello"
    // do something with s...
} // when s goes out of scope, the memory allocated for "hello" is automatically deallocated

Право собственности может передаваться от одного владельца к другому, но в любой момент времени у ценности может быть только один владелец:

fn main() {
    let s1 = String::from("hello"); // s1 is the owner of the String "hello"
    let s2 = s1; // s2 takes ownership of the String "hello", and s1 is no longer the owner
    println!("{}", s1); // compile-time error: value borrowed here after move
}

Концепция, которую нужно понять, заключается в том, что s1 больше не является владельцем памяти, выделенной для строковых данных «hello». После передачи права собственности на s2 одновременно может быть только один владелец памяти.

Наконец, когда переменная выходит за пределы области видимости, Rust сбрасывает выделенную память, освобождая при этом и свою память:

fn main() {
    let s1 = String::from("hello"); // s1 is the owner of the String "hello"
    {
        let s2 = String::from("world"); // s2 is the owner of the String "world"
        println!("{} {}", s1, s2); // prints "hello world"
    } // s2 goes out of scope and drops the String "world", deallocating its memory
    println!("{}", s1); // prints "hello"
} // s1 goes out of scope and drops the String "hello", deallocating its memory

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

Таким образом, система владения Rust предназначена для предотвращения многих распространенных проблем управления памятью в C++ за счет применения набора правил для доступа к памяти и управления ею.

Две новые концепции собственности для понимания

Заимствование
Заимствование — это ссылка на значение, принадлежащее другой части кода. Когда значение заимствовано, заемщик может использовать его, но не может каким-либо образом изменить его. Это гарантирует, что владелец значения сохраняет контроль над своей памятью.
Однако заимствование также может быть изменяемым, это означает, что ссылка передается функции, которая может изменить заимствуемое значение. :

// change_string takes a mutable reference to a String
fn change_string(some_string: &mut String) {
    // it can modify the string, but it cannot take ownership of it.
    some_string.push_str(", world");
}
fn main() {
    let mut my_string = String::from("hello");
    change_string(&mut my_string);
    // change_string can borrow my_string temporarily, 
    // but it must give it back when it's done
    println!("{}", my_string);
}

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

Управление памятью в Rust и C++: плюсы и минусы

Как и все новые технологии и наука, Rust стоит на плечах гигантов, и многие из этих гигантов внесли свой вклад в C++.
Джимми Хартцелл,
RAII: Управление памятью во время компиляции в C++ и Rust

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

Бесспорно, Rust устанавливает очень строгие правила для достижения того, чего не может сделать C++, но вам нужно использовать его только некоторое время, чтобы убедиться, что игра стоит свеч. Давайте посмотрим причины.

  1. Это предотвращает распространенные ошибки памяти
    Поскольку система владения Rust применяет строгие правила в отношении владения и заимствования, гораздо сложнее случайно получить доступ или изменить память, которая уже была освобождена или не принадлежит текущая область.
    Например, в C++ можно случайно освободить память дважды, что вызовет ошибку двойного освобождения. В Rust это предотвращается системой владения, которая гарантирует, что у каждого значения одновременно будет только один владелец.
  2. Это снижает потребность в ручном управлении памятью
    Мы уже видели, насколько ручное управление памятью в C++ может быть подвержено ошибкам, особенно для больших проектов. Система владения Rust автоматизирует большую часть процесса управления памятью, автоматически выделяя и освобождая память для значений. Это позволяет программистам сосредоточиться на написании правильного и эффективного кода.
  3. Легче рассуждать
    Поскольку система владения Rust применяет строгие правила, легче рассуждать о поведении программ Rust. Программисты могут быть уверены, что их код будет вести себя предсказуемо и не будет иметь проблем, связанных с памятью.
  4. Более безопасная и надежная
    Ошибки, связанные с памятью, могут представлять серьезную угрозу безопасности, поскольку они могут позволить злоумышленникам выполнить произвольный код или получить доступ к конфиденциальным данным.

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

Однако владение может иметь и некоторые недостатки:

  1. Сложность написания определенных программ
    Строгие правила владения могут затруднить написание определенных типов программ, особенно тех, которые требуют общего или изменяемого доступа к данным.
  2. Владение может увеличить нагрузку на выполнение программы
    Для работы средству проверки заимствования в Rust может потребоваться выполнить дополнительные проверки и анализ, чтобы убедиться, что правила владения соблюдаются правильно.
  3. Дополнительные усилия для изучения
    Являясь совершенно иным подходом к другим языкам, право собственности может потребовать дополнительных усилий для изучения и эффективного использования.

Итак, теперь возникает вопрос: когда использовать C++, а когда Rust?
По моему мнению, те же самые причины, по которым C++ был лучшим выбором, чем другие языки несколько лет назад, являются причинами, по которым сейчас лучшим выбором является Rust.
Таким образом, вы можете использовать Rust вместо C++:

  • Когда вам нужна высокая производительность и низкоуровневый контроль
  • Когда защита памяти имеет первостепенное значение
  • Когда вам нужно мощное параллельное и параллельное программирование

Однако на сегодняшний день существуют ситуации, в которых мы все еще должны использовать C++:

  • При работе с устаревшим кодом
  • Когда вы работаете с большими кодовыми базами, где требуется обширная поддержка библиотек.
  • Когда вы разрабатываете высокопроизводительные научные вычисления, поскольку C++ по-прежнему имеет лучшие возможности для оптимизации кода для конкретных аппаратных платформ.

Выводы

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

Преимущества, связанные с управлением памятью, — это лишь один из аспектов, в которых Rust, на мой взгляд, лучше C++ в целом.
И, изучая Rust, понимаешь, что его существование было бы невозможно без C++: кто его разработал имел в виду, чего он не должен делать.

Но на практике причина, по которой я все чаще предпочитаю Rust C++, заключается в времени: строгий подход Rust к управлению памятью на самом деле может сэкономить много времени и усилий при отладке и тестировании.

И самое приятное то, что у вас есть возможность больше сосредоточиться на функциональности программного обеспечения, а не на «проблемах», таких как правильное распределение переменных.
Попробуйте полагать.

Значит, это конец C++?
Возможно, нет (или пока нет), но одно можно сказать наверняка: для программиста на C++ даже отдаленная надежда на отсутствие утечек памяти является более чем веской причиной для перехода на Rust.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу