C ++引用 - 它们只是语法糖吗?

时间:2015-07-07 08:01:00

标签: c++

C ++参考只是语法糖,还是在某些情况下提供任何加速?

例如,无论如何,按指针调用都涉及到一个副本,而且对于逐个引用调用似乎也是如此。基本机制似乎是相同的。

编辑:经过大约六个答案和许多评论。我仍然认为参考文献只是合成糖。如果人们可以直接回答是或否,以及是否有人可以接受答案?

7 个答案:

答案 0 :(得分:60)

假设引用为:

指针
  1. 不能为空
  2. 初始化后,无法重新指向其他对象
  3. 任何使用它的尝试都会隐含地取消引用它:

    int a = 5;
    int &ra = a;
    int *pa = &a;
    
    ra = 6;
    
    (*pa) = 6;
    
  4. 这里看起来像反汇编:

        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作为有效值,应该重新定义的值或要传递给不同类型的值(例如指向接口类型的指针)

答案 1 :(得分:59)

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

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

答案 2 :(得分:20)

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

同样,编译器也不能假设指针永远不会改变值。 (它也不能假设指针指向一个有效的对象,尽管我很难想象一个在明确定义的上下文中这很重要的情况)

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

答案 3 :(得分:10)

引用与指针的不同之处在于,您无法对引用执行某些操作并将其定义为行为。

您不能获取引用的地址,而只能获取引用的地址。创建引用后,您无法修改引用。

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 s(基本上指针)参考。

这里我们有[&]隐含的引用,它们的存在比它们按值捕获的指针更容易被删除:

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,左值或右值。 Temporaries被标记为临时数据,用户不再需要的数据被标记为临时数据,将持久存储的数据标记为左值参考。

这里的优势引用超过了非引用,你可以形成一个临时的rvalue引用,并且你不能形成一个指向那个临时的指针而不通过rvalue引用到左值的引用转换。

答案 4 :(得分:9)

以前存在效率优势,因为编译器更容易优化引用。然而,现代编译器已经变得如此优秀,以至于不再有任何优势。

一个巨大的优势引用指针有一个参考可以引用寄存器中的值,而指针只能指向内存块。获取寄存器中的某些内容的地址,然后强制编译器将该值放入普通的内存位置。这可以在紧密循环中创造巨大的好处。

然而,现代编译器非常好,以至于它们现在识别出可能是所有意图和目的的引用的指针,并且将它视为与引用完全相同。这可能会在调试器中产生相当有趣的结果,您可以在其中使用int* p = &x等语句,让调试器打印p的值,只是让它说出“p”的内容。无法打印“因为x实际上在寄存器中,并且编译器将*p视为对x的引用!在这种情况下,p

实际上没有价值

(但是,如果你试图在p上进行指针运算,那么你就会强制编译器不再优化指针,就像引用一样,并且一切都会变慢)

答案 5 :(得分:9)

引用不仅仅是语法的区别;它们也有不同的语义

  • 引用始终为现有对象设置别名,与可能为nullptr的指针(标记值)不同。
  • 无法重新定位引用,它始终指向同一个对象。
  • 引用可以延长对象的生命周期,请参阅绑定到auto const&auto&&

因此,在语言层面,引用是它自己的实体。其余的是实施细节。

答案 6 :(得分:1)

8.3.2参考文献[dcl.ref]

  

引用可以被认为是对象的名称

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

内部引用可以实现为指针,但标准从未保证。

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

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