C++常量引用生命周期(容器适配器)

15
我有如下代码:

我有以下代码:

class T {};

class container {
 const T &first, T &second;
 container(const T&first, const T & second);
};

class adapter : T {};

container(adapter(), adapter());

我曾以为常量引用的生命周期将是容器的生命周期。

然而,事实并非如此,适配器对象在容器创建后被销毁,留下了悬空引用。

正确的生命周期是什么?

适配器临时对象的堆栈范围是容器对象还是容器构造函数?

如何正确地将临时对象绑定到类成员引用?

谢谢

5个回答

17
根据C++03标准,与引用绑定的临时对象在不同的上下文中具有不同的生命周期。在您的示例中,我认为以下突出部分适用(12.2/5“临时对象”):
引用绑定的临时对象或者是完全对象的子对象,到这个临时对象被绑定的子对象持久存在于引用的生命周期中,除非以下情况另有规定:在构造函数的ctor-initializer(12.6.2)中引用成员绑定的临时对象将一直持久到构造函数退出。在函数调用(5.2.2)中引用参数绑定的临时对象将持续到包含该调用的完整表达式完成。
因此,虽然绑定临时对象是一种扩展临时对象寿命的高级技术(GotW#88:最重要的const候选人),但显然在这种情况下它不能帮助您。
另一方面,Eric Niebler有一篇文章,您可能会对其中讨论的有趣(如果有些复杂)技术感兴趣,该技术可以让您的类构造函数推断是否已传递了临时对象(实际上是rvalue)(因此必须复制),还是已传递了非临时对象(lvalue)(因此可能可以安全地将引用存储在其中而不是复制): Conditional Love:FOREACH Redux 祝你好运 - 每次我阅读这篇文章,都需要把所有内容看作从未见过的一样去理解。它只会在我脑海里停留短暂的时间...
我还应该提到,C++0x的右值引用应该使Niebler的技术变得不必要。右值引用将被MSVC 2010支持,在一周左右即将发布(如果我没记错的话是在2010年4月12日)。我不知道GCC中右值引用的状态。

我认为在这种情况下,临时对象与函数调用参数(构造函数调用)绑定,就像下一句话中所说的那样。是的,它也会因为构造函数初始化列表中的别名而与成员绑定,而且它会一直存在,直到构造函数退出(实际上更长,如果包含构造函数调用的完整表达式也做其他事情的话)。但我认为突出显示的段落指的是像struct container { const &adapter a; container() : a(adapter()) {} };这样的东西。 - Steve Jessop
@Steve:仔细一看,我认为你是对的 - 我会更新答案(结果相同)。 - Michael Burr

6
临时const引用只有当前语句的生命周期(也就是说,在分号之前它们会超出范围)。因此,经验法则是不要依赖于作为参数接收它的函数的生命周期之外仍存在的const引用,这种情况下只是构造函数。所以一旦构造函数完成,不要依赖于任何const引用仍然存在。
临时变量的生命周期无法更改/覆盖/扩展。如果您想要更长的生命周期,请使用实际对象而不是临时对象:
adapter a, b; 
container(a, b); // lifetime is the lifetime of a and b

最好的做法是,除非对象非常密切相关且绝对不是暂时性的,在极其紧急的情况下,不要使用对类成员的常量引用。


2
更准确地说,它们生存的时间与创建它们的完整表达式的结束时间相同。 - GManNickG
1
“没有办法更改/覆盖/扩展临时对象的生命期” - 其实是有的,只是在这种情况下不是很有用。如果您使用临时对象来初始化具有自动持续时间的常量引用,那么临时对象的生命周期将延长到自动持续时间所在的范围退出为止。 - Steve Jessop

1

引用将在container的整个生命周期内存在,但是所引用的对象只会在该对象的生命周期内存在。在这种情况下,您已将引用绑定到具有自动存储分配(“堆栈分配”,尽管这不是C++术语)的临时对象上。因此,您不能指望临时对象存在于编写它的语句之外(因为在调用container的构造函数之后立即超出作用域)。处理此问题的最佳方法是使用复制而不是引用。由于您无论如何都使用了const引用,因此它将具有类似的语义。

您应该将类重新定义为:

template<typename T> 
class container 
{
    public:
        container(const T& first, const T& second) : first(first), second(second) {}
    private:
        const T first;
        const T second;
};

或者,您可以给对象命名以防止它们超出作用域:

   adaptor first;
   adaptor second;
   container c(first,second);

但是,我认为这不是一个好主意,因为诸如return c之类的语句是无效的。

编辑
如果您的目标是共享对象以避免复制成本,那么您应该考虑使用智能指针对象。例如,我们可以使用智能指针重新定义您的对象如下:

template<typename T> 
class container 
{
    public:
        container(const boost::shared_ptr<const T>& first, const boost::shared_ptr<const T>& second) : first(first), second(second) {}
    private:
        boost::shared_ptr<const T> first;
        boost::shared_ptr<const T> second;
};

然后您可以使用以下代码:

boost::shared_ptr<const adaptor> first(new adaptor);
boost::shared_ptr<const adaptor> second(new adaptor);
container<adaptor> c(first,second);

或者,如果您想在本地拥有first和second的可变副本:

boost::shared_ptr<adaptor> first(new adaptor);
boost::shared_ptr<adaptor> second(new adaptor);
container<adaptor> c(boost::const_pointer_cast<const adaptor>(first),boost::const_pointer_cast<const adaptor>(second));

真实的对象带有副作用构造函数,相当沉重。 我正在尝试避免复制构造。 - Anycorn
2
@aaa,如果是这样的话,你应该使用智能指针,例如boost::shared_ptr。 - Michael Aaron Safyan
我曾考虑过这样做,但是类在公共接口中,我试图保持boost的自由。 - Anycorn

0

如果您想避免复制,那么我想容器必须自行创建存储的实例。

如果您想调用默认构造函数,那么应该没有问题。只需调用Container的默认构造函数即可。

如果您想调用所包含类型的非默认构造函数,那么可能会更棘手。C ++ 0x将有更好的解决方案。

作为一种练习,容器可以接受T或包含T构造函数参数的对象。这仍然依赖于RVO(返回值优化)。

template <class T1>
class construct_with_1
{
    T1 _1;
public:
    construct_with_1(const T1& t1): _1(t1) {}
    template <class U>
    U construct() const { return U(_1); }
};

template <class T1, class T2>
class construct_with_2
{
    T1 _1;
    T2 _2;
public:
    construct_with_2(const T1& t1, const T2& t2): _1(t1), _2(t2) {}
    template <class U>
    U construct() const { return U(_1, _2); }
};

//etc for other arities

template <class T1>
construct_with_1<T1> construct_with(const T1& t1)
{
    return construct_with_1<T1>(t1);
}

template <class T1, class T2>
construct_with_2<T1, T2> construct_with(const T1& t1, const T2& t2)
{
    return construct_with_2<T1, T2>(t1, t2);
}

//etc
template <class T>
T construct(const T& source) { return source; }

template <class T, class T1>
T construct(const construct_with_1<T1>& args)
{
    return args.template construct<T>();
}

template <class T, class T1, class T2>
T construct(const construct_with_2<T1, T2>& args)
{
    return args.template construct<T>();
}

template <class T>
class Container
{
public:
    T first, second;

    template <class T1, class T2>
    Container(const T1& a = T1(), const T2& b = T2()) : 
        first(construct<T>(a)), second(construct<T>(b)) {}
}; 

#include <iostream>

class Test
{
    int n;
    double d;
public:
    Test(int a, double b = 0.0): n(a), d(b) { std::cout << "Test(" << a << ", " << b << ")\n"; }
    Test(const Test& x): n(x.n), d(x.d) { std::cout << "Test(const Test&)\n"; }
    void foo() const { std::cout << "Test.foo(" << n << ", " << d << ")\n"; }
};

int main()
{
    Test test(4, 3.14);
    Container<Test> a(construct_with(1), test); //first constructed internally, second copied
    a.first.foo();
    a.second.foo();
}

-1

不要这样做。临时变量在创建它的表达式结束后立即被销毁(除非它立即绑定到引用,此时它是引用的作用域)。生命周期不能延长到类的生命周期。

这就是为什么我从不将成员存储为引用 - 只复制对象或指针。对我来说,指针使得生命周期变得明显。特别是在构造函数的情况下,你的构造函数参数必须比类本身的寿命更长,这一点并不明显。


1
-1:尽可能使用引用替换指针。 - Billy ONeal
我没有使用-1,但它们会一直存在于它们被创建的完整表达式结束之前,而不是作用域。 - GManNickG
@Stephen:是的。这种行为对于其他所有主要的面向对象编程语言(除了Objective C)来说都是默认的行为,因此完全不明显。指针是C语言的遗留物,可以给你表达能力。但是如果你没有使用这种能力,那么指针就需要被引用所取代。 - Billy ONeal
1
@Billy ONeal:并不是真的,许多其他主要的面向对象语言都有可重置、可空的引用。由于C++的引用不可为空或可重置,因此说“嗯,Java使用引用,因此C++代码应该使用引用”并没有太多意义。这些引用并不相同。无论如何,使用指针并不会强制你进行指针算术运算,正是避免了这一点,导致那些其他语言避免使用指针。我注意到Go具有指针,但没有指针算术运算,也没有单独的指针成员访问运算符。 - Steve Jessop
@Billy:没有人说引用是“完全不明显”的。所说的是,通过引用接受构造函数参数并将其存储为成员变量会使调用站点不明显,即引用需要比类更长寿。 - Stephen
显示剩余8条评论

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