Как обнаружить постоянную ссылку на временные проблемы во время компиляции или во время выполнения? - PullRequest
8 голосов
/ 01 декабря 2010

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

#include <iostream>

class Z
{
 public:
 Z(int n) : n(n) {}
 int n;
};

class Y
{
 public:
 Y(const Z& z) : z(z) {}
 const Z& z;
};

class X
{
 public:
 X(const Y& y) : y(y) {}
 Y y;
};

class Big
{
 public:
 Big()
 {
   for (int i = 0; i < 1000; ++i) { a[i] = i + 1000; }
 }
 int a[1000];
};

X get_x() { return X(Y(Z(123))); }

int main()
{
 X x = get_x();
 Big b;
 std::cout << x.y.z.n << std::endl;
}

ВЫХОД: 1000

Я ожидаю, что эта программавывод 123 (значение xyzn установлено в get_x ()), но создание «Big b» перезаписывает временный Z. В результате ссылка на временный Z в объекте Y теперь перезаписывается с помощью Big b, и, следовательно,вывод не то, что я ожидал.

Когда я скомпилировал эту программу с помощью gcc 4.5 с опцией "-Wall", он не выдал предупреждение.

Исправление, очевидно, удаляет ссылку изчлен Z в классе Y. Однако часто класс Y является частью библиотеки, которую я не разрабатывал (boost :: fusion совсем недавно), и, кроме того, ситуация намного сложнее, чем в этом примере, который я привел.

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

Спасибо,

Клинтон

Ответы [ 3 ]

2 голосов
/ 01 декабря 2010

Я отправил такие случаи в список рассылки clang-dev несколько месяцев назад, но никто не имел времени поработать над ним тогда (и я, к сожалению, тоже).

В настоящее время Аргириос Киртзидисработая над этим, и вот его последнее обновление по этому вопросу (30 ноября 23h04 по Гринвичу):

Я отменил предыдущий коммит, гораздо лучше исправить в http://lists.cs.uiuc.edu/pipermail/cfe-commits/Week-of-Mon-20101129/036875.html., например, для

struct S {   int x; };

int &get_ref() {   S s;   S &s2 = s;   int &x2 = s2.x;   return x2; }

мы получаем

t3.cpp:9:10: warning: reference to stack memory associated with local variable 's' returned
  return x2;
         ^~
t3.cpp:8:8: note: binding reference variable 'x2' here
  int &x2 = s2.x;
       ^    ~~
t3.cpp:7:6: note: binding reference variable 's2' here
  S &s2 = s;
     ^    ~
1 warning generated.

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

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

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

0 голосов
/ 01 декабря 2010

Проблема здесь в коде

 Y(const Z& z) : z(z) {}

в качестве члена 'z' инициализируется со ссылкой на формальный параметр 'z'. Как только конструктор возвращает ссылку, ссылка на объект, который больше не действителен.

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

Кстати, лучше назвать члена 'Y :: z' как 'Y :: mz', если возможно. Выражение 'z (z)' не очень привлекательно

0 голосов
/ 01 декабря 2010

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

  • Изменить аргументы на неконстантные ссылки.Временное значение не будет соответствовать неконстантному ссылочному типу.

  • Удалять ссылки вообще, если это возможно.Если ваши константные ссылки не указывают логически общее состояние между вызывающим и вызываемым (если бы они это делали, эта проблема не возникала бы очень часто), они, вероятно, были вставлены в попытке избежать наивного копирования сложных типов.Современные компиляторы имеют расширенные возможности оптимизации копирования, которые делают передачу по значению столь же эффективной, как и передачу по ссылке в большинстве случаев;см. http://cpp -next.com / archive / 2009/08 / want-speed-pass-by-value для хорошего объяснения.Копирование ellision явно не будет выполнено, если вы передадите значения внешним функциям библиотеки, которые могут изменить временные значения, но если бы это было так, то вы либо не передаете их как ссылки на const, либо намеренно отбрасываете const-несс в оригинальной версии.Это мое предпочтительное решение, поскольку оно позволяет компилятору беспокоиться об оптимизации копирования и освобождает меня от беспокойства о других источниках ошибок в коде.

  • Если ваш компилятор поддерживает ссылки на rvalue, используйте их.Если вы можете хотя бы отредактировать типы параметров функций, в которых вы беспокоитесь об этой проблеме, вы можете определить метакласс обертки следующим образом:

template class need_ref {

T & ref _;

public:

need_ref (T && x) {/ * nothing * /}

need_ref (T & x): ref_ (x){/ * nothing * /}

operator T & () {return ref_;}

};

и затем замените аргументы типа T & на аргументы типа need_ref.Например, если вы определите следующий

пользователь класса {

int & z;

public:

user (need_ref arg): z(arg) {/ * nothing * /}

};

, тогда вы можете безопасно инициализировать объект типа user с кодом вида "int a = 1, b = 2; userua (a); ", но если вы попытаетесь инициализироваться как" пользовательская сумма (a + b) "или" пользовательская пятерка (5) ", ваш компилятор должен сгенерировать неинициализированную ошибку ссылки в первой версии конструктора need_ref ().Техника явно не ограничивается конструкторами и не накладывает никаких накладных расходов на время выполнения.

...