C++临时变量的生命周期

下面这段程序:

template<class T>
struct bar {
    T t;
    bar(T t) : t(t) {} //#3
    template<class U>
    bar(bar<U> other) : t(other.t) {} //#4
};
void foo(bar<const double&> b) {
    printf("%lf\n", b.t);
}
int main() {
    int a = 42;
    foo(bar<const double&>(a));//#1
    foo(bar<int>(a));          //#2
    return 0;
}

#1和#2虽然都能通过编译, 但是其中有一句是错的. 当然, 很明显#2看上去比较奇怪, 错的八成就是它, 可是初看上去却说不出它为什么错了.

我们来看一个简单一点的例子:

void foo(const double &arg) {}
int main() {
    int a = 42;
    foo(a);
    return 0;
}

相信这样一段代码没人会说它错. a被隐式转换为一个临时的double, 然后arg引用这个临时的double, 直到foo返回, 临时double销毁了. 我们很自然的认为这个临时的double的生命会延续到foo调用结束, 事实的确如此, 下面是C++标准对于这个过程的描述:

12.2.3 [...] Temporary objects are destroyed as the last step in evaluating the fullexpression (1.9) that (lexically) contains the point where they were created. [...]

1.9.12 A fullexpression is an expression that is not a subexpression of another expression.

这个”fullexpression”显然就是指foo(a);了.

现在再来看看开头的例子.

#1中, 在进入bar<const double&>的构造函数#3之前, int被转换成一个临时的double, 这个double作为参数进入#3, 构造出一个临时的bar, 然后这个bar通过bar的拷贝构造函数(注意这时调用的是默认拷贝构造函数, 而不是#4)把这个临时的double传递给foo的形参. 根据上述标准, 这个临时的double是#1的一部分, 所以其生命周期一直延续到#1结束.

#2中, 我们构造了一个临时对象bar<int>, 我们暂且称其为x, 然后在将其赋给foo的形参b时调用了#4, 此时x.t在构造给b.t的时候产生了一个临时的double, 我们称其为y, 然后b.t引用了y, 进入函数foo…

问题在于y属于#2吗? 如果你把”fullexpression”理解成从取a的地址到foo返回CPU所执行的指令序列, 那么显然y当然是这一序列的一部分. 然而事实并非如此, “fullexpression”仅仅指#2这一行C++代码, 一串符号. 而y属于另一串符号t(other.t), 因为y的是在那里创建的, 于是也应该在那里销毁.

最上面的代码原型是我在使用boost::tuple时遇到的, 为了简化问题, 就写了bar代替boost::tuple. 所以并不是说输入参数都写成const引用就好, 当遇到隐式类型转换的时候就要特别小心. 比如把tuple的模板参数写成某个类型的引用作为形参就不是什么好主意. 其它的比如optional, 内部保存引用也有很大风险. 不过因为optional没有泛型的单参数非explicit构造函数, 所以用它作为形参表示可选参数的时候被误用的概率通常比较小.

这个问题的讨论参见这里.

Related posts

Post a Comment

Your email is never published nor shared. Required fields are marked *