【问题标题】:Does pass-by-reference decay into pass-by-pointer in some cases?在某些情况下,按引用传递是否会衰减为按指针传递?
【发布时间】:2022-01-21 03:15:42
【问题描述】:

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

引用参数是否会衰减为逻辑上需要的指针?

让我解释一下我的意思:

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

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

(假设这不会被内联)

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

问题在于(再次假设 this 没有内联),如果从大量不同的位置调用函数,则每次调用的相关值可能位于堆栈中的不同位置,这意味着调用无法优化。

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

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

奖金问题代码:

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”的值是可以的,因为你从不尝试写入它,只读取。

【问题讨论】:

  • 每个问题只有一个问题。底线是非常简单的IMO。编译器通常比您在优化方面要好得多。如果您需要比较不同的方法,您应该对它们进行分析。猜测或推理编译器可能在内部做什么很可能不会给你任何帮助。此外,就像您在问题中所做的那样,将一堆假设叠加在一起,这也表明您正在以错误的方式思考这个问题。
  • 在某些情况下,引用传递是否会衰减为指针传递? 不,不是在 C++ 抽象机中。编译器可能使用指针作为实现细节,或者它可能使用 as-if 规则,或者它可能使用别名,或者它可能使用寄存器......它不是以编程方式公开的。
  • 引用传递通常使用对象的地址作为管理引用的方式;编译器知道这一点,并生成代码来处理对引用的读取和写入。如果您认为将地址传递为“衰减为传递指针”,那么,是的,引用衰减为传递指针。但这不是一个有用的特征,因为它混合了语义和实现细节。无论编译器如何实现引用,您可以使用引用执行的操作(如果可以使用指针执行)在源代码中的编写方式不同。
  • 将一些测试用例放在一起(与您的实际问题相匹配)并使用在线编译器(例如godbolt.org)来检查生成的代码。例如godbolt.org/z/TMnd4WjWT [注意for clang sum 永远不会被调用。]
  • 关于附加问题:即使b 没有被sum 函数改变,这并不意味着b 永远不会改变。例如,某些代码可能会调用它sum(x, x);

标签: c++ pointers reference


【解决方案1】:

编译器可以决定将引用实现为指针、内联或它选择使用的任何其他方法。就性能而言,这无关紧要。当涉及到优化时,编译器可以并且将会做任何它想做的事情。如果编译器愿意(并且在特定情况下这样做是有效的),编译器可以将您的引用实现为按值传递。 缓存结果无济于事,因为无论如何编译器都会这样做。 如果您想明确告诉编译器该值可能会更改(因为另一个线程可以访问相同的指针),您需要使用关键字 volatile (或者如果您还没有使用 std::atomic 则使用 std::atomic: :互斥)。如果您不使用关键字 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 是可编辑的,指针不是。

【讨论】:

  • &amp;5 是编译器错误:地址运算符的操作数必须是 L 值。
猜你喜欢
  • 1970-01-01
  • 2012-01-24
  • 1970-01-01
  • 2020-12-28
  • 2010-10-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多