将临时作为 const T& 传递并绑定到 class 引用的副作用

Side effects of passing temporary as const T& and binding to class reference

假设我有一个结构定义和一些像这样的函数:

struct A{
 const std::string& _s;
 A(const std::string& s):_s(s){}
 //A(const std::string s): _s(s){}
};
 int main(void){
    A a("E");
    std::cout << a._s << '\n';
}

默认情况下这根本不会出错或警告,您可以通过使用 asan 编译让它在运行时抛出错误。根据我的理解,发生的事情是 s 绑定到的临时 "E 在第一个表达式的末尾消失,这意味着 A a("E") 因此 class 中的引用得到后作废。所以在 cout << 点我们正在访问一个悬空引用。

在编译时,如果我们将第一个构造函数替换为第二个构造函数,则会生成一条警告,它会将引用绑定到临时 s,因此现在在 [= 之后访问 s 15=]表达完也是UB.

假设这两种情况都是正确的,有没有一种情况

auto x= A("E")._s

会是UB吗?如果是,什么时候?

c++17 standard §15.2 点 6.9 说

  • temporary object bound to a reference parameter in a function call (8.5.1.2) persists until the completion of the full-expression containing the call

所以你很好。这里没有 UB auto x= A("E")._s

其他问题你自己描述的很好。构建后访问该成员将是UB。

但是,您可以通过将临时对象绑定到本地常量引用来延长临时对象的生命周期,如下所示:

struct A {
   const std::string& _s;
   A(const std::string& s) : _s(s) {}    
};

int main(void) {
   const std::string& e = "E";
   A a(e);
   std::cout << a._s << '\n';
}

这不会是 UB,但我不会这样使用它。

下面是一些简单的测试代码,可以准确打印各种对象的生命周期开始和停止的时间:

struct loud {
    loud(const char*) { std::cout << __PRETTY_FUNCTION__  << '\n'; }
    loud(const loud&) { std::cout << __PRETTY_FUNCTION__  << '\n'; }
    ~loud() { std::cout << __PRETTY_FUNCTION__  << '\n'; }
};

struct A {
    const loud& _s;
    // A(const loud& s) : _s(s) {}
    A(loud s) : _s(s) {}
};

int main() {
    loud x = A("E")._s;
}

输出:

loud::loud(const char *)
loud::loud(const loud &)
loud::~loud()
loud::~loud()

如您所见,使用 "E" 构造的 loud 对象在从中复制之前未被销毁。


在这两种情况下,都会创建一个临时对象并将其绑定到构造函数的参数 const loud& sloud s。所有临时对象都在包含它们的完整表达式的末尾被销毁,因此它们将在整个 A("E")._s 期间都存在,因此可以从中复制。

这也意味着以下内容将不起作用,因为临时对象是在不同的完整表达式中创建的:

// The following is a hard compile time error on clang, but compiles on gcc
struct A {
    const loud& _s;
    A(loud s) : _s(s) {}
    A(const char* s) : _s(s)  /* Temporary `loud` used to initialize `_s` is destroyed here */ {}
};

// But this compiles
struct A {
    const loud& _s;
    A(loud s) : _s(s) {}
    A(const char* s) : A(loud(s))  /* Temporary materialized from `loud(s)` is destroyed here */ {}
};

临时对象在构造函数体的 { 之前被销毁(并且在任何后续数据成员的构造函数之前,如果有的话),所以它已经在 . 之前被销毁A("E")._s.

在这两种情况下,使用相同的 main 函数输出以下内容:

loud::loud(const char *)
loud::~loud()
loud::loud(const loud &)
loud::~loud()

(对象被销毁后使用的地方,如果复制构造函数试图访问对象上的任何数据成员,就像它是一个 std::string