我注意到我通常使用常量引用作为返回值或参数。我认为原因是它与在代码中使用非引用几乎相同。但它肯定需要更多的空间和功能声明变得更长。我很喜欢这样的代码,但我觉得有些人觉得编程风格不好。
你怎么看?是否值得在 int 上写 const int& ?无论如何,我认为它已经被编译器优化了,所以也许我只是在浪费我的时间来编写它,是吗?答案 0 :(得分:131)
在C ++中,我认为使用const T&
的反模式很常见,就像在处理参数时只是简单地说T
一样。然而,值和引用(无论是否为const)是两个完全不同的东西,并且总是盲目地使用引用而不是值可能导致细微的错误。
原因是在处理引用时,您必须考虑两个值不存在的问题:生命周期和别名。
作为示例,应用此反模式的一个示例是标准库本身,其中std::vector<T>::push_back
接受const T&
作为参数而不是值,这可以在代码中咬回来像:
std::vector<T> v;
...
if (v.size())
v.push_back(v[0]); // Add first element also as last element
这段代码是一个定时炸弹,因为std::vector::push_back
需要一个const引用,但是执行push_back可能需要重新分配,如果发生这种情况意味着在重新分配后,收到的引用将不再有效(生命周期问题),然后进入未定义的行为领域。
如果使用const引用而不是值,则别名问题也是细微问题的根源。我被这种代码所困扰:
struct P2d
{
double x, y;
P2d(double x, double y) : x(x), y(y) {}
P2d& operator+=(const P2d& p) { x+=p.x; y+=p.y; return *this; }
P2d& operator-=(const P2d& p) { x-=p.x; y-=p.y; return *this; }
};
struct Rect
{
P2d tl, br;
Rect(const P2d& tl, const P2d& br) : tl(tl), bt(br) {}
Rect& operator+=(const P2d& p) { tl+=p; br+=p; return *this; }
Rect& operator-=(const P2d& p) { tl-=p; br-=p; return *this; }
};
乍一看代码看起来非常安全,P2d
是一个二维点,Rect
是一个矩形,添加/减去一个点意味着翻译矩形。
但是,如果要在原点中将矩形转换回myrect -= myrect.tl;
,则代码将无效,因为已定义转换运算符接受(在这种情况下)引用同一实例的成员的引用。 / p>
这意味着在使用tl -= p;
更新topleft后,topleft将(0, 0)
,因为它应该p
同时(0, 0)
p
因为(0, 0)
只是对左上角成员的引用,因此右下角的更新将无效,因为它会将其翻译为const
,因此基本上什么也没做。
请不要因为单词const T&
而误以为const引用就像一个值。如果您尝试使用该引用更改引用的对象,则该单词仅存在为编译错误,但并不意味着引用的对象是常量。更具体地说,const ref引用的对象可以更改(例如,因为别名),并且在您使用它时可能会不再存在(生存期问题)。
在const
中,单词 const 表示引用的属性,而不是引用对象的属性:它是属性不可能用它来改变对象。可能 readonly 可能是一个更好的名称,因为 const 让IMO产生心理效应,即在使用引用时推动对象将保持不变的想法。
通过使用引用而不是复制值,您当然可以获得令人印象深刻的加速,特别是对于大类。但是在使用引用时应该总是考虑别名和生命周期问题,因为在封面下它们只是指向其他数据的指针。 然而,对于“本机”数据类型(整数,双精度,指针),引用实际上会比值慢,并且在使用它们而不是值时没有任何好处。
同样,const引用总是意味着优化器的问题,因为编译器被迫处于偏执状态,并且每次执行任何未知代码时,它必须假定所有引用的对象现在可能具有不同的值({{1}} for引用对于优化器来说绝对没有意义;那个词只是为了帮助程序员 - 我个人不太确定它是如此大的帮助,但这是另一个故事。)
答案 1 :(得分:14)
正如Oli所说,返回const T&
而不是T
是完全不同的事情,并且在某些情况下可能会中断(如他的例子中所示)。
以const T&
而非普通T
作为参数,不太可能破坏事物,但仍有几个重要的区别。
T
代替const T&
要求T
是可复制构建的。T
将调用复制构造函数,这可能很昂贵(以及函数退出时的析构函数)。T
可以将参数修改为局部变量(可以比手动复制更快)。const T&
可能会变慢。答案 2 :(得分:8)
int &
和int
不可互换!特别是,如果返回对本地堆栈变量的引用,则行为是未定义的,例如:
int &func()
{
int x = 42;
return x;
}
你可以返回对函数末尾不会被销毁的东西的引用(例如静态或类成员)。所以这是有效的:
int &func()
{
static int x = 42;
return x;
}
和外界一样,效果与直接返回int
相同(除了你现在可以修改它,这就是为什么你会看到const int &
很多)。
引用的优点是不需要复制,这对于处理大类对象很重要。但是,在许多情况下,编译器可以优化它;见例如http://en.wikipedia.org/wiki/Return_value_optimization
答案 3 :(得分:7)
如果被调用者和调用者是在单独的编译单元中定义的,则编译器无法优化远离引用。例如,我编译了以下代码:
#include <ctime>
#include <iostream>
int test1(int i);
int test2(const int& i);
int main() {
int i = std::time(0);
int j = test1(i);
int k = test2(i);
std::cout << j + k << std::endl;
}
在优化级别为3的64位Linux上使用G ++。第一次调用不需要访问主内存:
call time
movl %eax, %edi #1
movl %eax, 12(%rsp) #2
call _Z5test1i
leaq 12(%rsp), %rdi #3
movl %eax, %ebx
call _Z5test2RKi
第1行直接使用eax
中的返回值作为test1
中edi
的参数。第2行和第3行将结果推送到主存储器并将地址放在第一个参数中,因为该参数被声明为对int的引用,因此它必须可以例如拿它的地址。是否可以使用寄存器完全计算某些东西或者需要访问主存储器这些天可以产生很大的不同。因此,除了更多的类型,const int&
也可能更慢。经验法则是,通过值传递最多与字大小一样大的所有数据,并通过引用传递所有其他数据。还通过引用传递模板化参数;由于编译器可以访问模板的定义,因此它总是可以优化参考。
答案 4 :(得分:3)
而不是“思考”它被编译器优化了,为什么你不能得到汇编列表并确定?
junk.c ++:
int my_int()
{
static int v = 5;
return v;
}
const int& my_int_ref()
{
static int v = 5;
return v;
}
生成的汇编程序输出(省略):
_Z6my_intv:
.LFB0:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl $5, %eax
ret
.cfi_endproc
...
_Z10my_int_refv:
.LFB1:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
movl $_ZZ10my_int_refvE1v, %eax
ret
两者中的movl
指令非常不同。第一个将5
移动到EAX
(恰好是传统上用于返回x86 C代码中的值的寄存器),第二个将变量的地址(为清晰起见而省略的细节)移动到{{1} }}。这意味着第一种情况下的调用函数可以直接使用寄存器操作而不需要使用内存来使用答案,而在第二种情况下它必须通过返回的指针来访问内存。
所以看起来它没有被优化掉。
这超出了您在此处提供的其他答案,解释了为什么EAX
和T
不可互换。
答案 5 :(得分:0)
int与const int&不同:
2,int是另一个整数变量(int B)的值副本,这意味着:如果我们更改int B,则int的值不会改变。
请参见以下c ++代码:
int main(){
向量a {1,2,3};
int b = a [2]; //即使向量改变,该值也不会改变
const int&c = a [2]; //这是参考,因此值取决于向量;
a [2] = 111;
// b将输出3;
// c将输出111;
}
答案 6 :(得分:0)
const int &
可能仍需要传递一个指针,该指针的大小与int
相当。几乎不可能带来更好的性能。
传递const引用可能需要引起注意,以检查它不会是值的意外更改(例如,您调用的某些函数也可以访问它,甚至另一个线程)。但这通常是显而易见的,除非该变量具有异常长的使用寿命和非常宽的访问范围。