Pull to refresh

Comments 147

Да-да, сверху выглядит очень круто. А потом начинается обмазывание явным указанием времени жизни, десятком трейтов которые нужно протащить (Debug, Send, Sync, WhoKnowWhatElse), .unwrap()-ы на пустом месте...

Мне тоже понравилась его экосистема и управление памятью. Даже сам процесс сборки можно кастомизировать раст-кодом (build.rs ) - это ж огонь!
Но любить раст за то что можно сделать полиморфизм на трейтах вместо интерфейсов - странно.

явным указанием времени жизни

Явные указания времен жизни на самом деле нужны довольно редко, в 80% случаев если они понадобились то человек занимается несвоевременной оптимизацией или пытается выстрелить себе в ногу. Покажите кстати пожалуйста какой-нибудь кейс если не лень где вам понадобились явные указания времен жизни.

десятком трейтов которые нужно протащить

trait SomethingSomething: Debug + Send + Sync + WhoKnowWhatElse {}

impl<T> SomethingSomething for T
    where T: Debug + Send + Sync + WhoKnowWhatElse {}

.unwrap()-ы на пустом месте

Не unwrapайте на пустом месте :) Обработайте ошибку нормально, а если не хотите - пусть компилятор заставит вас написать шесть букв для явного, осознанного опт-аута, и правильно сделает. И заодно когда ваша очевидная и точно-точно правильная эвристика на тему того, почему тут никак не может быть None, таки окажется ошибочной, в сообщении об ошибке будет указание файла и строки, где она находится.

Косяки в языке есть, но не совсем те которые вы перечислили.

Без сарказма - а какие бы косяки перечислили бы вы? Хочется лучшего представления о языке.

Тяжело работать с древовидными структурами данных. Вроде и всё правильно компилятор пишет, а работать всё равно тяжело.

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

Математические концепции нормально не выразить, такие трейты как "вещественное число" или там "кольцо" нужно писать самому (а потом подгонять под примитивные типы макросами!)

Математические концепции нормально не выразить, такие трейты как "вещественное число" или там "кольцо" нужно писать самому (а потом подгонять под примитивные типы макросами!)

Или просто использовать num-traits, в которых это уже сделано за тебя. Но без обобщённых литералов не совсем удобно.

Первое: отсутствие negative trait bounds и специализации.

Поскольку в языке очень много завязано на трейтах и blanket implementations (которые автор оригинальной статьи называет "утиной типизацией"), то это прям досадный косяк, приходится идти на всякие хаки чтобы его обходить.

Пример на который я натолкнулся буквально вчера: есть такая популярная библиотека anyhow. Она экспортирует тип anyhow::Error, в который можно конвертировать любой конкретный тип Error из любой библиотеки, или просто текстовое сообщение, и т.п. В результате у вас получается один тип для всех ошибок, иногда это удобно / нужно, ну и там есть всякие удобные мелочи типа возможностей добавлять контексты. И есть трейт из стандартной библиотеки std::error::Error, который рекомендуется реализовывать всем типам ошибок. Теперь внимание:

// Хотим чтобы можно было конвертировать из любого типа ошибки
impl<E> From<E> for AnyhowError
    where E: StdError {
    //...
}

// Это сделать нельзя!
// Потому что иначе будет конфликт с impl<T> From<T> for T.
impl StdError for AnyhowError {}

В результате anyhow::Error вынужден прибегать к хаку, чтобы его можно было использовать там, где ожидается std::error::Error. Они это делают через AsRef<dyn StdError> + Deref<Target = dyn StdError>, последнее считается антипаттерном в языке.

Но работа в этом направлении ведется, обещают добавить.

Второе: публичные поля и геттеры.

Мелочь но бесит. Если вы делаете что-то типа

pub struct Foo {
    pub bar: usize
}

то вы даете гарантии насчет implementation details вашего типа, потому что если вы потом захотите сделать что-то вроде

pub struct Foo(Arc<FooInner>);

struct FooInner {
    bar: usize
}

impl Foo {
    fn bar(&self) -> usize {
        self.0.bar
    }
}

то у зависимого кода везде сломается foo.bar. Поэтому почти все библиотеки включая std не используют публичные поля практически вообще, а используют геттеры, которые компилятор просто потом заинлайнит. Я бы запретил публичные поля в принципе, и добавил бы лучше какой-нибудь специальный упрощенный синтаксис для геттеров.

Третье: отсутствие приватных методов в трейтах

Иногда нужно сделать что-то такое:

trait Template {
    // Этот метод предназначен для вызова и реализуется трейтом
    fn validate(&self) -> bool {
      memoize_or_something(self.run_validation())
    }

    // Этот метод не предназначен для вызова извне,
    // он для реализации трейта
    fn run_validation(&self) -> bool;
}

Пример немного синтетический но лучше не придумал :)

Мысль в том что у некоторых трейтов есть один-два "метода для реализации", которые предназначены только для того чтобы тип мог реализовать трейт. И большое количество "методов для вызова", которые потом вызывает код который с этим третом работает. Спрятать "методы для реализации" от вызывающего кода на сегодняшний момент можно только через уродливый хак (супертрейт), который сливает implementation details и который к тому же вроде как собираются запретить в будущем.

Четвертое: макросы

Мне в целом нравится как макросы сделаны в Rust, но их просто невозможно нормально отлаживать, на сегодняшний момент лучший вариант - это cargo extend, который выплевывает всю вашу кодовую базу с "развернутыми" макросами в stdout, и вам потом надо ковыряться в поисках того места где макрос выплюнул неработающий код, при чем без IDE. И копипастить куски этого кода в ваш остальной код, чтобы посмотреть, что не компилится, и потом убрать. Бесит страшно, и было бы круто если бы был какой-то инструмент, позволяющий посмотреть, во что макрос разворачивается.

Второй косяк макросов - они во многом регулируются не языком, а негласным сводом правил приличия. Иногда авторы библиотек делают неправильно, и например их макрос ломает остальные макросы. Или они изобретают какой-то причудливый синтаксис, который ломает автоформатирование во всем файле. Также каждая библиотека немного переизобретает эти конвенции, поэтому похожие макросы могут работать по-разному, поэтому рутинна например ситуация, когда в трех разных библиотеках будет работать один из трех вариантов, но не остальные:

#[some_macro]
struct Foo {
  #[some_macro(skip)]
  bar: String
}

#[some_other_macro]
struct Foo {
  #[some_other_macro(skip)]
  #[some_other_macro(ignore)]
  bar: String
}


#[yet_another_macro]
struct Foo {
  #[skip]
  #[ignore]
  bar: String
}

Пятое: Send + Sync

Не проблема имхо с "проброской" трейт баундов в целом, но это сочетание встречается уж очень часто. Если у вас асинхронный код, вы будете использовать рантайм, рантайм будет хотеть перекладывать Future с одного треда на другой, для этого ему нужно будет чтобы Future был Send + Sync, для этого любые данные внутри Future должны быть Send + Sync. Если у меня такое приложение где у меня multithreaded async рантайм, то ну нигде у меня не будет Future которые не Send и не Sync. Я буду использовать Arc вместо Rc и Mutex вместо RefCell. Ну позвольте мне один раз включить этот баунд везде, почему я должен каждый раз писать его?

