LINUX.ORG.RU

C++: как сделать, чтобы это не компилировалось?

 


2

2

Берём ссылку на временный объект, который непонятно сколько времени существует. Потом пытаемся печатать содержимое объекта по ссылке, которая хрен знает куда ведёт.

-Wall -Werror – достаточно по-пацански так жить?


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

   }

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

int main() {
   
   Data t(42);
   t.print(); // развал жопы на куски
   return 0;
}



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

Перейти на rust.

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

g++ test.cpp -O3 -Wall -Werror и готово.

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

Нужно принять следующее утверждение и не делать иначе: «Если что-то передаётся по ссылке/указателю, то оно не сохраняется внутри объекта, если для иного явно не приняты меры». Мерами могут быть - передача shared_ptr, unique_ptr, …, или подобная обёртка:

template <typename T>
requires std::is_pointer_v<T>
using stored_ptr = T;

struct S {
   int *p;
   S(stored_ptr<int *> i) : p(i) {}
};

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

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

В простых случаях опции компайлера помогут, в сложных нет.

Нахрена две темы про одно и то же создавать? Придёт модератор, сделает атата и будет прав.

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

В новых стандартах начали обсуждать borrow-checker. Может в каком ни будь c++34 и завезут вместе с inout семантикой.

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

Думаю, тут еще дело в том, что хранить ссылки вот таким способом - это неверный подход. Это как использовать unsafe на rust, после которого его адепты будут кричать про то, нафига вы так пишите.

Кстати на gcc 14.2 твой код компилируется с этими ключами (-Wall -Werror):

https://godbolt.org/z/1hzc4szW1

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

Нужно принять следующее утверждение и не делать иначе: «Если что-то передаётся по ссылке/указателю, то оно не сохраняется внутри объекта,

По указателю сохранять нормально.

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

Данный подход позволяет создавать всякие обёртки, которые что-то делают с каким-то объектом. Ссылка тут потому, что «не указатель», т.е. туда синтаксически сложнее присвоить любой бред, хотя и можно.

В общем, shared_ptr видимо надо поюзать в подобном месте.

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

По указателю сохранять нормально.

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

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

fn(&var);
// вместо безликого
fn(var);
kvpfs_2
()
Ответ на: комментарий от kvpfs_2

У меня такое правило:

// если fn собирается МЕНЯТЬ этот var.
fn(&var);

// в иных случаях (читать, "просто хранить")
fn(var);
lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 1)
Ответ на: комментарий от lesopilorama

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

template <typename T>
class Stored_ptr {
   template <typename U>
      friend Stored_ptr<U> make_stored_ptr(U *p);
   Stored_ptr(T *ptr) : p{ptr} {};
public:
   T *p;
};

template <typename U>
Stored_ptr<U> make_stored_ptr(U *p) {
   return {p};
}

struct S {
   int *p;
   S(Stored_ptr<int> ptr) : p{ptr.p} {}
};

int main() {
   int *i = nullptr;
   S s(make_stored_ptr(i));
}

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

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

Со стандарта:

15.2
Temporary objects
...
6 The third context is when a reference is bound to a temporary. 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 except:
—
(6.1)
A temporary object bound to a reference parameter in a function call (8.2.2) persists until the completion
of the full-expression containing the call.
—
(6.2)
The lifetime of a temporary bound to the returned value in a function return statement (9.6.3) is not
extended; the temporary is destroyed at the end of the full-expression in the return statement.
...

Конкретно в ОП-посте исключение п. 6.1. Для возвращаемых значений это работает:

9.6.3
The return statement
...

3
The copy-initialization of the result of the call is sequenced before the destruction of temporaries at the end
of the full-expression established by the operand of the return statement, which, in turn, is sequenced before
the destruction of local variables (9.6) of the block enclosing the return statement.
user@computer:$ cat cpp_reference.cpp 
#include <optional>
#include <iostream>

