在基于范围的for循环中的临时对象

14
临时范围上使用基于范围的for循环中,Barry 提到以下内容不受销毁的临时对象的影响,并且我测试了成员v确实在整个for循环中存在(因为析构函数~X没有在整个for循环中被调用)。这是什么原因?
struct X {
    std::vector<int> v;

    ~X()
    {
    }
};

X foo()
{
    return X();
}

for (auto e : foo().v) {
    // ok!
}

为什么有疑问?你能详细说明一下吗?那个问题完全是关于另外一件事的。 - πάντα ῥεῖ
1
你是否熟悉临时生命周期延长?这是一种相当晦涩的形式。 - HolyBlackCat
谢谢,我不太清楚的是这里的__range被赋值给了X.v而不是X,v应该通过auto&& __range = foo().v来保持活,然而X()作为临时对象没有分配给任何参考,因此在执行auto&& __range = foo().v后应该被销毁吗?如果是这样,那么在v被删除后它的成员值,v也应该被删除吗? - Anning Wuwang
2
我很高兴回答了这个问题(即使我的答案是错误的),因为我学到了东西。但我同意下面@user177的评论,我不会像这样编写代码。(无论在什么情况下,不仅仅是基于范围的for循环)。 - Paul Sanders
2个回答

17
这是一种晦涩的临时对象寿命延长形式。通常,你必须直接将临时对象绑定到引用上才能使其起作用(例如for (auto x : foo())),但是根据cppreference的说法,这种效果会通过以下方式传播:
  • 括号( )(分组,不是函数调用)
  • 数组访问[ ](未重载;必须使用数组而不是指针)
  • 成员访问..*
  • 三元运算符? :
  • 逗号运算符,(未重载)
  • 任何不涉及“用户定义转换”的强制类型转换(可能不使用构造函数或转换运算符)
也就是说,如果将a.b绑定到引用上,则a的生命周期会延长。

13
重要的是,这仅适用于直接访问数据成员的情况。如果有一个例如 auto& getV() { return v; } 成员函数,然后在 for (auto e : foo().getV()) 中使用,扩展程序将不适用,循环确实会使用悬空引用,导致未定义行为。因此,依靠类似的操作可能会有一些风险。 - user17732522
1
我们应该为“右值this”重载getter函数,以返回副本或更好的移动操作,例如auto getV()&& { return std::move(v); },根据Nicolai Josuttis的建议,然后它可能会起作用。尽管如此,我同意,依赖这种行为是有风险的。 - Alex Vask
请强调仅涵盖数组访问,而不是一般的索引。 - Deduplicator

1
临时寿命延长是通过将引用变量直接绑定到临时变量实现的,但不仅如此。关于临时寿命延长的详细列表,请参见规范:[class.temporary]HolyBlackCat大神提供的答案非常好,但我觉得需要一些示例。 ⦿ 直接绑定临时变量
// function prototype
std::string foo(); 

// calling foo:
const auto& b = foo(); // lifetime is extended, directly bind to a temporary

// also, similarly:
const std::string& s = "hi"; // lifetime is extended, the same

根据语言规则,在以下情况下也可以实现临时生命周期的延长:
⦿ 括号 ( ) (分组,而非函数调用)
const auto& a = (foo()); // lifetime is extended, grouping with parenths is ok 
const std::string& s = ("hello "s + "world"); // lifetime is extended, the same

对于接下来的案例,让我们添加以下结构体:
struct A {
    std::string str = "hey";
    int arr[3] = {2, 3, 4};
    int* ptr = arr;
    const auto& foo() const {
        return str;
    }
};

⦿ member access ., .*

const auto& b1 = A().str; // lifetime of A() is extended
const auto& b2 = A().arr; // lifetime of A() is extended
const auto& b3 = A().ptr; // lifetime of A() is extended
// BUT -
const auto& b4 = *A().ptr; // lifetime of A() is NOT extended (b4 dangling)

// pointer to member access
const auto& str_ptr = &A::str;
const auto& arr_ptr = &A::arr;
const auto& ptr_ptr = &A::ptr;

const auto& c1 = A().*str_ptr; // lifetime of A() is extended
const auto& c2 = A().*arr_ptr; // lifetime of A() is extended
const auto& c3 = A().*ptr_ptr; // lifetime of A() is extended

// BUT - not for a member function
const auto& foo_ptr = &A::foo;
// below initialization is bounded to a function call result
// not to a member access
const auto& c4 = (A().*foo_ptr)(); // lifetime of A() is NOT extended (c4 dangling)

⦿ 数组访问 [ ](非重载;必须使用数组而不是指针)

const auto& d1 = A().arr[0]; // lifetime of A() is extended
// BUT - not for pointers
// pointer access with []
const auto& d2 = A().ptr[0]; // lifetime of A() is NOT extended (d2 dangling)
// neither for overloaded []
const auto& d3 = A().str[0]; // lifetime of A() is NOT extended (d3 dangling)

⦿ ternary operator ? :

const auto& e1 = true? A() : A(); // lifetime of the 1st A() is extended
const auto& e2 = false? A() : A(); // lifetime of the 2nd A() is extended

⦿ comma operator , (not overloaded)

const auto& f1 = (A(), A()); // lifetime of the 2nd A() is extended

⦿ 任何不涉及“用户自定义转换”的 any cast(可能不使用构造函数或转换运算符)

const auto& g1 = const_cast<const A&&>(A()); // lifetime of A() is extended

const double& g2 = A().arr[0]; // lifetime of A() is NOT extended
                               // but this is a valid ref to a double
                               // converted from an int, as a temporary

对于不会延长生命周期的铸造,让我们添加一个额外的类:

class B {
    const A& a;
public:
    B(const A& a): a(a){}
};

以下转换经过用户定义的转换,因此不会延长A的生命周期:
const auto& g3 = ((B&&)A()); // lifetime of A() is NOT extended (g3 dangling)

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