为什么通过委托返回 const 引用会在 C++ 中出现分段错误,而没有委托只是 "fine"

Why returning a const reference via delegation gets a segmentation fault in c++, while without the delegation just be "fine"

考虑

#include <iostream>
#include <utility>
 
const auto& foo() {
    return std::make_pair("hi there", 2020);
}

int main()
{
    //const auto& p = std::make_pair("hi there", 2020); // Okay, just warning, no segfault
    const auto& p = foo(); // Oops, segmentation fault
    std::cout << "The value of pair is:\n"
              << "(" << p.first << ", " << p.second << ")\n";
}

奇怪的是,如果我们只是将一个 const 引用绑定到一个临时对象,它会“很好”,尽管有 returning reference to temporary 警告;而如果我们尝试第二个(通过委托),我们很可能会遇到 segmentation fault 错误(我在 Ubuntu 20.04 LTS with g++ 10.2.0 -std=c++17 以及一些在线编译器上测试过它,例如 coliru。但在 VS2019 发布模式下,它可以 运行 而不会出现段错误,这可能是由于其惰性 check/protection 进行优化)所以,...,委托有什么问题?

问题不在于委托,本身,而是委托给的函数:该函数returns对本地对象的引用,它将在调用例程获取它时不复存在(或 尝试 到)。

您的代码,按原样并使用 clang-cl 编译(Visual Studio 2019)给出以下内容:

warning : returning reference to local temporary object [-Wreturn-stack-address]

是的,我意识到我正在 returning 一个属于 foo 的调用堆栈的本地临时文件,它会在调用后被销毁。绑定对本地临时文件的引用非常糟糕。但是,如果我尝试使用 foo 来初始化对象对,即 auto p = foo();std::pair<const char*, int> p = foo();,它仍然会出现 分段错误 。这对我来说有点没有意义:const auto& 可以被认为是一个 rvalue,它是不可修改的,可以用来分配一个 左值。在执行std::pair<const char*, int> p = foo();的时候,foo()应该初始化对象对p,然后才可以杀死自己。然而,事实恰恰相反。所以,在任何情况下,我们不应该 return 一个本地临时对象作为 const reference,甚至将它用作 rvalue 来分配或初始化一个对象吗?

我发现了一些可能的用法:

#include <iostream>
#include <utility>
#include <memory>

auto mypair = std::pair<const char*, int>("hi there", 2020); // gvalue

const auto& foo() {
    //return std::make_pair("hi there", 2020); // bang! dead!

    //auto ptr = new std::pair<const char*, int>("hi there", 2020);
    //return *ptr; // Okay, return a heap allocated value, but memory leaks. Ouch!
                   // really wish this is a java program ;)

    return mypair; // Okay, return a global value
}

auto bar() {
    return std::make_unique<std::pair<const char*, int>>("hi there", 2020);
}

int main()
{
    //std::pair<const char*, int> p = foo(); // copy-init
    const auto& p = foo();
    std::cout << "The value of pair p is:\n"
        << "(" << p.first << ", " << p.second << ")\n";

    auto p2 = bar();
    std::cout << "The value of pair p2 is:\n"
        << "(" << p2->first << ", " << p2->second << ")\n";
}

如果需要 returning (const) 引用,同时我们必须在方法中创建它,我们该怎么做?

顺便说一句,这真的可能发生:

考虑一个 ternary search tree (TST),其键专门为字符串,我们只存储 mapped_type 值,并使用表示键(字符串)的路径:

enum class Link : char { LEFT, MID, RIGHT };
struct Node {
        char ch = '[=11=]';      // put ch and pos together so that they will take
        Link pos = Link::MID;// only 4 bytes (padding with another 2 bytes)
        T* pval = nullptr;   // here we use a pointer instead of an object entity given that internal
                             // nodes doesn't need to store objects (and thus saving memory)
        Node* parent = nullptr;
        Node* left{}, * mid{}, * right{};

        Node() {}
        Node(char c) : ch(c) {}
        Node(char c, Link pos, Node* parent) : ch(c), pos(pos), parent(parent) {}
        ~Node() { delete pval; }
};  

并且,比如说,如果我们想重载那些迭代器运算符(可以找到完整的代码here):