Шестое: время компиляции

На большом проекте (веб бэкенд с где-то 300 эндпоинтами) иногда сидишь как осел по 20-30 секунд после каждого Ctrl+S. Уже начинаешь ненавидеть и этот язык, и эту работу, и эту жизнь. Это было на аймаке 2017 года (4 ядра, 3.8ггц, 8гб), пришлось проапгрейдиться, пока полет нормальный.

Еще есть всякая неприятная мелкота типа невозможности использовать foo: impl Trait в некоторых контекстах, отсутствие поддержки async traits в языке и т.п, но все эти вещи вроде как в работе.

Я со своей колокольни как бэкендер и полный лапоть в более низкоуровневых вещах, у тех кто из C / C++ будет еще вагон своих претензий, на Reddit можно почитать по запросу "reddit what you don't like about rust".

Второе: публичные поля и геттеры.

Я как раз из С++ в раст пришёл, поэтому данная проблема меня совсем не смущает. Если метод переименовать или сделать приватным, то случится точно такая же проблема. Опять же, очень удобно использовать структуры с публичными полями просто как набор данных без ассоциированных методов. Для таких случаев уже геттеры/сеттеры будут выглядеть лишняя сущность.

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

Помнится в Borland C++ Builder была интересная сущность - __property

class TProfile : public TForm
{
  private:
    void SetCurrentTrack(PGPXTrack value);
    PGPXTrack GetCurrentTrack();

  public:
    __property PGPXTrack CurrentTrack  = { read=GetCurrentTrack, write=SetCurrentTrack };
};

Если CurrentTrack используется как lvalue, вызывается SetCurrentTrack с rvalue в качестве аргумента, а если как rvalue - вызывается GetCurrentTrack...

TProfile* pProfile = new TProfile;
PGPXTrack pTrack;

pTrack = pProfile->CurrentTrack; // вызовется pProfile->GetCurrentTrack();
pProfile->CurrentTrack = pTrack; // вызовется pProfile->SetCurrentTrack(pTrack);

В D нечто подобное есть(@property), но более многословное. В С# немного "наоборот": там вызывать всё-таки get/set приходится, зато сахар для объявления, кажется, наиболее короткий.

Ну вот дальше билдера в С++ это не пошло. А жаль.

Причем, там если написать

__property PGPXTrack CurrentTrack  = { write=SetCurrentTrack };

или

__property PGPXTrack CurrentTrack  = { read=GetCurrentTrack };

то получаем write only или read only проперть.

Естественно, что там в геттере или сеттере совершенно неважно. Т.е. геттер мог ее из БД читать, например, в сеттер в БД писать...

Помнится, для виндовых ini файлов такое удобно было в частности - геттер читает из файла, сеттер пишет в файл

В шарпах как раз так же:

public string CurrentTrack { get => getCurrentTask(); set => setCurrentTrack(value); }

 В С# немного "наоборот": там вызывать всё-таки get/set приходится, зато сахар для объявления, кажется, наиболее короткий.

Нет, вызывать явно ничего не нужно

class MyClass
{
   public int MyInt {get;set;}
}

Код выше сам сгенерирует то, что на тамошней терминологии называется backed field. И сгенерирует геттер и сеттер. Либо можно в явном виде:

class MyClass
{
   private int _myInt;

   public int MyInt {
     get => _myInt = value;
     set => _myInt;
   }
}

Использовать можно так:

var mc = new MyClass();
mc.MyInt = 0;
Console.WriteLine(mc.MyInt);

Спасибо, я почему-то думал, что сгенерятся как раз getMyInt/setMyInt.

К слову, методы get_MyInt/set_MyInt действительно генерятся:

foreach (var method in typeof(MyClass).GetMethods())
{
    Console.WriteLine(method.ToString());    
}

class MyClass
{
    public int MyInt { get; set; }
}

// Outputs
//     Int32 get_MyInt()
//     Void set_MyInt(Int32)        
//     System.Type GetType()        
//     System.String ToString()     
//     Boolean Equals(System.Object)
//     Int32 GetHashCode()

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

var c = new MyClass();
c.set_MyInt(42); // Error CS0571 : 'MyClass.MyInt.set': cannot explicitly call operator or accessor

В С# немного "наоборот": там вызывать всё-таки get/set приходится, зато сахар для объявления, кажется, наиболее короткий.

В Котлине всё же наиболее короткий, ибо все field по умолчанию являются property

class Foo {
  // Под капотом появляется `private int bar = 1`, `int getBar()`
  val bar = 1 

  // Под капотом появляется `private int faz = 1`, `int getFaz()`, `void setFaz(int value)`
  var faz = 2

  // Под капотом появляется только `public int boo = 3` (без геттеров и сеттеров)
  @JvmField
  var boo = 3
}

Управлять геттерами и сеттарми вручную можно аналогично с тем как это делают в Шарпах:

class Foo {
  val a // Int автоматически инферрится из геттера 
    get() {
      return 2
    }

  val b
    get() = 2 // делает то же, что и метод в b, но более коротко

  var c = 3
    private set

  @JvmField
  private var _d = 4 
  var d
    get() = _d
    set(value: Int) {
      _d = value
    }
}

Симпатично, правда не уверен, что правильно понял вот этот пример:

class Foo {
  val a // Int автоматически инферрится из геттера 
    get() {
      return 2
    }
}

Тут будет сгенерирован сеттер, который будет устанавливать значение a, но прочитать значение будет нельзя так как всегда будет возвращаться 2?

Тут будет сгенерирован сеттер

Нет, val - иммутабельная переменная. Сеттера сгенерировано не будет вообще. Сеттер генерируется только для var

Если метод переименовать или сделать приватным

Да, но это уже целенаправленное изменение API.

А тут получается: во-первых, дается гарантия насчет имплементации. Тип - структура а не enum или что-то еще, и у него есть такое-то поле. При чем поле это на самом типе а не вложено где-то внутри, и поле это не вычисляемое и не за каким-нибудь смарт пойнтером.

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

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

Для «примитивных» структур, которые работают просто как именованные кортежи, не имеют внутренних инвариантов и так далее, публичные поля - норм, но для таких структур тогда все поля должны быть публичными. Писать pub перед каждым полем - подбешивает.

И опять же если понадобится такую структуру поменять из примитивной на менее примитивную, это сломает зависимый код, хотя по сути это implementation detail. Так что в идеале я бы добавил ключевое слово для простого создания геттеров, и стер бы разницу между геттерами и полями как таковую.

По большому счёту согласен, но в целом с "целенаправленным изменением апи" не всё так просто. Не зря есть штуки вроде cargo-semver-checks - порой случайно сломать публичное апи легче, чем кажется.

Не бесполезная там гарантия.

