问题 绑定const和临时:没有编译器警告?


我有一个 TestClass 用一个 const& 成员变量。我从各个地方和自己的经验中知道,初始化这个是个坏主意 const& 引用临时值。所以我很惊讶下面的代码编译得很好(用 gcc-4.9.1clang-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还有一些测试正在进行中。


2956
2017-12-11 12:51


起源

只要引用变量在范围内且有效,常量引用就是有效的。 - Some programmer dude
关于术语的说明, "d" 不是变量,它是一个字符串文字。但是,将它传递给期望a的函数 std::string  创建 暂时的 std::string 目的。 - Some programmer dude
我不是这方面的专家,但如果需要,不会绑定const并临时延长该临时的生命周期? - Bathsheba
@Bathsheba在构造函数结束之前是不是临时的,在使用引用成员变量时稍后调用UB? - BЈовић
@Bathsheba在初始化带有临时的引用在大多数情况下会延长临时的生命周期(但不会在构造函数的初始化器列表中),但如果使用另一个引用初始化引用,则它对任何临时的生命周期都没有影响。 - James Kanze


答案:


没有任何违法的警告:

本地 const 引用延长了变量的寿命。

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

第二个上下文是引用绑定到临时的。引用绑定的临时值或临时值   完整对象到临时绑定的子对象   除下述规定外,在参考文件的生命周期内仍然存在。   临时绑定到构造函数中的引用成员   ctor-initializer(§12.6.2[class.base.init])一直持续到   构造函数退出。临时绑定到a中的引用参数   函数调用(第5.2.2节[expr.call])一直持续到完成   包含调用的完整表达式。

你可以看看 gotw-88 有关此主题的扩展和更易阅读的讨论。

未定义的行为

那么你的代码是否正确?不,并且它的执行将导致未定义的行为。代码快照中的真正问题是未定义行为是由两者完美混合引起的 法律 operations:构造函数的调用,传递临时对象(其生命跨越构造函数块)以及构造函数定义中引用的绑定。

编译器不是 足够聪明 检测这种爆炸性的声明组合,所以这就是你没有得到任何警告的原因。


9
2017-12-11 12:59



编译器不够智能,无法检测到这种爆炸性的语句组合  - 我花了一些时间编写基于clang的工具,我想知道是否 scan-build 可以意识到这一点。它还没有。 - user3520187
参考文献不延长寿命;使用临时函数初始化引用,而不仅仅是局部变量。你引用的条款界定了异常,但这不是问题,因为他没有用临时的方式初始化类成员。 - James Kanze
@JamesKanze我只想指出将临时文件传递给构造函数的操作完全合法...... - Alex Gidan
@ user3520187这是一个有趣的测试... - Alex Gidan
@j_random_hacker为什么不是错误?好问题:我想不出你为什么要这样做的任何理由。但是正交性以及你应该给程序员所需的所有绳索的原则可能都是他自己在脚下射击的基础。 - James Kanze


绑定一个 const & 临时是有效的,编译器将确保临时将至少与引用一样长。这允许您执行诸如将字符串文字传递到期望a的函数之类的操作 const std::string &

但是,在您的情况下,您正在复制该引用,因此终身保证不再成立。您的构造函数退出并且临时被销毁,并且您将获得对无效内存的引用。


3
2017-12-11 12:56



关于第二句:将文字传递给一个函数 std::string const& 不需要延长寿命;临时的生命周期(有几个例外)直到完整表达式结束。那时你将从函数返回。 - James Kanze


问题在于没有单一点可以发出警告。它只是构造函数的调用和它的实现的组合导致未定义的行为。

如果你只考虑构造函数:

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");
}

当然你也不想在这里发出警告。

总而言之,调用站点和构造函数实现都是完全可用的。它只是导致问题的组合,但这种组合超出了编译器可以合理地用来发出警告的上下文。


2
2017-12-11 12:59



以后使用成员函数 - 它有效吗? - BЈовић
@BЈовић哪个成员函数?你能澄清一下吗? - Angew
std::cout << "dut.d: '" << dut.d << "'\n"; 他正在使用main()中的引用。据我所知,那是UB,在这种情况下不确定(我仍然认为它是ub) - BЈовић
@BЈовић我的回答提到有UB,我的印象是OP知道的那么多。我将这个问题解释为“为什么这个无效程序没有警告?”,而不是“这是否有效?” - Angew


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's 一生都结束了 d's

所有编译器都不是那么聪明地弄清楚这些小问题......


1
2017-12-11 12:59