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)

А так не ругается, эти ворнинги не гарантируют ничего https://godbolt.org/z/M68b69Ec8

#include <iostream>

struct Data {
   const int &ref_;

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

   }

   void print();
};

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

int main() {
  Data t(42);

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

  return 0;
}
pftBest ★★★★
()
Ответ на: комментарий от pftBest
  // попытка напечатать 42
  // по ссылке на него, тогда как 42 давно
  // не существует (оно существовало только
  //во время вызова конструктора Data())

Что за безмозглый дебил это всё написал?

Ссылка продлевает жизнь любого захваченного временного объекта до конца своей жизни:

15.2
Temporary objects
 
The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference ...
LamerOk ★★★★★
()
Ответ на: комментарий от ox55ff

persists for the lifetime of the reference

До конца работы конструктора.

Проблемы с чтением английского текста? До конца жизни ссылки. Тут видимая сложность только в том, что контекстов со «временными объектами» больше одного, но сам объект корректно доедет от ссылки к ссылке и будет жить.

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

Во-первых, по твоей же ссылке всё прекрасно живёт:

Program returned: 0
Program stdout
Foo
~Foo
Print: 0

Зачем ты даёшь ссылку, по которой даже сам не сходил?

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

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

Отличный пример кстати. Он более злой, чем мой изначальный. И вот как с ним бороться в крестах средствами компилятора? Никак? Только статический анализатор тащить?

https://godbolt.org/z/cqeznTvxd

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

https://godbolt.org/z/xoM56jdhr

#include <iostream>

#ifndef _MSVC_LIFETIMEBOUND
#if !defined(__has_cpp_attribute)
#define _MSVC_LIFETIMEBOUND
#elif __has_cpp_attribute(msvc::lifetimebound)
#define _MSVC_LIFETIMEBOUND [[msvc::lifetimebound]]
#elif __has_cpp_attribute(_Clang::__lifetimebound__)
#define _MSVC_LIFETIMEBOUND [[_Clang::__lifetimebound__]]
#else
#define _MSVC_LIFETIMEBOUND
#endif
#endif

struct Foo {
    int a_;

    explicit Foo(int a) : a_(a) { std::cout << "C Foo\n"; }
    ~Foo() {
        std::cout << "~ Foo\n";
        a_ = 0;
    }
};

struct Data {
    const Foo &ref_;

    explicit Data(const Foo &_r _MSVC_LIFETIMEBOUND) : ref_(_r) {}

    void print();
};

void Data::print() { std::cout << "Print: " << ref_.a_ << "\n"; }

int main() {
    Data t(Foo(42));

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

    return 0;
}

<source>:36:12: error: temporary whose address is used as value of local variable 't' will be destroyed at the end of the full-expression [-Werror,-Wdangling]
   36 |     Data t(Foo(42));
      |            ^~~~~~~
1 error generated.
fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 1)
Ответ на: комментарий от fsb4000

Имя _MSVC_LIFETIMEBOUND потому что я копирнул его из сорцов MSVC STL. В своём коде не нужно именовать макросы и другие объекты начинающиеся с _.

Там он используется для части функций, чтобы компилятор выдавал ошибки на неправильное использование известных функций, типа min, max, minmax, clamp и т.д.

Вот как выглядит определение функции std::max:

_EXPORT_STD template <class _Ty, class _Pr>
_NODISCARD constexpr const _Ty&(max) (const _Ty& _Left _MSVC_LIFETIMEBOUND, const _Ty& _Right _MSVC_LIFETIMEBOUND,
    _Pr _Pred) noexcept(noexcept(_Pred(_Left, _Right))) /* strengthened */ {
    // return larger of _Left and _Right
    return _Pred(_Left, _Right) ? _Right : _Left;
}

_EXPORT_STD template <class _Ty>
_NODISCARD _Post_equal_to_(_Left < _Right ? _Right : _Left) constexpr const _Ty& //
    (max) (const _Ty& _Left _MSVC_LIFETIMEBOUND, const _Ty& _Right _MSVC_LIFETIMEBOUND)
        noexcept(noexcept(_Left < _Right)) /* strengthened */ {
    // return larger of _Left and _Right
    return _Left < _Right ? _Right : _Left;
}
fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 1)
Ответ на: комментарий от pftBest