struct T
{
    int i;
    T(int i):i(i){};
    ~T(){i = 42;};
};


T f1()
{
    return T(7);
}

std::optional<T> f2()
{
    return T(13);
}

int main()
{
    const T &t1 = f1();
    std::cout << "Print: " << t1.i << "\n";

    std::optional<T> t2;
    // UB: std::cout << "Print: " << t2->i << "\n";

    t2 = f2();
    std::cout << "Print: " << t2->i << "\n";

    return 0;
}
user@computer:$ 
user@computer:$ g++ -std=c++23 -O0 -Wall -Werror  cpp_reference.cpp -o temp_reference
user@computer:$ ./temp_reference 
Print: 7
Print: 13
user@computer:$
LamerOk ★★★★★
()
Последнее исправление: LamerOk (всего исправлений: 2)
Ответ на: комментарий от LamerOk

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

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

просто вызовите еще че-нить между инициализацией переменной и принтом.

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

никто тут «время жизни объекта» не продлевал.

Временный объект должен быть «уничтожен» по окончании выражения (по достижении sequence point’а). То, что с ним можно работать после - «extended lifetime».

-O0

запись 42 в деструкторе просто выкидывается, поскольку оптимизация

Мде.

в данном случае просто никто еще не переписал стек и потому вам принтуется нужный мусор
просто вызовите еще че-нить между инициализацией переменной и принтом.

А самому продемонстрировать слабо?

user@computer:$ cat cpp_reference.cpp 
#include <math.h>
#include <iostream>
#include <sstream>
#include <stdio.h>

struct T
{
    int i;
    T(int i):i(i){};
    ~T(){i = 42;};
};

T f()
{
    return T(7);
}

int main()
{
    const T &t = f();

    // allocate some space on stack
    char buf[1024] = {0};
    // fill it with data
    snprintf(buf, sizeof(buf), "%099f\n", M_PI);

    // more data on stack and function calls
    std::stringstream ss;
    ss.rdbuf()->str(buf);
    // std::hexfloat available since c++11 only
    ss << std::fixed << std::scientific << M_PI << "\n";
    std::cout << ss.str();

    // access to temporary object
    std::cout << "Print: " << t.i << "\n";

    return 0;
}
user@computer:$ g++ -std=c++98 -O0 -Wall -Werror  cpp_reference.cpp -o cpp_reference
user@computer:$ ./cpp_reference 
3.141593e+00
0000000000000000000000000000000000000000000000000000000000000000000000000000003.141593
Print: 7
user@computer:$ g++ -std=c++23 -O0 -Wall -Werror  cpp_reference.cpp -o cpp_reference
user@computer:$ ./cpp_reference 
3.141593e+00
0000000000000000000000000000000000000000000000000000000000000000000000000000003.141593
Print: 7
user@computer:$ 
LamerOk ★★★★★
()
Ответ на: комментарий от LamerOk

а не.. это я обчитался - почему-то показалось, что f1() возвращает ссылку на внутренний обьект. а она возвращает обьект по значению.

в приведенном коде разумеется проблем нет вообще.

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

Возвращается копия, которая рассматривается как rvalue, а конструктор для rvalue явно запрещен, так что будет ошибка компиляции.

#include <iostream>

struct Data {
    const int & _i;

    explicit Data(int && i) = delete;
    explicit Data(const int & i) : _i{i} {}
};

int main() {
    Data d{std::min({42, 52})};
    std::cout << d._i << std::endl;
}

https://wandbox.org/permlink/CyaBB1uSro8OCohv

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

Не панацея, увы.

Здесь, наверное, больше претензий должно быть к std::min() и std::max(): кто заморачивается давно уже написали свои возвращающие по значению если хотя бы один из аргументов rvalue. А подход с подавлением конструктора принимающим rvalue, имхо - идиоматически правилен. Ну, или заворачивать в std::reference_wrapper<> если аргументов больше одного.

