LINUX.ORG.RU

C++ vs Rust - проблемы этих языков на примерах кода.

 ,


2

5

Хочется срача по сабжу, но без эпистолярных рассуждений, а чисто на примерах кода.

  1. Rust фанат кидает пример C++ говнокода.
  2. C++ енджоер объясняет, что данный код в 2024 все C++ разрабы пишут не так (и показывает как) и никаких проблем не может быть.

И наоборот.

Например мне Rust-фанаты кинули такое:

#include <iostream>

struct Data {
   const int &ref_;

   explicit Data(const int &_r)
   : ref_(_r) {

   }

   void print() {
      std::cout << ref_ << "\n";
   }
};

int main() {
  Data t(42);

  // Тут ты умер - попытка напечатать 42
  // по ссылке на него, тогда как 42 давно
  // не существует (оно существовало только
  //во время вызова конструктора Data())
  t.print();

  return 0;
}

Но выяснилось, что Rust-фанаты наврали, потому что C++ такое тупо не скомпилирует, если вызывать компилятор с нормальными пацанскими опциями «просто не пропускай херню»:

g++ test.cpp -O3 -Wall -Werror



Последнее исправление: lesopilorama (всего исправлений: 3)
Ответ на: комментарий от intelfx

Можно кастовать в тот же POD, который был. В другой — нельзя. Добро пожаловать в TBAA :-)

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

А сделать это напрямую не могу. Это, так-то, ещё больший маразм.

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

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Ну и можно без сети просто через memcpy, по идее. Видимо это продиктовано strict aliasing’ом, но это должно быть UB только в случае нарушения strict aliasing’а. Если его нет, то с чего вдруг это UB должно быть.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Тогда отвалится такое:

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

Тогда при передаче ссылки на эту структуру в функцию всё сломается.

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

zurg
()
Последнее исправление: zurg (всего исправлений: 1)
Ответ на: комментарий от zurg

Эээ нет, с чего бы

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

в промежутках вызовов таких функций, он волен делать что угодно

Ну если он способен отследить это, то и обращения к другому члену сможет отследить и никакие оптимизации оно не сломает.

если активно с и надо скопировать весь union достаточно будет скопировать только маленький с

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

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

С чего вдруг нарушаю?

С того, что мы разговариваем в контексте вот этого:

Насколько я в курсе кастовать объект к std::byte* можно (как минимум POD). И кастовать std::byte* (с правильным выравниванием) в POD тоже можно. И это не UB. А вот кастовать напрямую нельзя. И union, который делает ровно тоже самое нельзя.

В рамках C++, ответ — «нельзя». Во всех трёх случаях.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 1)
Ответ на: комментарий от intelfx

В рамках C++, ответ — «нельзя». В обоих случаях.

Тут согласен.

Правда всё равно можно сделать отдельную so’шку, в которой байтики кастовать в один тип, а в основной программе кастовать другой тип в байтики и тогда всё тоже самое, но UB уже как бы и нет.

Но даже если без таких костылей, нарушением strict aliasing’а это будет не всегда. Например в чём оно тут:

const float f = 0.5;
const uint8_t* p = reinterpret_cast<const uint8_t*>(&f);
int32_t val = *reinterpret_cast<const int32_t*>(p);
Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

В том, что ты обращаешься к объекту типа float через lvalue типа int32_t. Ваш кэп.

(Это нарушение strict aliasing, и в твоём коде есть UB, вне зависимости от того, через сколько слоёв промежуточных кастов ты пройдёшь.)

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 1)
Ответ на: комментарий от intelfx

Ок, согласен. И тут не прав. Оно формулируется как вообще любой доступ через не compatible и не char* тип. Я почему-то думал, что там именно одновременный доступ нужен.

Ну тогда получается, что маразм не в reinterpret_cast и union, а в такой формулировке strict aliasing.

Можно собрать либу с

int32_t foo_cast(const uint8_t* data) {
    return *reinterpret_cast<const uint32_t*>(data); // start_lifetime_as...
}

И main с

int main() {
    float f = 0.5;
    int32_t i = foo_cast(reinterpret_cast<uint8_t*>(&f));
}

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