У поля можно не только прочитать значение - но и взять на него ссылку. Причём если у вас есть два поля - вы можете взять мутабельную ссылку на каждое, что для акцессоров невозможно.

Вы правы, совсем забыл про это - но зачем это может быть нужно вне модуля самого типа?

зачем это может быть нужно вне модуля самого типа?

Теперь мне любопытно в чём разница и почему в модуле это может понадобиться, а вне - нет?..

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

let mut s = S { a: 1, b: 2 };
foo(&mut s.a, &mut s.b);
// foo(s.get_a(), s.get_b());

Да, пример максимально синтетический и мне проблема тоже кажется преувеличенной.

Потому что эти поля скорее всего связаны будут между собой каким-то внутренним инвариантом или иной тесной логической связью. Например нам может быть нужно мутабельно итерировать одно поле, при этом мутируя второе поле. А если так, то эта логика по идее должна сидеть в методе структуры в том же модуле, где эта структура объявлена.

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

Спасибо за развернутый ответ)

Лайфтаймы обычно появляются, когда появляется более обобщённая библиотека. Ну и вторая проблема, что если лайфтам или trait bound появился в одном месте в коде, то он с большей вероятностью будет отравлять всех пользователей необходимостью писать этот же самый баунд/лайфтайм у каждого импла. Рефакторинг из-за этого превращается в некоторый локальный адок.

Так трейты суть интерфейсы.

Нет, они что-то сродни концептам.

В дженериках - да, но dyn Trait - это вполне себе интерфейс.

Ключевое слово dyn достаточно хитрое. По факту оно создает новый тип "dyn Trait", который содержит указатель на данные и указатель на методы. Но, помимоо этого вы можете сделать:

impl dyn Trait
{
  pub fn my_fn(&self) {
  }
}

У типажей нету механизма наследования когда вы пишите:

trait TraitA{}

trait TraitB: TraitA{}

Вы не наследуете TraitB от TraitA, вы накладывает ограничения, чтобы тип определяющий TraitB также определял бы и TraitA. И это две большие разницы.

Да нет никакой разницы, "наследование" интерфейсов работает по сути так же.

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

trait TraitA{
  fn a(&self);
}
trait TraitB: TraitA{
 fn b(&self);
}

struct My
{
  d: i32,
}
impl My
{
  fn a_p(&self) {
  }
}

impl TraitA for My{
  fn a(&self){
    self.a_p());
  }
}

impl TraitB for My{
  fn b(&self){
    self.a_p()); 
    // и только так, вы не можете сделать self.a()
  }
}

Значит уже затащили в стабильную ветку, где-то еще год назад такой фокус не проходил. Запаривало страшно.

Но, помимоо этого вы можете сделать:

И? Чем плоха возможность делать impl dyn Trait?

Вы не наследуете TraitB от TraitA, вы накладывает ограничения, чтобы тип определяющий TraitB также определял бы и TraitA. И это две большие разницы.

Можно эту разницу увидеть на примере?

Основное ограничение трейтов, которое приходит мне в голову: нельзя из dyn Child перейти к dyn Parent. Хотя может когда-то и сделают.

И? Чем плоха возможность делать impl dyn Trait?

Да ничем, более того это единственный вариант, когда можно реализовать кастование dyn объектов пока не завезли в стабильную ветку. Но то, что в ООП называется интерфейсами в Расте возможно только через dyn объекты, а их не для каждого типажа можно создать.

trait TraitA{
  fn a(); // колючевое слово self отсутствует
}
trait TraitB: TraitA{
 fn b(&self);
}

trait TraitC {
 fn c (&self);
}

struct A{
}

impl TraitA for A{}

impl TraitB for A{}

impl TraitC for A{}

struct B{
}

impl TraitC for B {}

Вот в такой конструкции вы не сможете создать dyn TraitA, а из-за это не может создать dyn TraitB, также мы не можем создать dyn TraitC для типа А, но можем для типа B.

А где в этом примере разница между наследованием и наложением ограничений?

также мы не можем создать dyn TraitC для типа А

А что мешает?

Мешает то, что тип А реализует TraitA. Этой ситуации не может быть в принципе при наследовании.

Чем именно оно мешает-то? Вот такой код у меня спокойно компилируется, если реализацию трейтов добавить:

fn test() {
    let a: &dyn TrainC = &A{};
    let b: &dyn TrainC = &B{};
}

Спасибо, мне казалось что добавит ограничения и на сам тип. Был не прав. Но в любом случае вы не сможете сделать let a: &dyn TraitA = &A{};

Разумеется, потому что TraitA - не интерфейс (с точки зрения ООП). Как и TraitB (если бы он был интерфейсом, он бы наследовал TraitA, а TraitA не интерфейс).

Всё ещё никаких отличий наследования интерфейсов от накладывания ограничения...

а их не для каждого типажа можно создать.

Всё-таки пример несколько синтетический. Ну и вот так вот можно:

trait TraitA {
    fn a() where Self: Sized;
}

trait TraitB: TraitA {
    fn b(&self);
}

struct A {}

impl TraitA for A {
    fn a() {}
}

impl TraitB for A {
    fn b(&self) {}
}

fn main() {
    let a: &dyn TraitA = &A {};
}

Это только в случае если мы их в качестве границ типажа указвыаем - как раз эти вот всякие Send+Sync. Если же оно как `impl/dyn Trait` в качестве параметра, то это вполне себе интерфейс.

Интерфейсы, трейты, концепты, прототипы, и классы это разные подходы к реализации идей объектно-ориентированного программирования, между ними много общего, но и достаточно много различий.

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

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

Классы целиком реализуют идеи ООП: наследование, полиморфизм и инкапсуляцию. Но из-за реализации полиморфизма через наследование имеют ограничения, особенно если язык не позволяет множественно наследование, что на практике приводит к проблемам в больших системах. Из того, что я понял, трейты и концепты избавлены от этой проблемы. ООП через классы было популяризованно языком Smalltalk, часть его разработчиков потом создали Self, который я так понимаю ввели идею прототипов, легшую в основу трейтов и концептов.

На самом деле надо смотреть не на название, а на идею, которые они реализуют. Интерфейсы - пытаются реализовывать некоторый API к некоторому поведению. Типажи в Rust, интерфейсы в C# и Go, абстрактные классы в Java/C++, протоколы в Swift - это как раз все про поведение. Например Iterator/Iterable - яркий пример такого поведения.

Вторая концепция - ограничение на поведение. У раста это соотвественно

  • lifetime bound, довольно уникальный механизм среди языков. static qualifier и DreamBerd не в счёт.

  • разветвлённая система trait bounds : `where T: Send+Sync+Blabla`

  • type bounds `call<T: Send>(var: T) -> _`.

У С++ есть <type_traits> и собственно в будущем должны будут выступать концепты плюс также type bounds (`template<typename T=Foo>`). В качестве управления лайфтаймом можно назвать какой-нибудь кастомный аллокатор, но строгости и эргономики такой как в Rust там не добиться. Пропозалы по лайфтаймам вроде были, но я насколько он жив и развивается не знаю. К с++37 глядишь появится.

