首页 > 解决方案 > 声明析构函数会影响对象的生命周期?

问题描述

15 年后回到 C++ 并玩弄“发现现代 C++”一书中描述的表达式模板概念,我遇到了一个我无法解释的行为(尽管当时我的 C++ 知识非常基础,所以我希望这个是显而易见的)。

这是最小的例子(我知道它很长,但这是我能做的最好的说明问题):

#include <iostream>
#include <iomanip>

template <typename T>
class container {
private:
    T data;

    template<typename Src>
    void copy_from(Src& that) {
        data = that.get();
    }

public:

    using value_type = T;

    inline T get() const {
        auto p= data;
        return p;
    }

    void set(T v) {
        data = v;
    }

    template<typename Src>
    container& operator=(const Src& that) {
        copy_from(that);
        return *this;
    }

    template<typename Src>
    container(const Src& that){
        copy_from(that);
    }

    container() = default;

    friend std::ostream& operator <<(std::ostream& s, container<T> const & matrix) {
        s << std::endl << std::fixed << std::setprecision(8) << matrix.data <<std::endl;
        return s;
    }
};

template <typename A1, typename A2>
class sum {
    using mytype = sum<A1, A2>;
public:
    sum(const A1 & a1, const A2 & a2):
    a1(a1), a2(a2) {
        std::cout <<"constructing sum ("<<(long)this<<") with a1 = " << (long)&a1 << " and a2 = " <<(long)&a2 << std::endl;
    }

    // (1)
    // ~sum() {}

    using value_type = std::common_type_t <typename A1::value_type, typename A2::value_type>;

    inline value_type get() const {
        std::cout <<"getting elem from sum ("<<(long)this<<") with a1 = " << (long)&a1 << " and a2 = " <<(long)&a2 << std::endl;

        auto x = a1.get();
        auto y = a2.get();
        auto p = x + y;
        return p;
    }

    void print() {
        std::cout <<"I'm a sum ("<<(long)this<<") with a1 = " << (long)&a1 << " and a2 = " <<(long)&a2 << std::endl;
    }

private:
    const A1 &a1;
    const A2 &a2;
};

template <typename A1, typename A2>
sum<A1, A2> inline operator+ (const A1& a1, const A2& a2) {
    return {a1, a2};
}

template <typename A>
class apply {

public:
    using value_type = typename A::value_type;
    using function_type = std::function<value_type(value_type)>;

    apply (const A& a, const function_type & f):
    a(a), f(f) {std::cout <<"constructing apply ("<<(long)this<<") with a = " << (long)&a<< std::endl; }

    inline value_type get() const {
        std::cout <<"address of apply's member obj is " << (long)&a << ", type is " <<typeid(a).name() << std::endl;
        auto p = f(a.get());
        return p;
    }

private:
    const A &a;
    const function_type & f;
};

template<typename T>
class applicator {
public:

    using value_type = T;
    using function_type = std::function<value_type(value_type)>;

    applicator( const function_type & f): f(f) { }

    template<typename A>
    // (2)
    inline apply<A> operator() (A param) {
        std::cout <<"address of () param is " << (long)&param << ", type is " <<typeid(param).name() <<": ";
        param.print();
        apply<A> op { param, f };
        return op;
    }

private:
    const function_type & f;
};

double square(double x) {
    return x*x;
}

int main() {

    std::cout << "--- Creating variable" << std::endl;
    container<double> W;
    std::cout << W;

    std::cout << "--- Setting values in the variable" << std::endl;
    W.set(4);

    std::function<double(double)> my_fun = square;
    applicator sq { my_fun };

    std::cout << "decltype(W) is_trivially_copyable? " << std::is_trivially_copyable_v<decltype(W)> << std::endl;
    std::cout << "decltype(W+W) is_trivially_copyable? " << std::is_trivially_copyable_v<decltype(W+W)> << std::endl;
    std::cout << "decltype(sq(W+W)) is_trivially_copyable? " << std::is_trivially_copyable_v<decltype(sq(W+W))> << std::endl;

    std::cout << "--- Performing function on addition" << std::endl;
    std::cout << std::hex;

    auto r = sq(W+W);

    std::cout << "Created var r with address " <<(long)&r<<", type: " <<typeid(r).name() <<std::endl;

    std::cout << "--- Copying to container and printing out results" << std::endl;
    std::cout << r << std::endl;

    return 0;
}

这是输出:

--- Creating variable

0.00000000
--- Setting values in the variable
decltype(W) is_trivially_copyable? 1
decltype(W+W) is_trivially_copyable? 1
decltype(sq(W+W)) is_trivially_copyable? 1
--- Performing function on addition
constructing sum (7ffeefbff3a0) with a1 = 7ffeefbff508 and a2 = 7ffeefbff508
address of () param is 7ffeefbff388, type is 3sumI9containerIdES1_E: I'm a sum (7ffeefbff388) with a1 = 7ffeefbff508 and a2 = 7ffeefbff508
constructing apply (7ffeefbff398) with a = 7ffeefbff388
Created var r with address 7ffeefbff4e0, type: 5applyI3sumI9containerIdES2_EE
--- Copying to container and printing out results
address of apply's member obj is 7ffeefbff388, type is 3sumI9containerIdES1_E
getting elem from sum (7ffeefbff388) with a1 = 7ffeefbff4c8 and a2 = 7ffeefbff3b0

0.00000000

in 的参数// (2)是按值传递的,所以临时对象在operator()存在时会被销毁,因此类的a成员apply引用垃圾。这对我来说很有意义。但是,如果我们取消注释类中的析构函数定义sum(请参阅参考资料// (1)),那么结果是正确的并且最终sum对象引用了正确container的 s。为什么?

如果我们保留析构函数的注释并更改// (2)为通过引用传递,一切似乎都正常工作。是不是因为 const 引用延长了返回的临时对象的生命周期operator+?如果是这样,为什么apply<A>构造函数不延长param对象的生命周期operator()?apply 类保持对构造函数中传递的对象的引用。

标签: c++

解决方案


in 的参数// (2)是按值传递的,所以临时对象在operator()存在时会被销毁,因此类的a成员apply引用垃圾。这对我来说很有意义。

正确的。在

inline apply<A> operator() (A param)

返回apply<A>的引用param已超出范围,因此您有一个悬空引用并且使用它是未定义的行为。

但是,如果我们取消注释类中的析构函数定义sum(请参阅参考资料// (1)),那么结果是正确的并且最终sum对象引用了正确container的 s。为什么?

再次,未定义的行为。仅仅因为代码不应该工作并不意味着它不能。由于您有未定义的行为,因此甚至可以为您提供“正确”的结果。

如果我们保留析构函数的注释并更改// (2)为通过引用传递,一切似乎都正常工作。是不是因为 const 引用延长了返回的临时对象的生命周期operator+

这是因为您已经绑定了对来自调用站点的对象的引用。这意味着当函数返回调用站点时,它的引用仍然指向您传递给它的有效对象。这意味着您的引用仍然指向一个有效的对象,并且您已经定义了行为。

如果是这样,为什么apply<A>构造函数不延长param对象的生命周期operator()?apply 类保持对构造函数中传递的对象的引用。

Aconst &仅延长函数本地临时的生命周期。

{ // start of some scope
    const int& foo = function_that_returns_temporary();
} // end of some scope

以上是合法的,编译器会将返回值的生命周期延长到作用域的末尾。有

struct Foo
{
    const int& bar
    Foo(const int& ref) : bar(ref)
};

不延长ref所指的寿命。如果在用它创建的对象被销毁ref之前,任何引用超出范围,那么该对象将留下一个悬空引用。Foo


推荐阅读