C++ 参数传递
一、参数传递:入与出
1.对于入参,拷贝开销低的类型按值传递,其他类型则以const引用来传递
2.对于转发参数,要用T&&来传递,并且只std::forward该参数
template <typename T, typename ... T1>
T create(T1&& ... t1)
{return T(std::forward<T1>(t1)...);
}
形参包的打包和解包
当省略号在参数类型T1的左边时,参数包被打包;当省略号在右边时,参数包被解包。返回语句T(std::forward<T1>(t1)...)中的这种解包实质上意味着表达式std::forward<T1>(t1)被不断重复,直到形参包的所有参数都被消耗掉,并且会在每个子表达式之间加一个逗号。
转发与变参模板的结合是C++中典型的创建模式
template<typename T, typename ... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
3.对于入-出参数,使用非const的引用来传递
4.对于出的输出值,优先使用返回值而非输出参数
用返回值就好,但别用const,因为它不但没有附加价值,而且会干扰移动语义。也许你认为值的复制操作开销巨大,这既对也不对。原因在于编译器会应用RVO(return value optimization,返回值优化)或NRVO(named return value optimization,具名返回值优化)。RVO意味着编译器可以消除不必要的复制操作。到C++17,原本只是可能会做的优化成了一种保证。
MyType func()
{return MyType{}; //C++17中不会拷贝
}MyType myType = func(); //C++17中不会拷贝
这几行中可能会发生两次不必要的拷贝操作:第一次在返回调用中,第二次在函数调用中,C++17中则不会有拷贝操作发生。如果返回值有个名字,我们就称这种优化为NRVO。
MyType func()
{MyType myValue;return myValue; //允许拷贝一次
}MyType myType = func(); //在C++17中不会拷贝
按照C++17,编译器仍然可以在返回语句中拷贝值myValue,但在函数调用的地方则不会发生拷贝。
5.要返回多个出值,优先考虑返回结构体或者多元组
std::set<int>::iterator iter;
bool inserted = false;std::tie(iter, inserted) = mySet.insert(0);
auto [iter2, inserted2] = mySet.insert(1);
二、所有权语义
- func(value):函数func自己有一份value的拷贝并且就是其所有者。func会自动释放该资源
- func(pointer*):func借用了资源,所以无权删除该资源。func在每次使用前都必须检查该指针是否为空指针
- func(reference&):func借用了资源。与指针不同,引用的值总是合法的
- func(std::unique_ptr):func是资源的新所有者。func的调用方显式地把资源的所有权传给了被调用方。func会自动释放该资源
- func(std::shared_ptr):func是资源的额外所有者。func会延长资源的生存期。在func结束时,它也会结束对资源的所有权。如果func是资源的最后一个所有者,那么它的结束会导致资源的释放。
三、值返回语义
1.返回T*(仅仅)用于表示位置
Node* find(Node* t, const std::string& s)
{if (!t || t->name == s) return t;if ((auto p = find(t->left, s))) return p;if ((auto p = find(t->righr, s))) return p;return nullptr;
}
2.但不需要发生拷贝,也不需要表达“没有返回对象”时,应返回T&
A& operator=(const A& rhs) {...};
A operator(const A& rhs) {...};A a1, a2, a3;
a1 = a2 = a3;
返回拷贝(A)的拷贝赋值运算符会触发两个额外的A类型临时对象的创建。
返回局部对象的引用(指针)是未定义行为。
3.不要返回T&&以及不要返回std::move(本地变量)
4.main()的返回类型是int
四、其他函数
lambda表达式
1.当函数不适用时(需要捕获局部变量,或编写一个局部函数),请使用Lambda表达式
- 如果可调用实体必须捕获局部变量或者它是在局部作用域内声明的,你就必须使用Lambda函数
- 如果可调用实体需要支持重载,那么应使用普通函数
2.在局部使用(包括要传递给算法)的lambda表达式中,优先通过引用来捕获
3.在非局部使用(包括要被返回、存储在堆上或要传给其他线程)的lambda表达式中,避免通过引用来捕获。
lambda表达式应该只对有效数据进行操作。当lambda通过拷贝捕获数据时,根据定义,数据总是有效的。当lambda通过引用捕获数据时,数据的生存期必须超过lambda的生存期。
4.在有选择的情况下,优先采用默认参数而非重载
如果你需要用不同数量的参数来调用一个函数,尽可能优先采用默认参数而不是重载。这样你就遵循了DRY原则。
void print(const string& s, format f = {});
若要用重载实现同样的功能,则需要两个函数:
void print(const std::string& s); //使用默认格式
void print(const std::string& s, format f);
5.不要使用va_arg参数
#include <cstdarg>int sum(int num, ...)
{int sum = 0;va_list argPointer; //保存下列宏的必要信息va_start(argPointer, num); //启用对变参数函数的参数for (int i = 0; i < num; ++i){sum += va_arg(argPointer, int); //访问下一个变参数函数的参数}va_end(argPointer); //结束对变参数函数参数的访问return sum;
}int main()
{std::cout << "sum(1, 5) " << sum(1, 5) << "\n";std::cout << "sum(3, 1, 2, 3) " << sum(3, 1, 2, 3) << "\n";// 参数的num的数量是错的std::cout << "sum(3, 1, 2, 3, 4) " << sum(3, 1, 2, 3, 4) << "\n";// double而不是int, 值错误std::cout << "sum(3, 1, 2, 3.5) " << sum(3, 1, 2, 3.5) << "\n";
}
C++17的折叠表达式解决
template<typename ...Args>
auto sum(Args ...args)
{return (... + args);
}int main()
{std::cout << "sum(5) " << sum1(5) << "\n";std::cout << "sum(1, 2, 3) " << sum1(1, 2, 3) << "\n";std::cout << "sum(1, 2, 3, 4) " << sum1(1, 2, 3, 4) << "\n";std::cout << "sum(1, 2, 3.5) " << sum1(1, 2, 3.5) << "\n";
}
它需要至少一个参数,并使用C++11的变参模板。变参模板可以接受任意数量的参数。这些任意数量的参数由所谓的参数包持有,用省略号(...)表示。此外,在C++17中,可以用二元运算符直接对参数包进行归约。这一针对变参模板的增强被称为折叠表达式。在sum函数的例子中,应用了二元的+运算符(...+args)。
总结:
- 一个函数应该执行一个操作,要简短,并有一个精心选择的名字。
- 要把可以在编译期运行的函数实现为constexpr
- 如果可能的话,将你的函数实现为纯函数
- 当你的函数需要接受任意数量的参数时,要使用变参模板而不是va_arg参数