О существовании аналогичных ограничителей в C# и Swift мне доподлинно не известно, но скорее всего в свежих редакциях языков наверняка есть или планируется что-то аналогичное ржавым трейтам и плюсовым type bound.

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

Третья концепция - это то как поведения компонуются с объектом и перегружаются им тут уже начинаются различные способы - наследование(class A derive B), композиция (impl A for B), расширение (class A extends B). Всё это дружно приводит к появлению виртуальных таблиц в каком-то виде и возможностью переопределять его в пределах конкретного объекта, давать доступ к полям объекта, вызывать всю иерархию поведения (а ля `super(B, self).parent_method()` ). Про это была помнится обширная статья на хабре про модели полиморфизма в различных языках.

В итоге получаем кучку концепций, которые в том или ином виде приклеиваются к одной сущности в разных пропорциях и получаем конфуз с тем что типажи мол это де не интерфейс, а граница типа, а не как вот в этих Go/С# тоненькая штука.

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

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

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

В Rust свои минусы тоже есть - как то макросы, которые довольно сложны в отладке. Отсутствие полноценного ООП во многих сценариях - не удобно. Но обо всем этом начинаешь задумываться, когда нужно выдавать объем работы.

Пять лет пишу на расте за деньги - отвращения пока не появилось. Да, не всё идеально и (хочется думать, что) я не фанатик языка - если появится что-то удобнее и лучше, то готов переключиться, но С++ даже последней версии (для меня) таковым языком не является.

Но да, было бы здорово, если бы на расте было побольше не блокчейновых проектов.

как считали? очень интересно

let new_owner = original_owner;

В C++ был подобный заход через auto_ptr. По-моему, скоро стало ясно, что нужно делать либо все владеют одной копией (shared_ptr), либо делать явную операцию перемещения.

Также из статьи не очень понятно, почему rust противопоставляется c++. Вот прямо подставь вместо c++ python (или другой язык) - статья особо не поменяется. И тогда всё сводится к тому, что есть любимая жена любимый язык, и нелюбимый.

В C++ был подобный заход через auto_ptr.

auto_ptr уже все, полномочия закончились. Теперь - unique_ptr. Но только с unique_ptr C++ не контролирует владение, что чревато ошибками. Вот вам пример, который минимально демонстрирует проблему отсутствия контроля владения:

#include <iostream>

struct Class1 { const int i1 = 42; };

void fun1(std::unique_ptr<Class1> smart_ptr){}

int main()
{
    auto smart_ptr1 = std::make_unique<Class1>();
    fun1(std::move(smart_ptr1));

    std::cout << smart_ptr1->i1 << std::endl;
}

Rust - выдал бы ошибку компиляции в строке 12.

Чисто для моего общего развития: что скажет компилятор, если сам вызов fun1 будет под if c некоторым условием, значение которого на момент компиляции неизвестно? Или это другое?

если сам вызов fun1 будет под if c некоторым условием, значение которого на момент компиляции неизвестно

Тут. Даже если условие известно и не выполняется - все равно не пропускает компилятор.

Походу он тупо проходит сверху вниз и смотрит вызовы, не взирая на условия. Как бы перестраховка. Если закомментировать 11 строку - то скомпилирует.

что лишний раз говорит о том, что не надо использовать явный move

там где надо, он всегда работает автоматически и безопасно

Ну да, ну да, полностью автоматически...

{
    auto smart_ptr1 = std::make_unique<Class1>();
    fun1(smart_ptr1); // Копирование либо ошибка компиляции
}

Казалось бы, очевидно же что переменная smart_ptr1 более нигде не используется, и её следует переместить. Но нет, без явного std::move фиг вам, а не перемещение!

ну так сегодня у вас func1() последняя строчка в блоке, а завтра вы туда что-то еще допишете и начнете использовать smart_ptr1...

очевидно, что желаемое должно выражаться однозначно, а не основании того, что там ниже по течению у вас происходит

однозначный корректный код: fun1( std::make_unique<Class1>() )

очевидно, что желаемое должно выражаться однозначно, а не основании того, что там ниже по течению у вас происходит

Вы же только что предлагали автоматически и безопасно?

однозначный корректный код: fun1( std::make_unique<Class1>() )

…не работает если над переменной нужно сделать что-то ещё перед перемещением.

ну так сегодня у вас func1() последняя строчка в блоке, а завтра вы туда что-то еще допишете и начнете использовать smart_ptr1...

Ну так компилирую я сегодня. Если завтра я что-то допишу, то пусть завтра компилятор и уберёт автоматический move.

Блин, написал фигню и собрал плюсов 🤦‍♂️
Я подумал ещё раз и понял, что не хочу чтобы у меня неявно менялось поведение передачи аргументов в функцию, в зависимости от того, сколько раз вызывается эта функция.

разве флагами компилятора такого эффекта в С++ не добиться?

Нет. У компиляторов C++ нет полноценного статического анализа аналогичного borrow checker в Rust. Можно поставить сторонние (cppcheck, pvs studio, sonar qube и тп), но и без того сложный плюсовый пайплайн ещё более усложнится и это не считая того что время на сборку тоже вырастет. В имплементациях stdlib 23 стандарта там конечно улучшают кое какие вещи при помощи концептов, включая сообщения об ошибках, но это не решает language level проблемы.

К слову, в С++ нет destructive move. То есть перемещенный указатель после мува остается в валидном состоянии. Соответственно, его можно использовать как полноценный объект дальше и с точки зрения компилятора все ок (хоть moved-from значение и будет равно nullptr, это детали имплементации move конструктора конкретного типа).

Ну как сказать, одно дело, когда доступ к перемещенному значению запрещает компилятор и другое, когда проверка происходит только в рантайме. Да, молчу ещё про то, что в плюсах этот заход работает только с памятью в куче. Так что прекрасно понимаю, почему auto_ptr убили.

Rust противопоставляется другим языкам, которые компилируются прямо в бинарный код и не имеют garbage collector. А это С, С++ и ... ? Только на таких языках можно написать прошивку контроллера, модуль для ядра Linux, серьёзный вирус. Если ограничивать область зрения микросервисами на средненагруженных серверах, то можно и одним PHP обойтись.

По ощущениям, он противопоставляется даже большему количеству языков, таких как Java, C#. Наверное, из-за его довольно уникального подхода работы с памятью и неплохой продуманности языка (когда создавали, пытались избегать проблем, которые возникли в других языках). А так, можете хоть fullstack приложение на Rust написать, противопоставляя это fullstack приложению на JavaScript, ничего этому не будет мешать)

На си и c++ можно использовать динамические библиотеки с функциями. На java динамически можно подгружать библиотеки классов. Можно ли динамически подгружать библиотеки классов в rust ?

Можно, но есть свои грабли, нет стандарта на ABI, так что делается это на свой страх и риск. (Впрочем в плюсах тоже нет стандарта на ABI, но его уже лет 12 не меняли).

