Обнаружение или избегание мертвых ссылок на временные во время компиляции

Следующая минималистичная программа дает сбой при компиляции с -O3 и, возможно, с -O2, но нормально выполняется с -O0 (с clang 4.0):

#include <iostream>

class A {
public:
  virtual void me() const { std::cerr << "hi!\n"; }
};

class B {
public:
  B(const A& a_) : a(a_) {}
  virtual void me() const { a.me(); }

private:
  const A& a;
};

class C {
public:
  C(const B& b_) : b(b_) {}
  void me() const { b.me(); }

public:
  const B& b;
};

int main() {
  C c = C(A());
  c.me();
}

Причина в том, что c.b инициализируется ссылкой на временный объект класса B, созданный из временного A. После выхода из конструктора c.C() временный B исчезает, но ссылка на него остается в c.b.

Какие хорошие методы я могу использовать, чтобы избежать этой ситуации, учитывая, что я не контролирую реализацию B или A? Существуют ли статические анализаторы, способные обнаруживать это состояние? (Моя версия scan-build не обнаружила проблемы.)

Связано: Обнаружение висячих ссылок на временные файлы


person krlmlr    schedule 15.09.2017    source источник
comment
Ну, теперь, rvalue ссылки. Они привязываются только к временным. Я думаю, что, возможно, это можно абстрагировать как обертку только для lvalue-ref.   -  person Cheers and hth. - Alf    schedule 15.09.2017
comment
Как правило, не объявляйте переменные-члены ссылки на тип. Вместо этого используйте указатель. И если вы получите указатель в конструкторе, результирующий код будет очевиден.   -  person rodrigo    schedule 15.09.2017
comment
@rodrigo: Звучит интересно. Я думаю, это также сработает, если я объявлю аргумент конструктора только указателем, а затем инициализирую ссылку? Я бы не стал переписывать весь код, использующий ссылку...   -  person krlmlr    schedule 15.09.2017
comment
Попробуйте передать -fsanitize=undefined,address компилятору и компоновщику. ASan и UBSan должен уведомлять вас во время выполнения.   -  person nwp    schedule 15.09.2017
comment
Ну да, изменение только конструктора сделает ошибку очевидной: если вы напишете C c = C(&A());, вы заслужили сбой. Мне просто не нравятся классы со ссылками на члены, они затрудняют перемещение и копирование.   -  person rodrigo    schedule 15.09.2017
comment
@rodrigo: Хочешь записать это как ответ?   -  person krlmlr    schedule 15.09.2017
comment
@nwp: только -fsanitize-address-use-after-scope надежно обнаруживает ошибку в моей системе.   -  person krlmlr    schedule 15.09.2017
comment
@krlmlr: я добавил ответ, объясняющий то, что я прокомментировал ранее...   -  person rodrigo    schedule 15.09.2017


Ответы (2)


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

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

class C {
public:
  C(const A *a_) : a(a_) {}
private:
  const A *a;
};

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

class C {
public:
  C(const A *a_) : a(*a_) {}
private:
  const A &a;
};

Чтобы неправильно использовать этот класс, как говорит ОП, вам придется написать что-то вроде:

C c = C(&A());

И тогда ошибка должна быть очевидна: брать указатель на временную — очень плохая идея!

PS: я бы добавил explicit в ваш конструктор, просто чтобы он не участвовал в автоматических преобразованиях, которые, я думаю, являются частью вашей проблемы.

person rodrigo    schedule 15.09.2017
comment
Спасибо. Я не могу изменить класс B или A, если вы это имеете в виду, добавляя explicit. - person krlmlr; 15.09.2017
comment
@krimir Нет, я имею в виду добавление литерала explicit C(const A *a_), чтобы конструктор не вызывался неявно как часть преобразования типов. Могут возникнуть некоторые ошибки компилятора, но вы подавите неожиданные вызовы вашего конструктора. - person rodrigo; 15.09.2017
comment
Лично не нравятся классы с эталонными членами? Классы со ссылочными членами нельзя копировать? Хм? На самом деле они могут иметь конструктор копирования по умолчанию, как и классы с указателями. - person Eric Roller; 14.09.2019

Я бы получил отдельные классы от B и C (возможно, даже используя класс-шаблон).

Эти классы будут содержать нессылочный член, который становится тем, на что ссылаются a и b.

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

(Тогда у меня был бы серьезный разговор с автором B и C).

person Bathsheba    schedule 15.09.2017
comment
Это для существующей кодовой базы. Какой хороший способ найти все варианты использования этого анти-шаблона? - person krlmlr; 15.09.2017