А для gcc есть такой аналог?

Насколько я знаю, атрибута lifetimebound нет.

Но у gcc есть опция -fanalyzer, которая тоже много чего находит. И в данном примере в частности:

https://godbolt.org/z/d4ec8qfh7

#include <iostream>

struct Foo {
    int a_;

    explicit Foo(int a) : a_(a) { std::cout << "C Foo\n"; }
    ~Foo() {
        std::cout << "~ Foo\n";
        a_ = 0;
    }
};

struct Data {
    const Foo &ref_;

    explicit Data(const Foo &_r) : ref_(_r) {}

    void print();
};

void Data::print() { std::cout << "Print: " << ref_.a_ << "\n"; }

int main() {
    Data t(Foo(42));

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

    return 0;
}
 In member function 'void Data::print()':
<source>:21:53: error: use of uninitialized value '*this_8(D)->ref_.Foo::a_' [CWE-457] [-Werror=analyzer-use-of-uninitialized-value]
   21 | void Data::print() { std::cout << "Print: " << ref_.a_ << "\n"; }
      |                                                ~~~~~^~
  'int main()': events 1-3
    │
    │   23 | int main() {
    │      |     ^~~~
    │      |     |
    │      |     (1) entry to 'main'
    │   24 |     Data t(Foo(42));
    │      |                  ~
    │      |                  |
    │      |                  (2) region created on stack here
    │......
    │   30 |     t.print();
    │      |     ~~~~~~~~~
    │      |            |
    │      |            (3) calling 'Data::print' from 'main'
    │
    └──> 'void Data::print()': events 4-5
           │
           │   21 | void Data::print() { std::cout << "Print: " << ref_.a_ << "\n"; }
           │      |      ^~~~                                      ~~~~~~~
           │      |      |                                              |
           │      |      (4) entry to 'Data::print'                     (5) ⚠️  use of uninitialized value '*this_8(D)->ref_.Foo::a_' here
           │
fsb4000 ★★★★★
()
Ответ на: комментарий от fsb4000

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

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

Не понятно почему претензия к c++? Он делает то, о чем его просят. Ты сам сохраняешь ссылку на объект, не обеспечивая его время жизни.

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

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

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

Делать то, чего просят - это удел сишечки

Ошибаетесь. C++ как раз был создан для того, чтобы не нужно было прибегать к сишечке, но при этом чтобы компилятор делал то, что просят.

Это сейчас те, кому нужна «безопасность и надежность» вместо того, чтобы брать Rust или любой другой безопасный ЯП, почему-то начинают требовать того же и от C++.

ЗЫ. Гораздо хуже не то, что C++ не контролирует вещи, которые проверяются в Rust-е, а то, что в самом C++ куча тонких моментов, про которые многие даже не знают, но которые ведут к UB. В итоге не всегда получается «сделай то, что просят», т.к. компилятор говорит «а у тебя здесь UB, поэтому я сделаю что мне в голову взбредет», даже без предупреждения. В качестве примеров:

  • многие ли из действующих C++ников помнят, что обращаться к неактивному члену union – это UB?
  • многие ли из действующих C++ников помнят, про alignas?
  • многие ли из действующих C++ников помнят, что reinterpret_cast не начинает lifetime для объекта?
eao197 ★★★★★
()
Ответ на: комментарий от lesopilorama

Вот я как пишущий на C++ активно интересуюсь как перед Rust оправдываться конечно же.

А как ты планируешь оправдываться перед пишущими на python, js, java и еще 100500 других ЯП?

BRE ★★
()

Вот пример побольше.

#include <iostream>
#include <string>
#include <string_view>

class Foo {
public:
  Foo(std::string_view str) : m_str(str) {}

  std::string_view getPart() { return std::string_view(m_str).substr(0, 10); }

private:
  std::string m_str;
};

void bar(std::string_view str) { std::cout << str << std::endl; }

int main() {
  // Good
  {
    Foo foo("abc01234567890");
    auto s = foo.getPart();
    // ...
    bar(s);
    (void)Foo("xxx01234567890").getPart();
    bar(s);
  }

  // Bad
  {
    auto s = Foo("xyz01234567890").getPart();
    // ...
    bar(s);
    (void)Foo("gggGgggggggggg").getPart();
    bar(s);
  }
  return 0;
}
$ g++ -O3 -Wall -Wextra -Wpedantic bad_string_view.cpp && ./a.out 
abc0123456
abc0123456
xyz0123456
gggGgggggg
AlexVR ★★★★★
()

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

не надо обвинять с++ что он умеет все. для того он и создан.

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

В смысле изменение одного члена через другой недопустимы?

В C++ – нет. И, насколько я помню, всегда нельзя было.

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

А если эти члены имеют одинаковый размер?

Без разницы.

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

Прежде всего для экономии памяти. Всякие type punning через union в C++ под запретом. Для таких вещей, если не ошибаюсь, в совсем современные плюсы bit_cast завезли.

Еще, вроде бы, union может использоваться как неинициализированное хранилище под последующее создание объекта compile-time.

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

Ну здрасьте, тип-сумма (хотя бы такая) это его изначальное предназначение, и так он гораздо важнее и полезнее, а вот использовать его для type punning странная и чреватая затея

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

А как ты планируешь оправдываться перед пишущими на python, js, java и еще 100500 других ЯП?

Тут банально скоростью, статической типизацией, мощью шаблонов, чем-то там ещё: тут как раз просто разбрехаться.

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

-fanalyzer - это кандидат на геноцид Rust-фанатов

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

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

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

Но если ты серьезно этим занимаешься, то попробуй оправдаться перед @lovesan с лиспом. Расскажи ему про мощь шаблонов со скоростью. :)

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