А если слинковаться статически, то UB, смерть, компилятор крушить.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Это единственная формулировка strict aliasing, которая позволяет проводить TBAA (type-based alias analysis). Вроде как это один из столпов, на которых вообще держатся оптимизирующие компиляторы C и C++.

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

Взаимодействие SysV ABI и стандарта C++ комментировать не берусь, но вполне возможно, что UB всё равно есть, просто на текущем уровне развития технологий этот факт нельзя заметить.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 1)
Ответ на: комментарий от intelfx

но вполне возможно, что UB всё равно есть

Ну по отдельности это две well defined program. Так что откуда тут UB возьмётся не понятно. Тогда вообще любая хоть что-то делающая программа UB.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

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

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

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)
Ответ на: комментарий от Ivan_qrt

А разве два динамических объектных файла в одном адресном пространстве — это две разные well-defined program?

(Потому и говорю, что не берусь судить, как именно взаимодействует динамическая компоновка и стандарт C++.)

intelfx ★★★★★
()
Ответ на: комментарий от Ivan_qrt

вполне достаточно, чтобы доступ через разные типы не пересекался

А ещё компиляторы умеют (и имеют право) менять обращения к памяти местами, если (опять же, в рамках стандарта) можно доказать, что эти обращения к памяти являются независимыми.

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

intelfx ★★★★★
()
Ответ на: комментарий от intelfx

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

Почему он так решит? Читается указатель на float и надо ли его перечитывать определяется именно им. int32_t в данном случае копия.

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

А разве два динамических объектных файла в одном адресном пространстве — это две разные well-defined program?

Тема тонкая и возможно я не прав, но если это не так, то тогда является ли программа well defined зависит от того, какие библиотеки находятся в системе и какое ядро загружено. Это по-моему перебор даже для C++.

А ещё компиляторы умеют (и имеют право) менять обращения к памяти местами

Но они не могут поставить запись после чтения, если в коде до. Соответственно reinterpret_cast будет после записи в оригинальный объект всегда. И тогда они по-прежнему останутся «не пересекающимися», если в коде так написано.

Ivan_qrt ★★★★★
()
Последнее исправление: Ivan_qrt (всего исправлений: 1)
Ответ на: комментарий от intelfx

Но reinterpret_cast читает из того же типа, что и объект.

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

Если ты сохранил ссылку другого типа и дальше работаешь только с ней, то оптимизациями компилятор это не сломает.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от intelfx

Насколько я в курсе кастовать объект к std::byte* можно (как минимум POD). И кастовать std::byte* (с правильным выравниванием) в POD тоже можно. И это не UB. А вот кастовать напрямую нельзя. И union, который делает ровно тоже самое нельзя.

В рамках C++, ответ — «нельзя». Во всех трёх случаях.

С чего это вдруг?

  1. Кастовать к char*/byte* можно абсолютно всё, для чтения и записи
  2. Кастовать массив байтов/чаров в типы с тривиальным ctr/destr - также можно в следующем случае - при создании массива чаров или возврат из malloc и смежных данный буффер находится как бы в состоянии суперпозиции и в нем можно создать любой implicit lifetime тип простым кастом без всякого ЮБ.
  3. Насколько понимаю, если в буфере кастом уже был создан объект в соответствии с пунктом 2, то в нем можно создать другой через std::start_lifetime_as()

Ну и очевидно, что т.к. implicit lifetime объект не имеет конструктор/деструктор, то нижележащий буфер не модифицируется. Всё это даёт возможность на законных основаниях, например, получить данные из сети и кастонуть их в структуру, у нее начнется лайфтайм, законно читаем структуру. До недавнего времени всё это было ЮБ, но вряд ли есть компилятор, который за это бил по рукам, фактически это всё давно работало

kvpfs_2
()
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

Кастовать к char*/byte* можно абсолютно всё, для чтения и записи

Против этого я ничего не говорил.

Кастовать массив байтов/чаров в типы с тривиальным ctr/destr - также можно в следующем случае - при создании массива чаров или возврат из malloc

Это особый исключительный случай.

Насколько понимаю, если в буфере кастом уже был создан объект в соответствии с пунктом 2, то в нем можно создать другой через std::start_lifetime_as()

Туда же.


Ты на контекст разговора смотри. Контекст разговора был в том, что можно скастовать A* -> char*, а потом char* -> B* и всё будет зашибись (не будет).

