C++参考 - 它们只是语法糖吗?

sam*_*foz 64 c++

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

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

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

Vin*_*ile 61

假设引用为指针:

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

    int a = 5;
    int &ra = a;
    int *pa = &a;
    
    ra = 6;
    
    (*pa) = 6;
    
    Run Code Online (Sandbox Code Playgroud)

这里看起来像反汇编:

    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  
Run Code Online (Sandbox Code Playgroud)

从编译器的角度来看,分配给引用与分配给解除引用的指针是一回事.正如您所看到的,它们之间没有区别(我们现在不讨论编译器优化)但是如上所述,引用不能为空并且对它们包含的内容有更强的保证.

至于我,我更喜欢使用引用,只要我不需要nullptr作为有效值,应该重新定义的值或要传递给不同类型的值(例如指向接口类型的指针).

  • 你实际上回答了用户的问题而没有添加任何类似"甚至不想那样"或"请提供证据来支持你的问题"或使用术语"XY问题".所以可能不会对此表示祝贺,但我愿意. (21认同)
  • 关闭课程优化.这允许我们在低级实现上看到高级语法.你最不希望看到什么优化?这个特定的例子根本不会被编译.在优化过程中它将被忽略为无用.优化代码的一个例子将向我们展示几乎没有.好吧,它会告诉我们,在某些情况下,一种方法比另一种更好,但仍有数千种情况尚未测试. (7认同)
  • @ user1903064:那么我可能是第一个祝贺你成为一个反对44万用户的人.当然,他们的意见与你的相比相形见绌,非常棒!:) (4认同)
  • 您提供了关闭优化的代码.比较没有优化的代码是相对没有意义的. (2认同)

Que*_*tin 58

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

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

  • @samofoz No.语言明智:你不能拿一个临时的地址,但你可以绑定一个临时的引用.优化方式:保证引用引用有效对象,指针始终为null. (8认同)
  • @LaurentLARIZZA取消引用无效指针或空指针是UB. (6认同)
  • 你的轶事似乎很奇怪,代码在哪里?似乎GCC也可以优化,如果指针是**const**(防止重新座位). (6认同)
  • 除了一些无用的形式主义之外,在解除引用(但实际上不是实际加载/存储)空指针以获得将在稍后用于加载/存储的引用与通过直接解除引用执行加载/存储之间没有区别.参考文献**在实际安全性方面没有比指针更强的"更强保证".他们只是改变了UB的正式点. (4认同)
  • @Quentin:如果编译器无法使用指针进行优化但可以使用引用进行优化,则编译器可能无法使用指针版本来证明非别名.有多种方法可以解决这个问题. (4认同)

小智 20

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

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

在另一方面,假设引用,作为指针实现,编译器仍然可以假设它是不为空,永远不会改变它指向,并指向一个有效的对象.

  • 不确定我是否同意第一点.问题是,只要指针被解除引用,编译器就可以自由地应用所有这些优化.如果指针确实为NULL或无效,则解除引用已将您发送到undefined-behavior-land,因此任何优化都是有效的.所以这种情况适用的唯一情况是,如果你知道指针有效,那么_could_优化的指针代码,但永远不要取消引用指针(所以UB参数不适用).我觉得这是一个相当罕见的情况. (4认同)

Yak*_*ont 10

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

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

A T&和a T*const(注意const适用于指针,而不是指向的指针),它们相对类似.获取实际const值的地址并对其进行修改是未定义的行为,因为修改(它直接使用的任何存储)是一个引用.

现在,在实践中,您可以获得一个参考存储:

struct foo {
  int& x;
};
Run Code Online (Sandbox Code Playgroud)

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 );
}
Run Code Online (Sandbox Code Playgroud)

上面的lambda只能捕获堆栈帧指针,并知道它的位置leftright相对位置,减小它的大小,而不是捕获两个ints(基本上是指针)引用.

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

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 );
}
Run Code Online (Sandbox Code Playgroud)

引用和指针之间还有一些其他差异.

引用可以延长临时的生命周期.

这在for(:)循环中使用很多.for(:)循环的定义都依赖于引用生命周期扩展以避免不必要的副本,并且for(:)循环的用户可以使用auto&&自动推导出最轻的权重方式来包装迭代对象.

struct big { int data[1<<10]; };

std::array<big, 100> arr;

arr get_arr();

for (auto&& b : get_arr()) {
}
Run Code Online (Sandbox Code Playgroud)

这里引用生命周期扩展小心地防止不必要的副本发生.如果我们更改make_arr为返回a,arr const&它将继续工作,没有任何副本.如果我们更改get_arr为返回big按值返回元素的容器(例如,输入迭代器范围),则不会再执行任何不必要的复制.

这在某种意义上是语法糖,但它允许相同的构造在许多情况下是最佳的,而不必根据事物的返回或迭代进行微观优化.


类似地,转发引用允许数据被智能地视为const,非const,左值或右值.临时工具被标记为临时工具,用户不再需要的数据被标记为临时数据,将持久存储的数据标记为左值参考.

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


Mat*_* M. 10

没有


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

  • 引用总是别名现有对象,不像指针可能是nullptr(哨兵值).
  • 引用无法重新定位,它始终指向同一个对象.
  • 引用可以延长对象的生命周期,请参阅绑定到auto const&auto&&.

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

  • @MichaelGrünewald:引用不能为null,取消引用空指针是未定义的行为,因此未定义的行为形成"空引用".讨论在表现出未定义行为的程序中可能发生的事情是毫无意义的(从语言的角度来看),因为根据定义,任何行为都是正确的.至于无法删除...好吧,在现代C++中,手动调用`delete`是一个红鲱鱼,所以我觉得它不是一个非常重要的区别. (3认同)

Cor*_*ica 9

过去有效率优势,因为编译器更容易参考优化.然而,现代编译器已经变得如此优秀,以至于不再有任何优势.

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

但是,现代编译器非常好,以至于它们现在可以识别出一个指针,它可以作为所有意图和目的的参考,并将其视为与引用完全相同.这可能会在调试器中产生相当有趣的结果,你可以在其中有一个语句,例如int* p = &x,要求调试器打印值p,只是让它说"p不能打印"的内容,因为x实际上是在寄存器中,编译器将其*p作为参考x!在这种情况下,实际上没有价值p

(但是,如果您尝试执行指针运算p,则会强制编译器不再优化指针以像引用那样运行,并且一切都会变慢)