Bind const & of временного: нет предупреждения компилятора?

У меня есть TestClass с переменной-членом const&. Я знаю из разных мест и из собственного опыта, что это плохая идея инициализировать этот const& со ссылкой на временное значение. Поэтому я был очень удивлен, что следующий код будет хорошо скомпилирован (протестирован с gcc-4.9.1, clang-3.5 и scan-build-3.5), но не сможет работать должным образом.

class TestClass {
  public:
    // removing the "reference" would remove the temporary-problem
    const std::string &d;

    TestClass(const std::string &d)
        : d(d) {
        // "d" is a const-ref, cannot be changed at all... if it is assigned some
        // temporary value it is mangled up...
    }
};

int main() {

    // NOTE: the variable "d" is a
    // temporary, whose reference is not valid... what I don't get in the
    // moment: why does no compiler warn me?
    TestClass dut("d");

    // and printing what we got:
    std::cout << "beginning output:\n\n";
    // this will silently abort the program (gcc-4.9.1) or be empty
    // (clang-3.5) -- don't know whats going on here...
    std::cout << "dut.d: '" << dut.d << "'\n";
    std::cout << "\nthats it!\n";

    return 0;
}

Почему ни один из двух компиляторов не предупреждает меня во время компиляции? См. также этот ideone, и еще несколько тестирований.

Ответ 1

Без предупреждения, так как без обид:

локальные ссылки const продлевают срок службы переменной.

Стандарт определяет такое поведение в §8.5.3/5, [dcl.init.ref], разделе об инициализаторах ссылочных объявлений. Увеличение продолжительности жизни не транзитивно через аргумент функции. §12.2/5 [class.tevent]:

Второй контекст, когда ссылка связана с временным. Временное, к которому привязана ссылка, или временное, которое является полный объект к подобъекту, с которым связан временный объект сохраняется в течение всего срока действия ссылки, кроме случаев, указанных ниже. Временная привязка к ссылочному элементу в конструкторах ctor-initializer (§12.6.2 [class.base.init]) сохраняется до конструктор выходит. Временная граница с опорным параметром в вызов функции (§5.2.2 [expr.call]) сохраняется до завершения полное выражение, содержащее вызов.

Вы можете взглянуть на gotw-88 для расширенного и более удобного обсуждения этой темы.

Неопределенное поведение

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

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

Ответ 2

Привязка const & к временному действительна, и компилятор будет гарантировать, что временный объект будет жить по крайней мере столько же, сколько и ссылка. Это позволяет вам делать такие вещи, как передавать строковые литералы в функции, ожидающие const std::string &.

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

Ответ 3

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

Если вы рассматриваете только конструктор:

class TestClass {
  public:
    const std::string &d;

    TestClass(const std::string &d)
        : d(d)
    {}
};

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

class Widget {
  std::string data;
  TestClass test;

public:
  Widget() : data("widget"), test(data)
  {}
};

Если вы рассматриваете только сайт вызова:

//Declaration visible is:
TestClass(const std::string &d);

int main() {
    TestClass dut("d");
}

Здесь компилятор не "видит" (в общем случае) определение конструктора. Представьте себе альтернативу:

struct Gadget {
  std::string d;

  Gadget(cosnt std::string &d) : d(d) {}
};

int main()
{
  Gadget g("d");
}

Конечно, вы не хотели бы здесь предупреждение.

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

Ответ 4

TestClass(const std::string &d1)
    : d(d1) {

TestClass dut("d");

Я думаю, что логически происходит следующее: -

1) Ваш строковый литерал ("d") будет неявно преобразован в std::string (давайте дадим ему имя 'x').

2) Итак, 'x' является временным, который связан с d1 здесь. Срок действия этого временного промежутка продлевается до срока службы вашего d1. Хотя этот строковый литерал всегда будет жив до конца программы.

3) Теперь вы делаете 'd' refer to 'd1'.

4) В конце вашего конструктора d1 время жизни истекло, как и d.

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