Раст прекрасно поддерживает `С ABI`, оно стабильное. Конечно, использовать неудобно, т.к. фактически доступны только примитивы, да Box с Option , но что поделать, так в любом языке будет.

А ещё, если очень надо, то есть костылик в виде abi_stable.

но его уже лет 12 не меняли

А вы историю с _GLIBCXX_USE_CXX11_ABI к какому году относите?

Так это вроде где-то рубеж 10ого и 11ого годов, вроде

Нет. Это переход с gcc-4 на gcc-5. Началось в 2015 году, но в куче дистрибутивов задержалось на несколько лет. А некоторые вообще только в прошлом году на новый ABI перешли.

На нем и писать C-like либы можно. Я на Rust писал либу, которую цепляю к пхп через FFI - полет отличный.

Разве что на стыке с "внешним миром" приходится уходить в unsafe, но тут ничего не поделаешь. Специфика-с. Ну и не забыть вернуть указатель обратно в раст из пхп на убой, конечно, а то при вытаскивании наружу растовая магия перестает работать и можно на ура наплодить утечек.

Спасибо за статью. Очень ëмко и просто навела на некоторые мысли.

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

Сейчас у меня есть опыт параллельного программирования в С++, в этом очень сильно помогли умные указатели, которые следят за владением объекта и чисткой памяти. Они гарантируют отсутствие битых указателей, т.к. пока какой-то поток "одолжил" указатель, память в нем не чистится. Как недостаток, нельзя сказать точно, когда в рантайме объект освободится и в какой момент запустится деструктор, а главное из какого потока (например, к OpenGL можно обращаться только из главного потока, поэтому просто так из деструктора объекта, хранящиеся в shared_ptr, данные OpenGL не удалишь).

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

А теперь вопрос (он наверняка гуглится, но если всë гуглить, то о чем писать в комментах?). Rust многие вещи делает в компиляторе. Это ведь и по эффективности получается лучше, чем ООП в плюсах со всякими контейнерами и умными указателями? В плюсах это наверняка сплошной рантайм и достаточно тяжелый.

Это не совсем корректный вопрос. Да, некоторые из тех проверок которые в Си++ проходится делать в рантайме, в Расте можно делать на этапе компиляции. Но вот в чем прикол: понятие "объект" у вас при этом исчезает. И если во многих случаях без него можно обойтись не потеряв производительности (хотя придется и поломать голову), то в некоторых областях (тот же пользовательский интерфейс) это доставлет массу проблем, которые, вполне возможно, выливаются в потерю производительсноти.

Писал на С, затем С++ (как основной язык) с ... 90-91-го и по 17-й годы (сейчас тоже иногда, но это уже "второй язык" для решения определенного класса задач). И никогда не было проблем с памятью (в т.ч. и в многопоточных приложениях). Просто есть правило, которое следует неукоснительно соблюдать (отступления возможны лишь в самых исключительных случаях и всегда должны быть явно выделены в коде (комментарии и т.п.). А именно - "кто девушку ужинает, тот ее и танцует". Т.е. за освобождение (и вообще контроль за состоянием) памяти отвечает тот, кто ее выделил. И никак иначе.

Rust многие вещи делает в компиляторе. Это ведь и по эффективности получается лучше, чем ООП в плюсах со всякими контейнерами и умными указателями? В плюсах это наверняка сплошной рантайм и достаточно тяжелый.

Да. Основной минус ООП (как минимум его плюсовой реализации) - в изрядных накладных расходах на создание и удалении объектов в рантайме.

По поводу накладных расходов я писал в комментариях к статье «Чистый» код, нет проблем с производительностью. Например, так все то же самое делается без ООП и лишних вызовов конструкторов, чисть на статической инициализации. Тут и тут показывал что поддержка определенных вещей на уровне языка (например, типов данных соответствующих типам данных БД) намного эффективнее чем использования ООП и создания новых типов через ООП механизмы.

Вообще, по описанию, те концепции, что заложены в Rust как альтернатива ООП лично мне импонируют. Но нужно вникать глубже - возможно и там есть подводные камни.

А именно - "кто девушку ужинает, тот ее и танцует". Т.е. за освобождение (и вообще контроль за состоянием) памяти отвечает тот, кто ее выделил. И никак иначе.

Увы, это слишком ограничивающий принцип, особенно в многопоточных программах...

Тем не менее, это работает и позволяет контролировать работу с памятью самому, а не полагаться на "времена жизни объекта" и вот это все вот (что еще больше ограничивает на самом деле).

Уж различной параллельной обработки в своей жизни написал преизрядно. Тут вопрос изначальной проработки архитектуры приложения.

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

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

Передавать данные по ссылке можно. Но контролировать память лучше в одном месте. От и до.

Или это должен быть контракт заранее оговоренный - "я тебе предаю выделенный мной блок памяти, который ты должен освободить когда он тебе станет не нужен". В целом допустимо.

Так или иначе, мое мнение в том, что память надежнее контролировать самому.

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

Вот к компилятору С++ с его огромным количеством UB как-то особого доверия нет...

Мне тоже :-)

И потому, что вместо того, чтобы понижать количество UB, туда тащат все, что увидели в других языках - чтобы "у нас все это тоже было, но с вистом и профурсетками". В результате получается какой-то Франкенштейн, а разработчики компиляторов просто не успевают за стандартописцами. И подавляющая часть разработки идет на С++ "предыдущих поколений". Особенно, на "долгоиграющих" проектах, подразумевающих длительную поддержку и развитие.

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

C++ так-то находится на передовой разработки новых концепций (особенно в области обобщенного программирования), так что скорее происходит несколько наоборот. Тот же RAII появился в C++, откуда он был перетащен в Rust и в некоторой степени в C# и Java (IDisposable, try-with-resources). Шаблоны - совершенно самобытная вещь потрясающей эффективности, находится в постоянном развитии - вариадики, свертки, автоматический вывод аргументов, вот это вот все. In place конструирование (emplace() и сотоварищи) из C++ в Rust так и не могут перетащить, хотя и были попытки - не хватает ряда механизмов, а в языках с GC его не может быть по определению. Ну и т.д.

разработчики компиляторов просто не успевают за стандартописцами

Это одни и те же люди.

И подавляющая часть разработки идет на С++ "предыдущих поколений".

Понятие "предыдущего поколения" постоянно сдвигается.

Так что там с UB? Может быть для начала от них избавиться, а потом уже "пилить революционные фичи"?

Или ABI стандартизировать. Ну просто так, интереса ради.

Или все это скучно?

Так процесс же открытый. Предлагайте пропозалы, покажите этим комитетским лохам, как надо. Может быть, тогда вы получше погрузитесь в тему и несколько разовьете свои представления о современном состоянии дел. А писать обличающие комментарии по Булгакову ("космического масштаба, ..." и т.д. по тексту) на хабрике много ума не надо.

UFO just landed and posted this here

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

unsafe only means that avoiding undefined behavior is on the programmer

Это как раз очень малый список.