Со всякими лиспами и хаскелями оправдание ещё проще: наш инструмент проще в применении, нужно быть менее умным, это расширяет рынок кадров. Линейная процедурная хренотень без всякой зауми.

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

Линейная процедурная хренотень без всякой зауми.

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

наш инструмент проще в применении, нужно быть менее умным

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

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

придётся кастом обходится

Формально это конечно UB. Но если какой-то компилятор это сломает, то столько кода отвалится, что на мой взгляд этого можно совершенно не опасаться. Если у тебя не какой-то узконаправленный специфичный компилятор, а clang/gcc/msvc, то я не вижу смысла делать не так, как удобно.

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

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

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

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

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

Формально это конечно UB. Но если какой-то компилятор это сломает, то столько кода отвалится, что на мой взгляд этого можно совершенно не опасаться.

Насколько я знаю и в libc++ и в MSVC STL обращаются к неактивному члену union в рамках реализации стандартной библиотеки. libstdc++ не смотрел.

Сейчас на это есть проверки лишь в constexpr у gcc и clang:

https://gcc.godbolt.org/z/anzP4daP9

constexpr int f()
{
    union {
        int x{0};
        char y;
    };
    auto & z = y;
    z = 1;
    x = 0;
    return x;
}

static_assert( f() == 0 );

int main() {}
error: static assertion expression is not an integral constant expression
   13 | static_assert( f() == 0 );
      |                ^~~~~~~~
<source>:8:7: note: assignment to member 'y' of union with active member 'x' is not allowed in a constant expression
    8 |     z = 1;
      |       ^
<source>:13:16: note: in call to 'f()'
   13 | static_assert( f() == 0 );
      |                ^~~
1 error generated.
fsb4000 ★★★★★
()
Ответ на: комментарий от zurg

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

Сомнительно, что такая оптимизация допустима. Тогда отвалится такое:

union U {
    char c;
    int  i[32];
};
U u{.c = 0};
memset(&u, 0, sizeof(U));

А по идее это вполне валидный код.

если структура лежит - поля может подвигать

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

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

То, что в constexpr нельзя reinterpret_cast делать (в том числе и через union) это вообще моя боль. Периодически хочется. bit_cast помогает, но это по сути constexpr memcpy, а это не тоже, что и reinterpret_cast.

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

Но вообще это, конечно, странная ситуация. Насколько я в курсе кастовать объект к std::byte* можно (как минимум POD). И кастовать std::byte* (с правильным выравниванием) в POD тоже можно. И это не UB.

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

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