为什么通过委托返回 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_ptr
或 Node*
.
有问题的 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 在访问数据之前没有被覆盖。
这完全不可移植,可能会在代码优化时发生变化(从调试版本更改为发布版本时的经典陷阱)并且根本不能依赖。
考虑
#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_ptr
或 Node*
.
有问题的 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 在访问数据之前没有被覆盖。 这完全不可移植,可能会在代码优化时发生变化(从调试版本更改为发布版本时的经典陷阱)并且根本不能依赖。