c++引用——它们只是语法糖吗?

C++ references - are they just syntactic sugar?

本文关键字:语法 引用 c++      更新时间:2023-10-16

c++引用只是语法糖,还是在某些情况下提供任何速度提升?

例如,指针调用无论如何都涉及到一个副本,而引用调用似乎也是如此。潜在的机制似乎是相同的。

编辑:在回答了大约六个问题和许多评论之后。我仍然认为参考文献只是语法糖。如果人们能直接回答"是"或"不是",如果有人能给出一个公认的答案?

假设引用是一个指针,

  1. 不能为NULL
  2. 一旦初始化,不能重新指向其他对象
  3. 任何使用它的尝试都将隐式地取消对它的引用:

    int a = 5;
    int &ra = a;
    int *pa = &a;
    ra = 6;
    (*pa) = 6;
    

在反汇编时的样子:

    int a = 5;
00ED534E  mov         dword ptr [a],5  
    int &ra = a;
00ED5355  lea         eax,[a]  
00ED5358  mov         dword ptr [ra],eax  
    int *pa = &a;
00ED535B  lea         eax,[a]  
00ED535E  mov         dword ptr [pa],eax  
    ra = 6;
00ED5361  mov         eax,dword ptr [ra]  
00ED5364  mov         dword ptr [eax],6  
    (*pa) = 6;
00ED536A  mov         eax,dword ptr [pa]  
00ED536D  mov         dword ptr [eax],6  
从编译器的角度来看,

对引用的赋值与对解引用指针的赋值是一样的。正如你所看到的,它们之间没有区别(我们现在不是在谈论编译器优化)但是,如上所述,引用不能为空,并且对其包含的内容有更强的保证。

对于我来说,我更喜欢使用引用,只要我不需要nullptr作为一个有效值,应该重指向的值或传递给不同类型的值(例如指针接口类型)。

引用比指针具有更强的保证,因此编译器可以更积极地进行优化。我最近看到GCC通过函数引用完美地内联了多个嵌套调用,但没有一个通过函数指针(因为它不能证明指针总是指向同一个函数)。

如果引用最终存储在某处,它通常占用与指针相同的空间。这并不是说,它将像指针一样使用:如果编译器知道引用绑定到哪个对象,它很可能会切断它。

编译器不能假定指针非空;在优化代码时,它必须要么证明指针非空,要么发出一个程序来说明它为空的可能性(在一个定义良好的上下文中)。

同样,编译器也不能假定指针永远不会改变值。(它也不能假设指针指向一个有效的对象,尽管我很难想象在一个定义良好的上下文中这有什么关系)

另一方面,假设引用是作为指针实现的,编译器仍然允许假设它是非空的,永远不会改变它指向的位置,并指向一个有效的对象。

没有


引用不仅仅是语法上的差异;它们也有不同的语义:

  • 一个引用总是别名一个现有的对象,不像一个指针可能是nullptr(一个哨兵值)。
  • 引用不能重新定位,它在整个生命周期中总是指向同一个对象。
  • 引用可以延长对象的生命周期,参见绑定到auto const&auto&&
因此,在语言层面上,引用是一个独立的实体。其余是实现细节。

引用与指针的不同之处在于,你不能对引用做一些事情,并使其成为定义行为。

不能取引用的地址,只能取被引用的地址。引用一旦创建,就不能修改。

T&T*const(注意,const适用于指针,而不是指向对象)是相对相似的。获取实际const值的地址并修改它是未定义的行为,就像修改(它直接使用的任何存储)引用一样。

现在,在实践中,你可以得到一个引用的存储:

struct foo {
  int& x;
};

sizeof(foo)几乎肯定等于sizeof(int*)。但是编译器可以自由地忽略直接访问foo字节的人实际上可能改变所引用的值的可能性。这允许编译器读取一次引用"指针"实现,然后不再读取它。如果我们有struct foo{ int* x; },编译器必须在每次执行*f.x时证明指针的值没有改变。

