Consider

#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";
}

It's very curious that if we just bind a const reference to a temporary it will be "fine", though with returning reference to temporary warning; while if we try the second one (via delegation) we would highly likely get a segmentation fault error (I tested it on Ubuntu 20.04 LTS with g++ 10.2.0 -std=c++17 as well as some online compilers, e.g. on coliru. But on VS2019 release mode it can run without segfault which may due to its lazy check/protection for optimization) So, ..., what's wrong with the delegation?

3

There are 3 answers

5
Adrian Mole On BEST ANSWER

It's not the delegation, per se, that's the problem, but the function to which you delegate: that function returns a reference to a local object, which will cease to exist by the time the calling routine gets hold of it (or tries to).

You code, taken as is and compiled with clang-cl (Visual Studio 2019) gives the following:

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

0
Persixty On

The temporary object in main() in question is the return value of type const std::pair<>&. But the temprorary object it is a reference to which is destroyed foo() at tend of the return. It's a dangling pointer before you even create the reference p in main.

Chapter and verse is as an exception to extended lifetime (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.

That's an explicit statement the OP code won't work.

So the return of foo() is a dangling reference to an object destroyed at the end of the return statement and the copy made in main() is dangling to.

Intuitively all the local variables and temporaries in foo() are destroyed at the end of foo(). The return value 'survives' but in this case its a reference not the object.

In even shorter this works:

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

The value of the temporary object will be returned and its lifetime can be extended in the calling function (main) by a const reference.

If it works at all it will most likely be because the space allocated to the temporary pair isn't being overwritten before the data is accessed. That's totally non-portable, may change when the code is optimised (a classic gotcha when changing from Debug to Release build) and cannot be at all relied on.

0
Mark Taylor On

Yeah, I realized that I was returning a local temporary which belongs to foo's call stack, and it would be destroyed after the call. And binding a reference to local temporary is just awful. But if I try to use foo to initialize an object pair, i.e. auto p = foo(); or std::pair<const char*, int> p = foo();, it still gets a segmentation fault. It just kind of doesn't make sense to me: const auto& could be considered as a rvalue which is non-modifiable and can be used to assign a lvalue. When executing std::pair<const char*, int> p = foo();, foo() should initialize the object pair p and then could it kill himself. Yet, the fact indicates the opposite. So, on any cases, shouldn't we return a local temporary as a const reference, even using it as a rvalue to assign or initialize an object?

I found some possible usage:

#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";
}

If returning (const) reference is required and meanwhile we must create it within the method, how could we do this?

Btw, this can really happen: TST

Consider a ternary search tree (TST) with key specialized as string, we just store the mapped_type values, and use the paths the represent the keys (strings):

enum class Link : char { LEFT, MID, RIGHT };
struct Node {
        char ch = '\0';      // 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; }
};  

And, say, if we want to overload those iterator operators (the complete code can be found 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*() is a hardass, as according to the standards, the return type should be reference which in this case is desirable to be const std::pair<const std::string, T>&. When we do *it we want to get a normal pair, just as what we do in std::map<const std::string, T>. The problem is we don't actually have the std::string data member, but we do have the mapped type data member which can be obtained via dereferencing the pointer, i.e. *(x->pval), where x is of type node_ptr or Node*.