在某些情况下,按引用传递会衰减为按指针传递吗?

Does pass-by-reference decay into pass-by-pointer in some cases?

我一直在寻找这个问题的答案,但我似乎找不到任何东西,所以我在这里问:

引用参数是否在逻辑上必要的地方衰减为指针?

让我解释一下我的意思:

如果我声明一个函数并将对 int 的引用作为参数:

void sum(int& a, const int& b) { a += b; }

(假设这不会被内联)

逻辑假设是可以通过不传递任何参数来优化调用此函数,而是让函数访问已在堆栈上的变量。直接更改这些可以避免传递指针。

问题在于(同样,假设这没有被内联),如果从大量不同的地方调用该函数,则每次调用的相关值可能位于堆栈中的不同位置,这表示调用无法优化。

这是否意味着,在那些情况下(如果从代码中的大量不同位置调用函数,这可能构成大多数情况),引用衰减为指针,该指针被传递到函数并用于影响外部范围内的变量?

额外问题:如果这是真的,是否意味着我应该考虑在函数体内缓存引用的参数,这样我就可以避免传递这些引用时隐藏的取消引用?然后我会保守地访问实际的参考参数,只有当我需要实际向它们写入一些东西时。如果认为取消引用的成本高于一次复制它们的成本,这种方法是否有保证或最好相信编译器为我缓存这些值?

奖励问题代码:

void sum(int& a, const int& b) {
    int aCached = a;
    // Do processing that required reading from a with aCached.
    
    // Do processing the requires writing to a with the a reference.
    a += b;
}

额外的问题:可以安全地假设(假设上面的一切都是真的),当传递“const int& b”时,编译器将足够聪明,在传递指针时按值传递 b'效率不够高?我的理由是“const int& b”的值是可以的,因为你从不尝试写入它,只读。

编译器可以决定将引用实现为指针、内联或它选择使用的任何其他方法。就性能而言,这是无关紧要的。当涉及到优化时,编译器可以并且将会做任何它想做的事情。如果需要,编译器可以将您的引用实现为按值传递(如果在特定情况下这样做是有效的)。 缓存结果无济于事,因为编译器无论如何都会这样做。 如果你想明确地告诉编译器该值可能会改变(因为另一个线程可以访问同一个指针),你需要使用关键字 volatile(或者 std::atomic 如果你还没有使用 std::mutex).
编辑:多线程永远不需要关键字“volatile”。 std::mutex 就够了。
如果您不使用关键字 volatile,编译器几乎肯定会为您缓存结果(如果适用)。 但是,指针和引用之间的规则至少有 2 个实际差异。

  1. 获取临时值(右值)的地址(指针)在 C++ 中是未定义的行为。
  2. 引用是不可变的,有时需要包裹在std::ref.

这里我将举例说明两者的区别。

此代码使用引用有效:

static int do_stuff(const int& i)
{
}
int main()
{
  do_stuff(5);
  return 0;
}

但是这段代码有未定义的行为(实际上它可能仍然有效):

static int do_stuff(const int* i)
{
}
int main()
{
  do_stuff(&5);
  return 0;
}

那是因为获取临时值(非左值)的地址在 C++ 中是未定义的行为。不保证该值具有地址。注意取这样的地址有效的:

static int do_stuff(const int& i)
{
  const int *ptr = &i;
}
int main()
{
  do_stuff(5);
  return 0;
}

因为在函数内部do_stuff,变量有一个名字,因此是一个左值。这意味着当它进入 do_stuff 时,它肯定有一个地址。

这就是 C++ 中指针和引用之间的一个区别。 还有一个区别,那就是常量性/不变性。

在 C++ 中需要了解的一件重要事情是辅助函数 std::ref 的使用。 考虑以下代码:

#include <functional>
#include <thread>
#include <future>
#include <chrono>
#include <iostream>

struct important_t
{
  int val = 0;
};

static void work(const volatile important_t& arg)
{
  std::cout << "Doing work..." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(3));
}

int main()
{
  important_t my_object;
  {
    std::cout << "Starting thread" << std::endl;
    std::future<void> t = std::async(std::launch::async, work, std::ref(my_object));
    std::cout << "Waiting for thread to finish" << std::endl;
  }
  
  return 0;
}

上面的代码编译得很好,是完全有效的 C++ 代码。 但是如果你这样写:

std::future<void> t = std::async(std::launch::async, work, my_object);

它不会编译。那是因为std::ref。 没有 std::ref 代码无法编译的原因是函数 std::async(以及 std::thread)要求作为函数参数传递的每个对象都是 复制构造。 这证明了引用与 C++ 中所有其他内置类型之间的根本区别。引用是不可变的,并且没有办法使它们可编辑。 考虑以下代码:

#include <iostream>
int main()
{
  // Perfectly valid
  // Prints 5
  {
    int val = 0;
    int& val_ref = val;
    val_ref = 5;
    std::cout << val << std::endl;
  }
  // Compiler error:
  // A reference must always be initialized.
  // A reference will always point to the same value throughout its lifetime.
  {
    int val = 0;
    int& val_ref;
    val_ref = val;
    val_ref = 5;
    std::cout << val << std::endl;
  }
  // We will encounter a similar compiler error with a const pointer:
  // A const value must always be initialized.
  // A const pointer will always point to the same value throughout its lifetime.
  {
    int val = 0;
    int *const val_ptr;
    val_ref = &val;
    val_ref = 5;
    std::cout << val << std::endl;
  }
  return 0;
}

由此得出的结论是,引用与 C++ 中的指针不同。它几乎与 const 指针相同。 稍微说明一下:

指向 const int 的 const 指针:

void do_stuff(const int *const val)
{
  int i;
  val = 5; // Error
  val = &i; // Error
}

指向 int 的 const 指针:

void do_stuff(int *const val)
{
  int i;
  val = 5; // Allowed. The int is not const.
  val = &i; // Error
}

指向 const int 的指针:

void do_stuff(const int* val)
{
  int i;
  val = 5; // Error
  val = &i; // Allowed
}

C++ 中的 int 引用最接近指向 int 的 const 指针。 int 是可编辑的,指针是不可编辑的。