2

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?

Mark Taylor
  • 117
  • 8
  • 1
    basically duplicate of this: https://stackoverflow.com/questions/6441218/can-a-local-variables-memory-be-accessed-outside-its-scope But I suppose you want to know specifically why extending the lifetime does not work inside the function – 463035818_is_not_an_ai Dec 02 '20 at 14:13
  • 2
    [Pro Tip] **Never** return a reference to a function local variable. It leaves you with a reference to an object that no longer exists. – NathanOliver Dec 02 '20 at 14:13
  • you're returning a dangling reference from that function – Yamahari Dec 02 '20 at 14:14
  • 2
    What warning do you get without `foo`? You should not got any, the code is well-formed. – SergeyA Dec 02 '20 at 14:15
  • @SergeyA `In function ‘const auto& foo()’: warning: returning reference to temporary [-Wreturn-local-addr] 5 | return std::make_pair("hi there", 2020);` But I made a mistake that in line 10 `const auto& p = std::make_pair("hi there", 2020);` , it actually doesn't incur any warnings, the warning is from method `foo` . – Mark Taylor Dec 03 '20 at 15:01

3 Answers3

3

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]

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • Is it possible to move the `std::pair` of `std::make_pair` using `std::move` like `return std::move(std::make_pair("hi there", 2020));` – Harry Dec 02 '20 at 14:28
  • 5
    @Harry There is no need for that. The function should be `auto foo() { return std::make_pair("hi there", 2020); }` and this works perfectly. In C++17, it's guaranteed to cause no copy or move, and before C++17 you are guaranteed it will be moved at worst, but most compilers will still elide it like C++17 now mandates – NathanOliver Dec 02 '20 at 14:34
  • 2
    @Harry That would still be returning a reference to a local variable (the destination of that `std::move()` call is local). To resolve the issue, the simplest way is to return by value (i.e. just take the `&` out of the `foo` declaration). - ninja'd by Nathan. – Adrian Mole Dec 02 '20 at 14:35
  • @NathanOliver I knew returning an object will be totally fine, plus the compiler can do RVO. But for some certain reasons, if we must return a (const) reference from that method, what's the good ways we can apply to achieve that? – Mark Taylor Dec 03 '20 at 14:43
  • @MarkTaylor There isn't a good way. You **cannot** return a function local object by reference. You just can't do it, since it is destroyed when the function ends. There is no lifetime extension mechanism that will save you. – NathanOliver Dec 03 '20 at 14:44
1

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*.

Mark Taylor
  • 117
  • 8
0

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.

Persixty
  • 8,165
  • 2
  • 13
  • 35