返回 C++ 引用变量的做法是邪恶的吗?

Is the practice of returning a C++ reference variable evil?

提问人: 提问时间:4/16/2009 最后编辑:8 revs, 5 users 88%Nick Bolton 更新时间:11/1/2022 访问量:373057

问:

我认为这有点主观;我不确定意见是否会一致(我已经看到很多返回引用的代码片段)。

根据我刚才提出的这个问题的评论,关于初始化引用,返回引用可能是邪恶的,因为[据我所知]它更容易错过删除它,这可能导致内存泄漏。

这让我很担心,因为我遵循了例子(除非我在想象事情)并在相当多的地方这样做了......我误会了吗?是邪恶的吗?如果是这样,到底有多邪恶?

我觉得由于我的指针和引用混合在一起,再加上我是 C++ 的新手,并且对何时使用什么完全感到困惑,我的应用程序一定是内存泄漏地狱......

此外,我知道使用智能/共享指针通常被认为是避免内存泄漏的最佳方法。

C 参考手册 C++-FAQ

评论

1赞 John Z. Li 2/10/2018
如果你正在编写类似 getter 的函数/方法,这并不是邪恶的。
1赞 Beyondo 7/29/2022
不,不是。用户总是可以说他是否想要引用(使用&)或复制引用(否&),因此这取决于用户。它绝不是邪恶的;但实际上非常方便,因为它为开发人员提供了更多的控制和自由,并且还节省了内存。

答:

16赞 Mehrdad Afshari #1

这不是邪恶的。就像 C++ 中的许多东西一样,如果使用得当,它很好,但是在使用它时应该注意许多陷阱(例如返回对局部变量的引用)。

用它可以实现一些好东西(比如 map[name] = “hello world”)

评论

1赞 wrongusername 10/20/2011
我只是好奇,有什么好处?map[name] = "hello world"
7赞 Mehrdad Afshari 10/20/2011
@wrongusername 语法很直观。有没有尝试过增加 Java 中存储在 a 中的值的计数?:PHashMap<String,Integer>
0赞 wrongusername 10/21/2011
哈哈,还没有,但看看 HashMap 的例子,它看起来确实很粗糙:D
0赞 flohack 6/11/2014
我遇到的问题是:函数返回对容器中对象的引用,但调用函数代码将其分配给局部变量。然后修改了对象的一些属性。问题:容器中的原始物体保持不变。程序员很容易忽略返回值中的 &,然后你会得到非常意想不到的行为......
67赞 2 revs, 2 users 94%Charlie Martin #2

不。

邪恶的是引用动态分配的对象并丢失原始指针。当你一个对象时,你承担了一个有义务得到保证。newdelete

但是看一下,例如,:必须返回引用,或者operator<<

cout << "foo" << "bar" << "bletch" << endl ;

行不通。

评论

31赞 4/16/2009
我投了反对票,因为这既没有回答这个问题(OP 已经明确表示他知道需要删除),也没有解决返回对 freestore 对象的引用会导致混淆的合理担忧。叹息。
5赞 Charlie Martin 4/16/2009
返回引用对象的做法并不邪恶。呃,没有。他所表达的恐惧是正确的恐惧,正如我在第二段中指出的那样。
0赞 4/16/2009
你实际上没有。但这不值得我花时间。
3赞 Kobor42 5/26/2013
Iraimbilanja@ 关于“不”,我不在乎。但是这篇文章指出了GMan缺少的重要信息。
5赞 anon #3

它不仅不是邪恶的,有时还是必不可少的。例如,如果不使用引用返回值,就不可能实现 std::vector 的 [] 运算符。

评论