Это неполный список (о чем, кстати, там и написано), а полный список неизвестен. Один из "приколов" растовского unsafe - при написании unsafe кода, ты никогда не можешь быть уверен, что то, что ты написал, не вызывает UB. Unsafe Rust куда "опаснее" в этом смысле, чем C или C++.

Unsafe Rust куда "опаснее" в этом смысле, чем C или C++.

Соглашусь со всем комменатрием кроме этого предложения. UB в С++, как и в Unsafe Rust контринтутивен и хрен редьки не слаще.

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

И это всё требует unsafe, который по определению может вызвать UB. Вот только в реальном коде unsafe почти никогда не нужен.

Этот в принцип в более сложном проекте вырождается в написание своего велосипедного аллокатора со всякими счётчиками и сборщиком мусора.
Что на самом деле - плохая практика. Во-первых, нужно использовать уже готовый, отлаженный. А во-вторых, все преимущества ручного управления памятью теряются - тогда уж и язык надо менять.

Ну... Может быть мои проекты был недостаточно сложны (всего-то порядка мегабайта кода), но проблем с ручным управлением памятью не испытывал. А производительность там была потребна на достаточно высоком уровне - "микроядро" системы мониторинга инженерного оборудования зданий - с одной стороны сеть промконтроллеров, с другой - несколько "интерфейсных клиентов". Микроядро выполняло роли монитора состояния контроллеров верхнего уровня, фильтра-маршрутизатора (что от кого кому передавать), реализовывало отношение "многие-ко многим" ну и еще много чего. Работало все это в режиме 24/7 (причем, "где-то там", куда физического доступа у меня не было, я мог только подключиться к ядру удаленно и смотреть что там происходит в реальном времени специальным клиентом-шпионом) в несколько потоков (как минимум - поток контроллеров, поток клиентов, поток обработки данных и поток мониторинга работоспособности (остальных потоков, сервера БД и т.п.). И ряд вещей там должен был выполняться с микросекундными таймаутами.

Никакого "своего аллокатора" там не было. Но архитектура всего этого выстраивалась очень тщательно.

Сейчас все намного проще. Вся параллельная обработка строится не на потоках, а на фоновых заданиях (запускаемых через spawn) с конвейером для раздачи данных от головы обработчикам. Ну и система совсем другая.

Кстати язык Rust так и появился.

Сначала в Mozilla исп стандартный аллокатор. Потом стали писать свой. И в какой-то момент кодовая база этого аллокатора и проблем вокруг превысила предел и ребята решили, что проще написать свой новый язык, а не вот это вот всё :)

Очень актуально для игрового движка. При отрисовке каждого кадра приходится создавать и чистить большое количество данных (треугольники, иерархии игровых сущностей). Быстрее всего сразу "застолбить" побольше памяти, а потом с помощью собственного аллокатора брать оттуда кусочки под расчеты. Поскольку игра должна работать быстро и плавно, обращаться в ОС за памятью не эффективно, а данных перекладывать с места на место надо очень много

Передавать данные по ссылке можно. Но контролировать память лучше в одном месте. От и до.

Удачи вам контролировать память в одном месте от и до при использовании архитектуры "каналы + обработчики". А ведь она считается самой дуракоустойчивой в плане многопоточности, и не просто так...

Или это должен быть контракт заранее оговоренный - "я тебе предаю выделенный мной блок памяти, который ты должен освободить когда он тебе станет не нужен". В целом допустимо.

Ага, и самый простой способ "оговорить" такой контракт - использовать умный указатель с возможностью перемещения.

Удачи вам контролировать память в одном месте от и до при использовании архитектуры "каналы + обработчики". А ведь она считается самой дуракоустойчивой в плане многопоточности, и не просто так...

А как думаете была устроена архитектура многопотчного приложения, где один поток работал с UDP портом, получал оттуда данные, передавал их в поток обработки-маршрутизации, откуда они потом уходили в поток, работающий с TCP портом и там отправлялись дальше? Аналогично и наоборот - от TCP через обработку в UDP.

Все данные оформлялись в виде датаграмм.

И как же вы эти данные возвращали потом обратно, чтобы освободить память?

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

Более того, по маршруту UDP->TCP одна посылка со стороны UDP могла уходить в несколько TCP каналов. А могла в один...

И все это годами работало (и все еще работает в нескольких местах, говорят, хоть я оттуда 6 лет как ушел) в режиме 24/7.

Никаких утечек памяти не было и нет. Просто делаем все аккуратно.

Какие же это тогда  "каналы + обработчики"?

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

Действительно, после такого опыта многопоток кажется не сложным. Это и "один пишет, другие читают", и "как танцевать девушку". При этом мютексы ставились только на очень узкие места (в основном взятие или добавление объектов в контейнеры).

Но поскольку у нас игра, объекты может создавать и игрок, и поток подгрузки. Возникают очень хаотичные взаимодействия потоков в самых разных комбинациях. Каким-то образом всë это получилось разрулить и заставить работать быстро и без вылетов.

Как Rust меняет мышление разработчика

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

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

Я страшно ненавижу C++

Я бы так не сказал. Я люблю С (т.к. начинал фактически с него, не считая немного фортрана и бейсика), потом появился С++ (в первых версиях "С с классами") - понравилось. Но сейчас мне не нравится то, куда идет современный С++ - туда пытаются втащить все на свете, что увидели в других языках (вместо того, чтобы работать над сокращением количества UB, которое только растет), разработчики компиляторов не успевают за писателями стандарта.

Как же этот язык, появившийся на сцене меньше десятка лет назад, стал настолько популярным?

А насколько он стал популярным? На мой взгляд популярность языка начинается тогда, когда появляется его устойчивый стандарт и язык активно используется в "большом энтерпрайзе". И, как ни парадоксально, когда появляется достаточное количество легаси кода, на нем написанного.

А пока в языке даже нет устоявшегося ABI... Использование его только на свой страх и риск.

Хотя, трудно спорить, в Rust есть действительно интересные концепции. Хотя с первого взгляда трудно сказать для какого класса задач эти концепции могут дать какое-то реальное преимущество перед тем что есть в других языках.

Откровения, снизошедшие на меня при изучении Rust в течение последних нескольких месяцев, трансформировали меня схожим образом.

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

А это никак не "изучение в течении нескольких месяцев".

В целом я Вас понимаю. Сам через такое проходил неоднократно. Первое откровение снизошло еще в начале 90-х, когда столкнулся с Clarion (тогда еще под DOS) где можно было спроектировать таблицу, нарисовать экранную форму для работы с ней и оно само генерировало код.

Второй раз - чуть позже, но примерно в ту же эпоху. db_Vista - БД, основанная на сетевой модели (например, поддержка "наборов" где "запись типа А является владельцем набора записей типа Б") с ее DDL - Data Description Language - язык описания данных.

Последний раз - относительно недавно (17-й год) когда сменил работу и познакомился с платформой IBM i (AS/400). Которая абсолютно не похожа ни на какую другой ОС, построена на принципе "все есть объект" и поддерживает концепцию "интегрированной языковой среды" - ILE позволяющей для решения одной задачи одновременно использовать (в разных ее частях) несколько разных языков.

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

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

