Как обнаружить постоянную ссылку на временные проблемы во время компиляции или во время выполнения?
Недавно я обнаружил, что большинство ошибок в моих программах на 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 теперь перезаписывается с помощью Большой б, и, следовательно, выход не то, что я ожидал.
Когда я скомпилировал эту программу с помощью gcc 4.5 с опцией "-Wall", он не дал предупреждения.
Очевидно, что исправление заключается в удалении ссылки из члена Z в классе Y. Однако часто класс Y является частью библиотеки, которую я не разработал (boost::fusion совсем недавно), и, кроме того, ситуация намного сложнее чем этот пример, который я дал.
Это какая-то опция для gcc или любого дополнительного программного обеспечения, которое позволило бы мне обнаруживать такие проблемы предпочтительно во время компиляции, но даже время выполнения было бы лучше, чем ничего?
Спасибо,
Клинтон
3 ответа
Я отправил такие случаи в список рассылки clang-dev несколько месяцев назад, но никто не имел времени работать над ним тогда (и я, к сожалению, тоже).
Argyrios Kyrtzidis в настоящее время работает над этим, хотя, и вот его последнее обновление по этому вопросу (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 будет признателен за обратную связь (независимо от того, обнаружена она или нет).
[Отредактировано третье обозначение, чтобы продемонстрировать способ, который может помочь.] Это кроличья нора, которую вы прокладываете, когда язык разрешает передавать аргументы по значению или ссылке с тем же синтаксисом вызывающей стороны. У вас есть следующие варианты:
Измените аргументы на неконстантные ссылки. Временное значение не будет соответствовать неконстантному ссылочному типу.
Отбросьте ссылки вообще в тех случаях, когда это возможно. Если ваши константные ссылки не указывают логически общее состояние между вызывающим и вызываемым (если бы они это делали, эта проблема не возникала бы очень часто), они, вероятно, были вставлены в попытке избежать наивного копирования сложных типов. Современные компиляторы имеют расширенные возможности оптимизации копирования, которые делают передачу по значению столь же эффективной, как и передачу по ссылке в большинстве случаев; см. http://cpp-next.com/archive/2009/08/want-speed-pass-by-value для хорошего объяснения. Копирование ellision явно не будет выполнено, если вы передадите значения во внешние библиотечные функции, которые могут изменить временные значения, но если это так, то вы либо не будете передавать их как ссылки на const, либо намеренно отбрасывать const -несс в оригинальной версии. Это мое предпочтительное решение, так как оно позволяет компилятору беспокоиться об оптимизации копирования и освобождает меня от беспокойства о других источниках ошибок в коде.
Если ваш компилятор поддерживает rvalue ссылки, используйте их. Если вы можете хотя бы отредактировать типы параметров функций, в которых вы беспокоитесь об этой проблеме, вы можете определить метакласс-оболочку следующим образом:
шаблон
T & ref_;
общественности:
need_ref (T && x) {/ * Ничего */ }
need_ref (T & x): ref_ (x) {/ * Ничего */ }
оператор T & () {return ref_; }
};
а затем замените аргументы типа T & на аргументы типа need_ref. Например, если вы определите следующее
класс user {
int & z;
общественности:
пользователь (need_ref
};
тогда вы можете безопасно инициализировать объект типа user с помощью кода вида "int a = 1, b = 2; user ua(a);", но если вы попытаетесь инициализировать как "user sum(a+b)" или "Пользователь 5 (5)" ваш компилятор должен генерировать неинициализированную ссылку ссылки в первой версии конструктора need_ref(). Техника, очевидно, не ограничивается конструкторами и не накладывает никаких затрат времени выполнения.
Проблема здесь в коде
Y(const Z& z) : z(z) {}
поскольку член 'z' инициализируется со ссылкой на формальный параметр 'z'. Как только конструктор возвращает ссылку, ссылается на объект, который больше не действителен.
Я не думаю, что компилятор будет или может во многих случаях обнаруживать такие логические недостатки. Исправление тогда IMO, очевидно, чтобы знать о таких классах и использовать их способом, соответствующим их дизайну. Это действительно должно быть задокументировано поставщиком библиотеки.
Кстати, лучше назвать члена 'Y::z' как 'Y::mz', если это возможно. Выражение "z(z)" не очень привлекательно