0赞 Nick Bolton 4/16/2009
啊,是的,当然;我想这就是我开始使用它的原因;当我第一次实现下标运算符 [] 时,我意识到了引用的使用。我被引导相信这是事实上的。
1赞 Ben Voigt 8/1/2017
奇怪的是,您可以在不使用引用的情况下为容器实现...并且确实如此。(并在此过程中造成真正的混乱)operator[]std::vector<bool>
0赞 Sergey.quixoticaxis.Ivanov 10/13/2017
@BenVoigt嗯,为什么一团糟?对于具有复杂存储的容器来说,返回代理也是一种有效的方案,这些容器不会直接映射到外部类型(如你提到的)。::std::vector<bool>
1赞 Ben Voigt 10/13/2017
@Sergey.quixoticaxis.Ivanov:混乱的是,在模板代码中使用是坏的,如果可能是的话,因为其行为与其他实例化非常不同。它很有用,但它应该被赋予自己的名字,而不是 .std::vector<T>Tboolstd::vector<bool>std::vector
0赞 Sergey.quixoticaxis.Ivanov 10/13/2017
@BenVoight 我同意关于将一个专业化“非常特别”的奇怪决定的观点,但我觉得您最初的评论暗示返回代理通常很奇怪。
492赞 6 revs, 4 users 96%GManNickG #4

通常,返回引用是完全正常的,并且一直在发生。

如果你的意思是:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

那是各种邪恶。堆栈分配将消失,您什么也没引用。这也是邪恶的:i

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

因为现在客户最终必须做奇怪的事情:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

请注意,右值引用仍然只是引用,因此所有邪恶的应用程序都保持不变。

如果要分配超出函数范围的内容,请使用智能指针(或通常为容器):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

现在,客户端存储了一个智能指针:

std::unique_ptr<int> x = getInt();

引用也可用于访问您知道生存期在更高级别上保持开放的内容,例如:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

在这里,我们知道返回引用是可以的,因为任何调用我们的东西都会管理类实例的生存期,因此至少会存在那么长时间。i_i_

当然,只是:

int getInt() {
   return 0;
}

如果生存期应该留给调用方,而你只是在计算值。

摘要:如果对象的生存期在调用后没有结束,则可以返回引用。

评论

30赞 Arelius 6/11/2011
这些都是不好的例子。正确使用的最佳示例是返回对传入对象的引用。ALA 运营商<<
193赞 Jamin Grey 3/7/2013
为了后人,以及任何碰巧碰到这一点的新程序员,指针还不错。指向动态内存的指针也不是坏的。它们在 C++ 中都有其合法的位置。不过,在动态内存管理方面,智能指针绝对应该是您的默认首选,但您的默认智能指针应该是unique_ptr,而不是shared_ptr。
16赞 GManNickG 3/20/2014
编辑审批者:如果您无法保证编辑的正确性,请不要批准编辑。我回滚了不正确的编辑。
9赞 Lightness Races in Orbit 4/10/2015
为了后人,以及任何碰巧遇到这种情况的新程序员,不要写 return new int
8赞 Shoe 4/10/2015
为了后代,以及任何碰巧遇到这种情况的新程序员,只需从函数中返回 T。RVO将照顾好一切。
8赞 Sasha #5

有两种情况:

  • const reference --好主意,有时,特别是对于重对象或代理类,编译器优化

  • non-const reference --坏主意,有时会破坏封装

两者都有相同的问题 - 可能指向被破坏的物体......

我建议在需要返回引用/指针的许多情况下使用智能指针。

此外,请注意以下事项:

有一个正式的规则 - C++ 标准(如果您有兴趣,第 13.3.3.1.4 节)规定临时只能绑定到 const 引用 - 如果您尝试使用非常量引用,编译器必须将其标记为错误。

评论

1赞 4/16/2009
non-const ref 不一定会破坏封装。考虑 vector::operator[]
0赞 4/16/2009
这是一个非常特殊的情况......这就是为什么我有时会说,尽管我真的应该在大多数时候声称:)
0赞 Nick Bolton 4/16/2009
所以,你是说正常的下标算子实现是必然的恶吗?我既不反对也不同意这一点;因为我并不聪明。
0赞 4/16/2009
我不是这么说的,但如果滥用:))) vector::at 应该尽可能使用,它可能是邪恶的......
0赞 4/16/2009
啊?vector::at 还返回一个非常量引用。
11赞 2 revsJohn Dibling #6