Но сейчас мне не нравится то, куда идет современный С++ - туда пытаются втащить все на свете

Мне кажется, это должен решать каждый программист сам. Например, пользоваться только модерн C++, запретив себе использовать старые парадигмы после перехода на новый стандарт. Тогда код будет написан в одном стиле, понятен и легок в масштабировании. Получится фактически тот самый новый прекрасный язык с небольшим количеством нужных инструментов. И новый язык изобретать будет уже не нужно только для того, чтобы новички не "стреляли себе в ногу", просто потому что им в руки попалось ружьë. То есть искусственно ограничивать набор инструментов путем создания нового языка

Называть систему типажей в Rust утиной типизацией - это виртуозная ментальная гимнастика.

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

Истинная утиная типизация есть в шаблонах C++. Там можно в шаблонном коде вызвать у переданного типа метод крякания. И всё будет работать, пока в шаблон передаются типы с нужным методом и сигнатурой. А если нету нужного метода - компиляция сломается.


Каждый подход имеет как достоинства и недостатки. Но я лично склоняюсь к подходу C++, т. к. он всё же менее многословен.

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

Данное достоинство может показаться не критичным, по сравнению с крутыми фишками альтернативного подхода (работает само и иногда даже так, как автор и предположить не мог), но по сути это ровно та же проблема, что и в ситуации с ручным управлением памятью, которую хотели решить в Rust: человек делает ошибки, особенно там, где нужно быть очень внимательным и всё проконтролировать на 100%. Ручное соблюдение семантики типажа/интерфейса мельком (и не всегда полно) упомянутой в его доке всеми типами, авторам которых показалось полезным сделать поддержку этого типажа… по сути, это рулетка. В том же Go как раз утиная типизация, и я много раз ловил на ревью баги в реализациях даже "простейшего" (из одного метода) интерфейса io.Reader. И это при том, что уж что-что, а его доку в недостаточно чётком описании семантики обвинить точно нельзя. Но написать мало, ещё надо чтобы кто-то написанное прочитал, прочитал внимательно, целиком, и понял написанное. А с этим проблемы неизбежны просто в силу человеческой природы.

Я веду к тому, что для языка, который декларирует ценности Rust, утиная типизация здесь выглядит неприемлемой. И это принципиально, а не "ну, у всех подходов есть свои плюсы/минусы".

Все в общем то нормально с типажами за исключением того что я не могу реализовать не свой типаж для не своего типа. Да да очередная защита не понятно кого не понятно от чего, но факт в том что я даже не могу из за этого организовать СВОИ типы по разным модулям потому, что тогда меня начинает бить по рукам бланкет реализация. А ещё есть такие типы и такие трэйты которые НИКОГДА не встретят друг друга кроме как в вашем коде, какого хера мне запрещают реализовывать трэйты для типов в СВОЕМ модуле, добавьте ключевое слово или атрибут override что бы был явный опт ин в это и всё.

Я точно не знаю, но предполагаю, что это могут запрещать из следующих соображений:

  • Если автор типа не намеревался реализовать данный типаж, то даже если в данный момент так случайно получилось что типаж типом реализован (по сигнатуре методов), всё-равно нет гарантий, что будущие изменения реализации этого типа данную (не намеренную) поддержку не сломают (напр. изменением семантики этих методов). Соответственно, если кто-то посторонний у себя в проекте объявит что этот тип поддерживает данный типаж, то это может сломаться при обновлении библиотеки с данным типом.

  • Если дать возможность легко объявлять поддержку типом типажа в стороннем коде, то высоки шансы что это будут делать достаточно бездумно, механически, не проверяя насколько тип поддерживает семантику типажа, и в результате эта "фича" будет восприниматься разработчиками как "кривая поддержка утиной типизации, которая вроде и есть, но при этом требует почему-то писать лишний код ручками чтобы связать сторонний тип с типажом".

  • Возможно есть чисто технические сложности реализации этой фичи в компиляторе (если все поддерживаемые типажи известны в момент компиляции типа это может что-то где-то сильно упрощать). Отдельно этой причины может быть недостаточно чтобы отказаться от реализации фичи, но вместе с предыдущими это вполне может перевесить.

Да да очередная защита не понятно кого не понятно от чего

Eсли разрешить реализовать "чужие" трейты для "чужих" типов, то не ломающие (по semver) изменения очень даже смогут ломать компиляцию. Было бы совсем не весело получать сломанный билд при обновлении зависимостей, причём зависимости сами по себе ничего плохого как бы и не изменили.

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

use lib_a::SomeTrait for crate::MyType, lib_b::AnotherType;

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

я даже не могу из за этого организовать СВОИ типы по разным модулям потому

По разным модулям - можно, нельзя по разным крейтам.

Я несколько не понял - а как явное указание реализации типажа защищает от багов в реализации?

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

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

Явное указание автором типа что тип поддерживает данный типаж предполагает, что автор типа не случайно по сигнатуре совпал с типажом (в этом случае высок шанс что сигнатура-то совпала а вот семантика типажа - нет), а намеренно реализовал поддержку типажа, т.е. и сигнатуру и семантику. От багов - да, это не защищает, но в такой ситуации если семантика нарушена то это именно баг, и его можно и нужно пофиксить, и фикс такого бага не будет считаться изменением ломающим обратную совместимость (т.е. может быть сделан в патч-версии а не следующей мажор-версии).

Истинная утиная типизация есть в шаблонах C++. Там можно в шаблонном коде вызвать у переданного типа метод крякания. И всё будет работать, пока в шаблон передаются типы с нужным методом и сигнатурой.

Угу, вот только forward iterator от input iterator вы так не отличите, потому что набор синтаксических требований одинаков.

"каждый проект, с которым я имел дело на C++, ощущался как монотонная рутина"

Значит, автор решал в своих проектах скучные задачи. Когда задача скучная, начинаешь играть с языком, чтобы хоть как-то веселее писать было. Для интересной задачи лучше, когда выражение идей в коде -- тривиальный процесс, а не квест.

В чём проблема написать if(smart_ptr1) ?

Звучит как "в чём проблема не совершать ошибок?"

Умные указатели для того и придумали, чтобы не совершать ошибок. Только их надо использовать с умом. Просто в C++ у вас есть выбор затрачивать ли ресурсы на проверку, что память жива или сделать быстрый алгоритм, если уверены. Не нравятся указатели используйте ссылки и значения.

Только их надо использовать с умом

А в чём их умность тогда? Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move.

Всё что они делают в С++ это RAII но не гарантируют безопасной работы с указателями (но всё же лучше чем сырые указатели конечно)

Не нравятся указатели используйте ссылки и значения

Указатели в С++ тоже могут стать не валидными (dangling references)

Ни я, ни C++ не заставляем вас и кого угодно ещё использовать указатели сырые/умные, ссылки и мувы. Используйте переменные по значению! И будет вам счастье, если вы так хотите. Это ваш выбор. Весь посыл в том, что C++ предоставляет этот выбор.

Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move.