如果你有struct foo{ int*const x; },它再次开始在其不可变性中表现得像引用(修改被声明为const的东西是UB)。


我不知道任何编译器编写者使用的一个技巧是在lambda中压缩引用捕获。

如果你有一个lambda通过引用捕获数据,而不是通过指针捕获每个值,它可以只捕获堆栈帧指针。每个局部变量的偏移量是堆栈帧指针之外的编译时常量。

例外是由引用捕获的引用,在c++的缺陷报告中,即使引用变量超出了作用域,它也必须保持有效。所以这些必须被伪指针捕获

对于一个具体的例子(如果是一个玩具):

void part( std::vector<int>& v, int left, int right ) {
  std::function<bool(int)> op = [&](int y){return y<left && y>right;};
  std::partition( begin(v), end(v), op );
}

上面的lambda可以只捕获堆栈帧指针,并且知道leftright相对于它的位置,从而减小它的大小,而不是通过(基本上是指针)引用捕获两个int

这里我们有[&]暗示的引用,它们的存在比由value捕获的指针更容易消除:

void part( std::vector<int>& v, int left, int right ) {
  int* pleft=&left;
  int* pright=&right;
  std::function<bool(int)> op = [=](int y){return y<*pleft && y>*pright;};
  std::partition( begin(v), end(v), op );
}

引用和指针之间还有一些其他的区别:

引用可以延长临时对象的生命周期。

for(:)循环中大量使用。for(:)循环的定义依赖于引用生命周期扩展来避免不必要的复制,for(:)循环的用户可以使用auto&&自动推断出最轻的权重方式来包装迭代对象。

struct big { int data[1<<10]; };
std::array<big, 100> arr;
arr get_arr();
for (auto&& b : get_arr()) {
}

引用生命周期扩展仔细地防止不必要的复制发生。如果我们将make_arr更改为返回arr const&,它将继续工作而不需要任何副本。如果将get_arr更改为返回一个按值返回big元素的容器(例如,输入迭代器范围),则同样不会进行不必要的复制。

这在某种意义上是语法糖,但它允许相同的结构在许多情况下是最优的,而不必基于返回或迭代的方式进行微优化。


类似地,转发引用允许将数据智能地视为const、非const、左值或右值。临时值被标记为临时值,用户不再需要的数据被标记为临时值,将保留的数据被标记为左值引用。

引用相对于非引用的优势在于,您可以形成对临时对象的右值引用,如果不通过右值引用到左值引用的转换传递,则无法形成指向该临时对象的指针。

过去存在效率优势,因为引用更容易被编译器优化。然而,现代的编译器已经非常擅长于此,因此不再有任何优势。

引用相对于指针的一个巨大优势是引用可以指向寄存器中的值,而指针只能指向内存块。取某个本来应该在寄存器中的地址,你会强制编译器将该值放入正常的内存位置。这可以在紧密的循环中创造巨大的好处。

然而,现代的编译器非常好,它们现在可以识别出可以作为引用的指针,并将其视为引用。这可能会在调试器中导致相当有趣的结果,在调试器中,您可以有一个语句,如int* p = &x,要求调试器打印p的值,只是让它沿着"p不能打印"的行说一些东西,因为x实际上是在寄存器中,编译器将*p视为对x的引用!在本例中,实际上没有p 的值

(然而,如果你试图在p上做指针运算,你就会迫使编译器不再优化指针,使其像引用一样工作,并且一切都会变慢)

8.3.2 References [dcl.ref]

引用可以看作是对象的名称

不同于指针, 是保存Object**内存位置地址的变量(与引用不同)。该变量的类型是指向Object的指针。

内部引用可以实现为指针,但标准不保证如此。

所以回答你的问题:c++引用不是指针的语法糖。它是否提供任何加速已经得到了深入的回答。

******对象在这里是指任何具有内存地址的实例。甚至指针也是对象,函数也是对象(因此我们有嵌套的指针和函数指针)。类似地,我们没有指针可以引用,因为它们没有实例化。