在某些情况下,按引用传递会衰减为按指针传递吗?
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 个实际差异。
- 获取临时值(右值)的地址(指针)在 C++ 中是未定义的行为。
- 引用是不可变的,有时需要包裹在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 是可编辑的,指针是不可编辑的。
我一直在寻找这个问题的答案,但我似乎找不到任何东西,所以我在这里问:
引用参数是否在逻辑上必要的地方衰减为指针?
让我解释一下我的意思:
如果我声明一个函数并将对 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 个实际差异。
- 获取临时值(右值)的地址(指针)在 C++ 中是未定义的行为。
- 引用是不可变的,有时需要包裹在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 是可编辑的,指针是不可编辑的。