bugfixer ★★★★★
()

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

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

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

В этом контексте совершенно не принципиально откуда rvalue берётся: это запросто может быть результат какого нибудь вызова.

bugfixer ★★★★★
()

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

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

goingUp ★★★★★
()

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

Вот я недавно такое говно обнаружил. Внезапно! И это после многих лет использований крестов.

Вот возьмем класс

class Foo
{
public:
  Foo(const std::wstring& name, int x);

  Foo(const Foo&) = delete;
 
  Foo(Foo&&) = default;

  Foo& operator=(const Foo&) = delete;

  Foo& operator=(Foo&&) = default;

  ~Foo();

  void Start();

private:
  std::thread _thr;
  std::unique_ptr<int[]> _data;

  void ThreadFunc();
};

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

Далее мы складываем это в vector и запускаем.

Вот при таком раскладе - у нас нихрена ничего не работает! Ну, в частности, падает, и так далее.

std::vector<Foo> v;

for(int i = 0; i<n; ++i)
{
  v.emplace_back(names[i], i);
  v[i].Start();
}

А вот так - работает.

std::vector<Foo> v;

for(int i = 0; i<n; ++i)
{
  v.emplace_back(names[i], i);
}

for(int i = 0; i<n; ++i)
{
  v[i].Start();
}

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

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

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

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

C++ примерно полностью состоит из того, как делать нельзя, вообще говоря

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

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

Тред естественно, при запуске, берет this.

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

мораль. резервируй сразу место под n элементов в векторе, или не бери this, пока не сформировал вектор. или не используй вектор.

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

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

Многопоток здесь вообще не при чем, такие же грабли сей персонаж мог бы отгрести и без многопотока:

struct A {};
struct B {};
struct C {
  A _a;
  B _b;
  ...
};

std::vector<C> c;
std::vector<const B *> b;

for(std::size_t i = 0; i != n; ++i) {
  c.emplace_back(...);
  b.push_back(&(c.back()._b));
}

const B * first_b = b.front(); // Oops.

Здесь можно увидеть типичную проблему программистов, воспитанных на языках с GC и ссылочных типах – они привыкли к тому, что при создании экземпляра ссылочного типа его this остается валидным вне зависимости о того, куда и как ссылку на объект помещают. Даже при работе compacting GC.

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

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

В других языках такого не возникает.

Ну так почему ты с упорством достойного лучшего применения возвращаещься к C++?

Твой любимый Lisp настолько велик и могуч, что зарабатывать на жизнь приходится разработкой на C++ и C#, так получается?

PS. Кстати, спасибо за то, что еще раз продемонстрировал собственное незнание C++. Будет еще одна возможность макать тебя в известную субстанцию в следующий раз, когда заявишь о знании С++ и о том, что типа «писал на C++ нормальный код».

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

Многопоток здесь вообще не при чем

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

Это же база, он даже её не осилил

kvpfs_2
()

Вот, братела, я написаль. Правда за такие худодожества тебя уволят нахер сразу:


struct Data {
    int lhs[0];
   explicit Data(const int rhs) {
    lhs[rhs];
    lhs[3]=rhs;
   }

   void print() {
      std::cout << lhs[3] << "\n";
   }
};


int main() {
   Data t(42);
   t.print(); // развал жопы на куски
   return 0;
}
Ygor ★★★★★
()
Ответ на: комментарий от kvpfs_2

Он же не смог синхронизацию, где она в его коде?

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

Собственно, причину я показал на более простом примере, без тредов. Указатели на объекты B сохраняемые в векторе b могут протухать по мере добавления новых объектов в вектор c.

Более того, чтобы мир заиграл совсем уж яркими красками, а жопы у таких как lovesan подорвались еще больше, то можно вспомнить про ABA проблему (хотя она описана для многопотока, но не суть).

