参数修饰情形
在C++11里,一个类型std::string有以下4种情形。
std::string s = "123"; // 值
const std::string s_c = "456"; // 不可修改的值
std::string& s_ref = s; // 引用
const std::string& s_cref = s; // 不可修改的引用
假如我们实现一个Test函数传入std::string作为参数,我们会遇到以下情形的调用:
Test(s);
Test(s_c);
Test(s_ref);
Test(s_cref);
另外再加上临时变量的场景。
Test(std::string("temp"));
最理想的情况下, 我们希望Test函数只需要写一遍, 即可完美匹配以上所有的类型加修饰。
参数的匹配规律
1.void Test(std::string str);
参数按值传递到函数,无论输入的std::string哪一种, 都会对输入参数做一次拷贝,再传递到函数内部处理。通用所有情形,缺点就是拷贝开销。
2.void Test(const std::string str);
与1几乎一模一样, 除了在函数内部无法修改std::string和调用std::string的非const函数。在std::string已经进行了拷贝的情况下, 拷贝的值只有函数内部用到, 保持const没有什么意义。
3.void Test(std::string& str);
只能匹配s和s_ref。 所有带const修饰, 或者临时变量, 都无法匹配到这个版本。但是这个版本有一个其他版本没有的特点,就是可以修改传进来的值。但是不建议使用, 后面会说到需要返回值的情况应该如何定义。
4.void Test(const std::string& str);
可以匹配所有情形, 并且无需拷贝。 从通用和效率来考虑,这种无疑是最高。
5.void Test(std::string&& str);
c++11新增的右值引用, 只能匹配临时变量的情形。合情合理, 右值引用本来就是为了临时变量而生。 这个版本的特点就是,str是在外部构造的,右值引用明确表达了, 外部不再需要用到str的了,str可以随便处置, 特别是str内部的一些资源, 可以直接拿来构造一个新的类, 避免对这些资源进行拷贝。 大部分情况下, 右值引用的版本都是为了提升性能而额外定义的,即使删掉函数, 系统依然能在其他版本的函数中匹配成功,通常是Test(const std::string& str),只是效率没那么高。
6.void Test(const std::string&& str);
与5几乎一模一样, 却又限制了str不能修改, 不能调用非const函数,有了5以后,6就没什么意义。
匹配冲突规律
1.void Test(std::string str) 和 其他所有冲突。
2.void Test(const std::string str) 和 其他所有冲突。
3.void Test(std::string& str) 和 void Test(const std::string& str)不冲突
当参数为std::string&时,void Test(std::string& str)优先匹配, 当它不存在时,
会匹配void Test(const std::string& str)版本。
4.void Test(std::string&& str);与void Test(std::string str);会冲突
void Test(std::string&& str);与void Test(const std::string str);会冲突
void Test(std::string&& str);与void Test(std::string& str);不冲突, 他们面向的场景完全不一样。
void Test(std::string&& str);与void Test(const std::string& str);不冲突, 当参数为临时变量, 或者std::move()包含的变量时, 优先匹配void Test(std::string&& str);。
5.void Test(const std::string&& str);没什么意义, 当void Test(std::string&& str);不存在,并且参数为临时变量, 或者std::move()包含的变量时,才会匹配到它。
重载最佳实践
1.对于输入参数来说, 永远使用void Test(const std::string& str);版本。
首先它能匹配所有情形, 其次不会对外部传入的参数做拷贝操作。 若真有拷贝需要,在函数内部拷贝一份便是。
2.对于某些对性能要求高的场景下,可在1的基础上,增加void Test(std::string&& str);版本。
3.避免使用void Test(std::string& str)版本,例如:
std::string str = "123";
Test(str);
在阅读以上代码时,Test函数到底有没有对str做过修改? 我们是无法一眼看得出来的。 当我们阅读这样的代码时, 每次遇到一个函数,都需要跳转到函数实现处查看才能确定,给大脑的思绪造成停顿, 对代码阅读,维护有十分具大的影响。
假如我们统一规定:
需要返回值给函数外部时, 只通过返回值, 或者指针的方式。
需要从外部传递值给内部时,统一通过const &的方式, 追求效率时再增加&&右值引用方式。
保持这种习惯, 我们代码的可读性将会上一个层次。
void Test(std::string* p_str);
std::string str = "123";
Test(&str);
Show(str);
AppendTo(&dst, src);
我们一眼就能看出, 函数需要哪些变量输入, 函数会修改到哪些变量。 逻辑瞬间清晰。
4.对于函数确定类型并且是int, double等基本类型, 可以不用const &的方式, 直接传值代码更加简洁, 并且基本类型拷贝成本低, 没有右值引用的需求。 唯独存在一种情形, 就是在模板的时候, 因为类型是不确定的, const&方式最能兼顾通用与效率, 它最优先。
举个例子
template<typename T>
T Add(const T& t1, const T& t2)
{
return (t1 + t2);
}
T可能是int, 也可以是std::string, 也可以是其他自定义支持+操作符的类。
最佳使用const T&的声明方式。有更高的性能等需求,可以再通过进一步的模板特化去实现。