intelfx ★★★★★
()
Ответ на: комментарий от intelfx

Ты на контекст разговора смотри. Контекст разговора был в том, что можно скастовать A* -> char*, а потом char* -> B* и всё будет зашибись (не будет).

Прямой каст, конечно, нет. Но вот кто помешает иметь char буфер и начинать в нём лайфтайм разных структур? Эффект тот же будет.

Это особый исключительный случай.

Ну как особый, по сути теперь можно кастануть что угодно (не считая нетривиально констр/дестр типы). Меня лишь одно парит - почему нужно было придумывать новый start_lifetime_as(), когда можно было разрешить аналогичное для reinterpret_cast()? Было бы на одну сущность меньше. Ну хоть так

kvpfs_2
()
Ответ на: комментарий от kvpfs_2

Прямой каст, конечно, нет.

Кстати, я не вижу причин, почему нельзя вызвать start_lifetime_as() используя другую простую структуру как storage, ну если все вопросы по части там выравнивания и тп, не мешают. Но может ошибаюсь, может я не знаю о каких-то требованиях

kvpfs_2
()

Пожалуйста:

#include <memory>
#include <iostream>
#include <functional>

int main() {
    std::vector<std::unique_ptr<int>> storage;

    auto p = std::make_unique<int>(42);

    auto closure = [p = std::move(p), &storage] () mutable {
        std::cout << *p << " " << storage.size() << std::endl;
        storage.push_back(std::move(p));
    };

    closure();
    //closure();

    return 0;
}

и эквивалентный

use std::boxed::Box;

fn main() {
    let mut storage: Vec<Box<i32>> = Vec::new();

    let p = Box::new(42);

    let closure = {
        let storage = &mut storage;
        let p = p;
        
        move || {
            println!("{} {}", *p, storage.len());
            storage.push(p);
        }
    };

    closure();
    // closure();
}

Раскомментирование второго вызова в расте - ошибка компиляции, в цпп - UB.

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

Сегфолт не потому что ты move unique_ptr, котоый уже мувнут, а потому что ты разыменовываешь нулевой указатель

std::cout << *p ...

Не заметил это сразу. Пвоторяю - мувнуть мувнутый объект - не ЮБ, это норм.

Эх, растаманы …

kvpfs_2
()
Ответ на: комментарий от AntonI

Тогда непонятно нафик этот юнион вообще…

Чтобы реализовать на нём std::variant и дальше пользоваться им. Ну или для систем динамической типизации с оптимизациями под часто используемые типы, как в QVariant.

annulen ★★★★★
()

если хочется срача, вся т.н. «безопасность» раста берется из 3 источников: 1) он заставляет тебя писать все явно, вместо каких-либо абстракций 2) он тупо не реализовывает «небезопасные» фичи в стандартной библиотеке 3) он просто врет.

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

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

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

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

Lrrr ★★★★★
()
Последнее исправление: Lrrr (всего исправлений: 1)
Ответ на: комментарий от Ivan_qrt

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

Даже если отбросить высокие материи из разряда strict aliasing - что гарантирует что alignment не съедет?

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

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

Ivan_qrt ★★★★★
()

struct Data { const int &ref_; };

Что вот это такое? Указатель в структуре или что ты туда пытаешься записать?

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

оно существовало только во время вызова конструктора Data()
g++ test.cpp -O3 -Wall

Так нет никакого конструктора, шизик, у тебя все проинлайнилось там. Приводим код в адекватный вид и:


struct Data {
   const int ref_;

   Data(const int _r) : ref_(_r) {}

   void print();
};

void Data::print() {
    std::cout << *((int *)&this[0]) << " size(" << sizeof(Data) << ")\n";

int main() {
  Data t(42);
  t.print(); //=> 42 size(4)
}


Теперь посмотрим что у нас в палате творится
struct Durka {
   const int &ref_;

   explicit Durka(const int &_r) : ref_(_r) {}

   void print();
};

void Durka::print() {
    std::cout << *((int *)&this[0]) << " size(" << sizeof(Durka) << ")\n";
}

int main() {
  Durka t(42);
  t.print(); //=> -1379995908 size(8)
}

В структуре лежит указатель понятное дело

uin ★★★
()