临时变量的const引用绑定:为什么没有编译器警告?

11
我有一个名为TestClass的类,其中包含一个const&类型的成员变量。从各种渠道和自己的经验中,我知道使用临时值初始化此const&是不明智的。因此,当我发现以下代码可以编译通过(在gcc-4.9.1clang-3.5scan-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,其中有更多的测试。

只要引用变量在作用域内并且有效,常量引用就是有效的。 - Some programmer dude
1
关于术语的说明,"d"不是一个变量,它是一个字符串字面值。然而,将其传递给期望std::string的函数会创建一个临时的std::string对象。 - Some programmer dude
1
我对此并不是专家,但是如果需要,将const&绑定到临时对象是否会延长该临时对象的生命周期呢? - Bathsheba
@Bathsheba 临时对象不是持续到构造函数结束,当引用成员变量被使用时会导致未定义行为吗? - BЈовић
1
@Bathsheba 不。在大多数情况下(但不包括构造函数的初始化列表),使用临时变量初始化引用会延长临时变量的生命周期,但如果引用是由另一个引用初始化的,则不会影响任何临时变量的生命周期。 - James Kanze
显示剩余2条评论
4个回答

9

没有警告,因为没有冒犯:

本地的const引用会延长变量的生命周期。

标准在§8.5.3/5 [dcl.init.ref]中指定了这种行为,即参考声明的初始化器部分。 生命周期延长不能通过函数参数进行传递。 §12.2/5 [class.temporary]:

第二种情况是当引用被绑定到临时对象时。 引用绑定的临时对象或完整对象的子对象的临时对象,除非以下特殊规定, 否则会持续到引用的生命周期结束。 在构造函数的构造函数初始化器(§12.6.2 [class.base.init])中绑定到引用成员的临时对象将持续 直到构造函数退出。 在函数调用(§5.2.2 [expr.call])中绑定到引用参数的临时对象将持续到包含该调用的完整表达式完成。

您可以查看gotw-88以获取有关此主题的扩展和更易读的讨论。

未定义的行为

你的代码正确吗?不,它的执行将导致未定义行为。你的代码快照中真正的问题是由两个完全“合法”的操作混合引起的未定义行为:在构造函数中传递临时对象(其生命周期在构造函数块内)并在构造函数定义中绑定引用。
编译器不足够“聪明”以检测到这种爆炸性语句组合,所以你没有收到任何警告。

编译器无法智能地检测到这种爆炸性语句组合。我花了一些时间编写基于clang的工具,并且我想知道scan-build是否可以意识到这一点。但目前还没有。 - user3520187
引用不会延长生命周期;使用临时对象初始化引用时,不仅适用于局部变量。你引用的从句限定了异常,但这不是问题所在,因为他没有使用临时对象初始化类成员。 - James Kanze
@JamesKanze 我只是想指出,将临时对象传递给构造函数的操作是完全合法的... - Alex Gidan
在构造函数的ctor-initializer(§12.6.2 [class.base.init])中对引用成员进行临时绑定,持续到构造函数退出。哇,我想不出任何一种情况需要这种标准规定的行为。以这种方式初始化的引用成员如果被后续方法调用的任何方式访问,就保证会导致UB。这种情况可以在编译时轻松检测到,那么为什么编译器不会报错而生成几乎肯定有缺陷的代码呢? - j_random_hacker
2
@j_random_hacker 为什么这不是一个错误?好问题:我想不出你可能想要这样做的任何理由。但正交性和给程序员足够的自由度让他自己犯错的原则可能是其基础。 - James Kanze
显示剩余3条评论

3

将一个const &与临时变量绑定是有效的,编译器会确保临时变量的生存期至少与引用一样长。这使得您可以将字符串字面值传递到期望const std::string &的函数中。

然而,在您的情况下,您正在复制该引用,因此生命周期保证不再存在。您构造函数退出并销毁临时变量,留下对无效内存的引用。


1
关于第二个句子:将字面值传递给一个接受 std::string const& 的函数不需要扩展生命周期;临时对象的生命周期(有几个例外)是在完整表达式结束之前。在此时,您将已经从函数返回。 - James Kanze

2
问题在于没有一个单一的点需要警告。只有构造函数的调用和实现的组合才会导致未定义行为。
如果只考虑构造函数:
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");
}

当然,您也不希望在这里看到警告。

总之,调用站点和构造函数实现都可以按原样使用。只有它们的组合会导致问题,但是这种组合超出编译器合理使用以发出警告的上下文范围。


后面使用成员函数,这样做是有效的吗? - BЈовић
@BЈовић 您指的是哪个成员函数?您能澄清一下吗? - Angew is no longer proud of SO
std::cout << "dut.d: '" << dut.d << "'\n"; 这里在 main() 中使用了引用。据我所知,这是未定义行为,只是不确定在这种情况下是否是(我仍然认为它是未定义行为)。 - BЈовић
@BЈовић 我的答案提到了UB,我的印象是OP也知道。我理解这个问题是“为什么对于这个无效的程序没有警告?”,而不是“这个有效吗?” - Angew is no longer proud of SO

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

TestClass dut("d");

我猜逻辑上发生了以下事情:
1)您的字符串文字(“d”)将隐式转换为std :: string(让我们称其为“x”)。
2)因此,“x”是一个临时变量,绑定到此处的“d1”。该临时变量的生命周期延长到“d1”的生命周期。尽管该字符串文字始终会在程序结束之前保持活动状态。
3)现在,您正在使“d”引用“d1”。
4)在构造函数结束时,“d1”的生命周期已经结束,所以“d”的生命周期也已经结束。
并非所有编译器都能聪明地找出这些小问题...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接