Допустим, мы добавили в c 4 элемента и они легли по адресам (условно) 0x0f0000, 0x0f0004, 0x0f0008, 0x0f000c.

Затем добавляем еще один элемент и идет реаллокация вектора c. Соответственно, адреса, сохраненные в b «протухнут».

Затем мы еще что-то добавляем в вектор c и вектор реаллоцируется еще раз. При некотором стечении обстоятельств (и некотором специфическом поведении дефолтного аллокатора) новый вектор может быть выделен по старым адресам, т.к. первые четыре элемента вектора опять лягут по адресам 0x0f0000, 0x0f0004, 0x0f0008, 0x0f000c.

И тогда окажется, что в b часть адресов валидные, часть адресов – невалидные.

Можно представить себе вопли lovesan-а: Как так? Какое говно! Вселенное опасносте!

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

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

многопоток тут только дело запутал, но причина не в нем.

будь у него список, проблем бы не было.

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

кстати про руст. там ж вроде тоже есть динамический массив.

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

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

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

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

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

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

some_type& ref = & my_dyn_array[10].some_field;

на знаю какой там значок взятия ссылки и есть ли он вообще.

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

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

также например если вызвать метод X для обьекта, лежащего в векторе, и в этом методе взять this например, и запомнить его… то если просто накидывать новые обьекты в вектор, - ссылка повиснет. тут нет итератора, просто берется this в методе.

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

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

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

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

Откуда инфа, что он брал адреса? Этого в «дано» вообще не было, он передал индекс, вот такое будет вполне рабочим:

struct S {
   jthread t;
   int i = 0;
   S(void(*f)(int), int idx) : t(f, idx) {}
   static void tfn(int i);
};
vector<S> v;
mutex mtx;

void S::tfn(int i) {
   while (true) {
      this_thread::sleep_for(1s);
      lock_guard l(mtx);
      ++ v[i].i;
   }
}

int main() {
   int cnt = 0;
   auto add = [&]() {
      lock_guard l(mtx);
      v.emplace_back(S::tfn, cnt++);
      v.emplace_back(S::tfn, cnt++);
   };

   while (true) {
      this_thread::sleep_for(1s);
      add();
   }
}

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

Может и брал адреса, неясно, но может наговнокодил с синхронизцацией. А его второй пример, где запуск в отдельном цикле - там проблемы уже нет, тк запуск потоков в главном «synchronized-with» началом выполнения в новых, и, естественно, санитайзеры ошибку уже находить не будут

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

Откуда инфа, что он брал адреса? Этого в «дано» вообще не было

Посмотрите на прототип его метода ThreadFunc. Это нестатический метод, чтобы этот нестатический метод заработал на отдельной нити на эту самую нить нужно пробросить this.

он передал индекс

Какой индекс он куда передал? Кто вам сказал, что это именно индекс в исходном массиве, а не просто абстрактный идентификатор таска?

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

ему пришлось брать адрес, чтобы запустить тред. поскольку функции тела треда std::thread надо передать данные из обьекта воркер. он сам об этом пишет. да это и так ясно, тому кто этим занимался.

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

Какой индекс он куда передал? Кто вам сказал, что это именно индекс в исходном массиве, а не просто абстрактный идентификатор таска?

А кто сказал, что не индекс? А его пример - условный псевдокод с опущенными подробностями? Ок, это вряд ли индекс, ведь он сам писал: «Тред естественно, при запуске, берет this». Но всё это следствие многопотока, не было бы в однопотоке в функции-члене зависшего указателя на себя, в этом нет никакого смысла, может при других условиях можно так наговнокодить, но не здесь. Я и сказал про многопоток, вы потянули меня в спор в детали, и я зацепился не за самую удачную версию, согласен (хотя не невероятную). Но причина здесь - многопоток. Товарищ крупными мазками вкинул дерьма, я абстрактно назвал его маргаринновым и копаться в деталях этого говнокода не собирался.

kvpfs_2
()