“返回引用是邪恶的,因为, 简单地[据我所知]它做到了 更容易错过删除它”

不对。返回引用并不意味着所有权语义。也就是说,仅仅因为你这样做:

Value& v = thing->getTheValue();

...并不意味着您现在拥有 v 所指的内存;

然而,这是可怕的代码:

int& getTheValue()
{
   return *new int;
}

如果你这样做是因为“你不需要该实例上的指针”,那么:1)如果你需要引用,只需取消引用指针,2)你最终将需要指针,因为你必须将new与delete匹配,并且你需要一个指针来调用delete。

59赞 2 revs, 2 users 89%David Thornley #7

您应该返回对现有对象的引用,该对象不会立即消失,并且您不打算进行任何所有权转移。

永远不要返回对局部变量或类似变量的引用,因为它不会被引用。

您可以返回对独立于函数的内容的引用,您不希望调用函数负责删除该内容。典型功能就是这种情况。operator[]

如果你正在创建一些东西,你应该返回一个值或指针(常规或智能)。您可以自由返回一个值,因为它将进入调用函数中的变量或表达式。永远不要返回指向局部变量的指针,因为它会消失。

评论

1赞 j_random_hacker 4/16/2009
很好的答案,但对于“您可以返回临时作为常量引用”。以下代码将编译,但可能会崩溃,因为临时代码在 return 语句的末尾被销毁:“int const& f() { return 42;void main() { int const& r = f(); ++r; }”
0赞 Mark Ransom 4/17/2009
@j_random_hacker:C++对临时引用有一些奇怪的规则,临时生命周期可能会延长。对不起,我不太了解它,不知道它是否涵盖您的情况。
3赞 j_random_hacker 4/17/2009
@Mark:是的,它确实有一些奇怪的规则。临时的生存期只能通过用它初始化一个常量引用(不是类成员)来延长;然后它一直存在,直到 ref 超出范围。可悲的是,返回 const ref 没有被涵盖。但是,按值返回 temp 是安全的。
0赞 David Thornley 4/17/2009
参见 C++ 标准 12.2 第 5 段。另请参阅赫伯·萨特 (Herb Sutter) 在 herbsutter.wordpress.com/2008/01/01/ 的流浪大师本周
4赞 j_random_hacker 4/19/2009
@David:当函数的返回类型为“T const&”时,实际发生的情况是,return 语句根据 6.6.3.2 将 T 类型的 temp 隐式转换为类型“T const&”(合法的转换,但不会延长生命周期),然后调用代码使用函数的结果初始化类型为“T const&”的 ref, 也是“T const&”类型——同样,这是一个合法但非寿命延长的过程。最终结果:寿命没有延长,而且有很多混乱。:(
-2赞 Dan Olson #8

作为左值的函数(也就是返回非常量引用)应该从 C++ 中删除。这非常不直观。斯科特·迈耶斯(Scott Meyers)想要一个具有此行为的min()。

min(a,b) = 0;  // What???

这并不是真正的改进

setmin (a, b, 0);

后者甚至更有意义。

我意识到函数作为左值对于 C++ 样式的流很重要,但值得指出的是,C++ 样式的流很糟糕。我不是唯一一个这么想的人......我记得 Alexandrescu 有一篇关于如何做得更好的文章,我相信 boost 也试图创建一种更好的类型安全 I/O 方法。

评论

2赞 j_random_hacker 4/16/2009
当然这很危险,应该有更好的编译器错误检查,但如果没有它,就无法完成一些有用的构造,例如 std::map 中的 operator[]()。
2赞 Miles Rout 10/29/2014
返回非常量引用实际上非常有用。 例如。你宁愿写还是?后者远胜一筹。vector::operator[]v.setAt(i, x)v[i] = x
1赞 scravy 8/1/2018
@MilesRout我随时都会去。它远胜一筹。v.setAt(i, x)
-3赞 2 revs, 2 users 77%developresource #9

我遇到了一个真正的问题,它确实是邪恶的。从本质上讲,开发人员返回了对矢量中对象的引用。这很糟糕!!

我在 Janurary 中写过的全部细节:http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html

评论

2赞 j_random_hacker 4/16/2009
如果需要修改调用代码中的原始值,则需要返回 ref。事实上,这与将迭代器返回给向量一样危险——如果向量中添加或删除元素,两者都会失效。
1赞 Trent Gm 2/19/2013
该特定问题是由于保留对向量元素的引用,然后以使引用无效的方式修改该向量引起的: “C++ 标准库:教程和参考”第 153 页第 6.2 节 - Josuttis 写道:“插入或删除元素会使引用以下元素的引用、指针和迭代器无效。如果插入导致重新分配,则会使所有引用、迭代器和指针失效”
-16赞 2 revs, 2 users 94%Anatoly #10

关于可怕的代码:

int& getTheValue()
{
   return *new int;
}

因此,确实,内存指针在返回后丢失。但是,如果您像这样使用shared_ptr:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

返回后内存不会丢失,分配后将释放。

评论

13赞 7/23/2013
它丢失是因为共享指针超出范围并释放了整数。
0赞 dgsomerton 4/24/2016
指针不会丢失,引用的地址就是指针。
1赞 MinandLucy #11

return reference 通常用于 C++ 中大型 Object 的运算符重载,因为返回值需要复制操作。(在 perator 重载中,我们通常不使用指针作为返回值)

但是返回引用可能会导致内存分配问题。由于对结果的引用将作为对返回值的引用从函数中传出,因此返回值不能是自动变量。

如果要使用返回引用,则可以使用静态对象的缓冲区。 例如

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

这样,您可以安全地使用返回引用。

但是你总是可以使用指针而不是引用来返回函数中的值。

0赞 Tony #12

我认为使用引用作为函数的返回值比使用指针作为函数的返回值要简单得多。 其次,使用返回值所引用的静态变量始终是安全的。

0赞 2 revs, 2 users 67%Drezir #13

最好的办法是创建对象并将其作为引用/指针参数传递给分配此变量的函数。

在函数中分配对象并将其作为引用或指针返回(但指针更安全)是一个坏主意,因为在功能块结束时释放内存。

46赞 5 revs, 2 users 99%thorhunter #14

我发现答案不令人满意,所以我会加两分钱。

下面我们来分析一下以下案例:

错误用法

int& getInt()
{
    int x = 4;
    return x;
}

这显然是错误的

int& x = getInt(); // will refer to garbage

与静态变量一起使用

int& getInt()
{
   static int x = 4;
   return x;
}

这是正确的,因为静态变量在程序的整个生命周期中都存在。

int& x = getInt(); // valid reference, x = 4

这在实现单例模式时也很常见

class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

};

用法:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"
    

运营商

标准库容器在很大程度上依赖于返回引用的运算符的使用,例如

T & operator*();

可用于以下情况

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

快速访问内部数据

有时 & 可用于快速访问内部数据

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

用法:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

但是,这可能会导致如下陷阱:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

评论

0赞 clickMe 12/22/2016
返回对静态变量的引用可能会导致不良行为,例如,考虑一个乘法运算符,它返回对静态成员的引用,那么以下结果将始终为:trueIf((a*b) == (c*d))
0赞 Xeaz 2/21/2017
Container::data()的实现应改为return m_data;
0赞 Andrew 2/26/2017
这很有帮助,谢谢!@Xeaz这不会导致追加调用出现问题吗?
0赞 thorhunter 2/27/2017
@Andrew 不,这是一个语法恶作剧。例如,如果返回指针类型,则使用引用地址创建并返回指针。
-1赞 Amarbir #15
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

getPtr 函数可以在删除后访问动态内存,甚至可以访问空对象。这可能会导致错误访问异常。相反,在返回之前,应实现 getter 和 setter 并验证大小。

2赞 AMA #16

除了已接受的答案之外:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

我认为这个例子是不行的,如果可能的话应该避免。为什么?很容易得到一个悬空的参考

举个例子来说明这一点:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

进入危险区域:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!