Вообще-то unique_ptr гарантированно обнуляются после std::move, да и как может быть по-другому? Иначе они бы не были уникальными.

Всё же обнуление - это фича в данном случае. Могли бы не обнулять, а просто написать после move значение не определено

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

Не могли бы.

После перемещения объект должен оставаться в валидном состоянии (как минимум, деструктор всё ещё должен иметь возможность отработать!).

Они не достаточно умные чтобы не обнуляться или перестать быть валидными после std::move

Остающиеся в "unspecified but valid state" (c) moved-from объекты — by design. Если вам совсем-совсем не хочется их видеть после мува, то можно ограничить скоуп руками, это несложно и добавляет лишь немного шума.

unspecified but valid state

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

если указатель переместили (std::move), то предыдущая локация станет по факту uninitialized и попытки читать что-либо из таких переменных является неопределённым поведением. В дебаге оно вам радостно занулит память, если повезёт, а в релизе вы с большой долей вероятности получите магическое поведение и рандомные сегфолты.

если указатель переместили (std::move), то предыдущая локация станет по факту uninitialized и попытки читать что-либо из таких переменных является неопределённым поведением.

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

https://timsong-cpp.github.io/cppwp/n4861/unique.ptr#single.ctor-20

Это из C++20, но и раньше было так же.

Я скорее про ситуацию что выше рисовали.

auto ptr = std::make_unique<T>();
call(std::move(ptr));
if (ptr) { // <<< вот это по идее будет UB
   // some code
}

Соответсвенно, от такого кода никакие проверки не спасут.

вот это по идее будет UB

Нет. Я же выше даже ссылку на драфт C++20 кинул. Нет тут никакого UB. Откуда ему тут взяться?

Интересно, что автор скажет про Питон, с высоты своего опыта освоения Раста?

В том плане, что ему нравится и не нравится в парадигме, концепции языков, несмотря на столь разные области применения?

И может ли кто сказать об опыте использования Раст для нейросетей и Дата Сайенс, вообще?

И может ли кто сказать об опыте использования Раст для нейросетей и Дата Сайенс, вообще?

HuggingFace выкатили Candle, фреймворк для машинного обучения. На него уже портировали известные сети, вроде YOLO. Так что, видимо, опыт хороший.

Не автор, но рефакторю сейчас большой проект на Python. Хочу сказать что надо на законодательном уровне запретить более N строк кода на нем писать, а в качестве наказания - гулаг.

Data Science - единственное для чего нет толковой альтернативы Питону, неизбежное зло.

Это вы ещё левиафанов MATLAB не рефакторили ... с пользовательским интерфейсом и печатью в pdf ...
Вот уж реальная альтернатива Python (точнее наоборот - пайтон - это альтернатива матлабу, слава аллаху!)

Я не начинаю писать на Python что-либо если моя оценка "больше 1000 строк кода".

1 KLoC оценки обычно превращаются в 2 KLoC написанного кода, включая нюансы.
Если сервис выходит за 5 KLoC - то после этого объёма прототип перестаёт быть нормально поддериваемым и каждое возвращение это боль и вспоминание "как вот это сделано".

Теоретически - наверное на Python можно инженерить ПО. Но зачем тогда вообще Python если его киллер-фичу (возможность бысто запрототипировать) мы вообще не используем.

Спасибо за статью. Очень грамотно изложено.

Графомания. playground есть - вау, запишу в плюсы расту. Утиная типизация с "for Trait" - ага, ага, хотя бы определение в вики прочитать

Rust феноменально сильно влияет на мышление, но ничего действительно важного в статье не написано. Очередной раз сказали, что borrow checker это сложно, ну а дальше то что? Как поменялся подход к мышлению? Как по-другому проектирутся алгоритмы или архитектура? Может на других языках автор начал писать по-другому? Я только издалека посмотрел на rust, так и то интересные идеи почерпнул для своего C++ кода. Абсолютно пустая статья.

То, что в раст свежая, прям "нулёвая" экосистема - это приятно. Вместо Make / CMake / Doxygen / ... просто cargo xxx. Но это может и протухнуть лет за 10.

Но давайте дадим слово начальнику транспортного цеха? Как всё-таки Rust меняет мышление?

Вот мне с Haskell понравилось (реально появилось много хороших приёмов в программировании).
А с Rust - мне кажется у меня ситуация обратная ожиданиям.

Ожидание - написание программ на Rust это доведение до автоматизма практик написания кода, где некоторые классы ошибок в принципе отсутствуют.
Реальность - Rust настолько хоршо тебя контролирует, что ты даже забываешь думать о таких ошибках. И при переходе с Rust на привычные языки (скажем на C где у нас низкоуровневые библиотеки) - пару дней приходится заново привыкать следить за вещами, которые за тебя делал Rust.



Первое впечатление от статьи: знающий haskell и умеющий в unique_ptr<> в плюсах обнаружит, что ему всё уже знакомо. Правильное или обманчивое впечатление?

Вы про Rust или про статью?

Если про Rust - то, да будет кардинально проще (если придираться контрольного "изучить Rust не зная Haskell и C++").

Хотя от понимания машинерии многих мелких нюансов это вас это не убережёт.

Автор, если тебе после C++ понравился Rust, то попробуй еще PHP. Через год Rust и другие языки типа Go, Python, C#, Java будут казаться архаичными, неудобными, странными. Привыкнув к PHP ты не захочешь пересаживаться на что то еще, как человек, катавшийся на Лексусе или Порше вдруг сядет в ржавую "копейку". У меня было "за плечами" штук 15 языков программирования, начиная от скриптов, basic-а и ASM, заканчивая 1C и SQL. До этого скакал с языка на язык быстро и легко. После перехода на PHP, пытался поглядывать в сторону руби, питона, го, эрланга - не получается себя заставить обратно загнать в убогие неудобные рамки.

э... это какая-то очень тонкая, понятная лишь избранным ирония?

Если так - поясните для более приземлённых пользователей Хабра.

Но потом я понял. Rust позволяет мне делать то, что я хочу, но сначала просит подумать, действительно ли я хочу именно этого, и заставляет задуматься о последствиях моего решения.

Аллилуя!

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

Впусти господа в сердце.

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

Аминь.

Тег сарказм:)

Тема trait не раскрыта. Это сильно похоже на interface в Golang. Только в Go это будет единственный способ полиморфизма, и там это похоже на C++ vtable чем-то, т.е. просто вызвать функцию не получится, надо прыгать ещё через один указатель (cache и branch prediction пострдают).
Тема не раскрыта, потому что в расте trait - это толи C++20 concepts, которые в шаблонах нам дают ту самую утиную типизацию (а значит компилятор может напрямую вызывать функции), или тупо базовый интерфейсный класс, с виртуальными функциями (и виртуальным деструктором). Я бы прогулил быстрее, чем писал, но друг ещё кому-то интересно тоже.

Вы не любите с++? Вы просто не умеете его готовить)))

Sign up to leave a comment.