class Tst_const_iter
{
        using _self = Tst_const_iter;
    public:
        using iterator_category = std::bidirectional_iterator_tag;
        using value_type = std::pair<const std::string, T>;
        using difference_type = ptrdiff_t;
        using pointer = const value_type*;     // g++ needs these aliases though we don't use them :(
        //using reference = const value_type&; // g++ uses `reference` to determine the type of operator*()
        using reference = value_type;          // but we can fool it :)

        Tst_const_iter() : _ptr(nullptr), _ptree(nullptr) {}
        Tst_const_iter(node_ptr ptr, const TST* ptree) : _ptr(ptr), _ptree(ptree) {}
        Tst_const_iter(const Tst_iter& other) : _ptr(other.ptr()), _ptree(other.cont()) {}

        // we cannot return a const reference since it's a temporary
        // so return type cannot be reference (const value_type&)
        value_type operator*() const {
            _assert(_ptr != nullptr, "cannot dereference end() iterator");
            return std::make_pair(get_key(_ptr), *(_ptr->pval));
        }

        // pointer operator->() const = delete;

        _self& operator++() {
            _assert(_ptr != nullptr, "cannot increment end() iterator");
            _ptr = tree_next(_ptr);
            return *this;
        }

        _self operator++(int) {
            _assert(_ptr != nullptr, "cannot increment end() iterator");
            _self tmp{ *this };
            _ptr = tree_next(_ptr);
            return tmp;
        }

        // --begin() returns end() (its pointer becomes nullptr)
        _self& operator--() {
            if (_ptr == nullptr) _ptr = rightmost(_ptree->root);
            else                 _ptr = tree_prev(_ptr);
            return *this;
        }

        // begin()-- returns a copy of begin(), then itself becomes end()
        _self operator--(int) {
            _self tmp{ *this };
            --*this;
            return tmp;
        }

        friend bool operator==(const _self& lhs, const _self& rhs) {
            _assert(lhs._ptree == rhs._ptree, "iterators incompatible");
            return lhs._ptr == rhs._ptr;
        }

        friend bool operator!=(const _self& lhs, const _self& rhs) {
            _assert(lhs._ptree == rhs._ptree, "iterators incompatible");
            return lhs._ptr != rhs._ptr;
        }

        // auxiliary functions
        node_ptr ptr() const noexcept { return _ptr; }
        const TST* cont() const noexcept { return _ptree; } // get container

        std::string key() const {
            _assert(_ptr != nullptr, "cannot get the key of end() iterator");
            return get_key(_ptr);
        }

        const T& val() const {
            _assert(_ptr != nullptr, "cannot get the value of end() iterator");
            return *(_ptr->pval);
        }
    private:
        node_ptr _ptr;
        const TST* _ptree;
};

operator*() 是硬汉,因为根据标准,return 类型应该是 reference,在这种情况下最好是 const std::pair<const std::string, T>&。当我们执行 *it 时,我们希望得到一个正常的 pair,就像我们在 std::map<const std::string, T> 中所做的那样。 问题是我们实际上没有 std::string 数据成员,但我们确实有映射类型数据成员,它可以通过取消引用指针获得,即 *(x->pval),其中 x 是类型 node_ptrNode*.

有问题的 main() 中的临时对象是类型 const std::pair<>& 的 return 值。但是临时对象是一个引用,它在 return 之后被销毁 foo()。在 main.

中创建引用 p 之前,它是一个悬空指针

章节和诗句是延长生命周期的例外(point 6.10 in Sec. 15.2)。

The lifetime of a temporary bound to the returned value in a function return statement (9.6.3) is not
extended; the temporary is destroyed at the end of the full-expression in the return statement.

这是一个明确的声明,OP 代码将不起作用。

所以 foo() 的 return 是对在 return 语句末尾销毁的对象的悬垂引用,而 main() 中制作的副本悬垂到.

直觉上,foo() 中的所有局部变量和临时变量都在 foo() 结束时被销毁。 return 值 'survives' 但在这种情况下它是一个引用而不是对象。

甚至更短的作品:

const auto foo() {
    return std::make_pair("hi there", 2020);
}

临时对象的值将被 returned 并且其生命周期可以在调用函数 (main) 中通过 const 引用来延长。

如果它能正常工作,很可能是因为分配给临时对的 space 在访问数据之前没有被覆盖。 这完全不可移植,可能会在代码优化时发生变化(从调试版本更改为发布版本时的经典陷阱)并且根本不能依赖。