Effective Modern C++ 之 避免对以全局引用为参数的函数重载
考察以下代码:
std::multiset<std::string> names; //global data structure void LogAndAdd(const std::string &name) { auto now = //got current time std::chrono::system_clock::now(); log(now, "logAndAdd"); //make log entry names.emplace(name); }
这看起来似乎没什么问题. 没错, 实际上这么编译也是可以通过的, 可是还存在优化的空间. 考察以下情况:
std::string petName = ("Darla"); LogAndAdd(petName); LogAndAdd(std::string("Persephone")); LogAndAdd("Patty Dog");
其中, 只有第一个输入的参数是左值, 第二个是一个右值, 而第三个会先产生一个临时拷贝(所以说也是右值), 但是由于没有相应的函数对应, 调用这三次操作的开销都是拷贝的开销, 咱们本来是可以把后两个移动过去的对不? 所以可以这样:
template<typename T> void LogAndAdd(T &&name) { auto now = std::chono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); }
这下怎么样? 爽! 但是问题来了, 假如需求变了, 名字啥的需要通过索引来查找:
std::string NameFromIdx(int idx); void LogAndAdd(int idx) { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(NameFromIdx(idx)); }
对啊, 就是中间要多一道, 你这不是也实现了吗? 可是问题来了, 扭曲的用户可是什么都能做出来的, 如果你遇到这这么一个人. TA:
short nameIdx; ... LogAndAdd(nameIdx);
那么问题就来了. 怎么, 因为 nameIdx 是个 short(短小) 所以参数为 int 的那个版本就不会接受它, 而使用模板的版本则以它开放的胸怀把它接受了, 当然结果就是一大长串莫名其妙的错误信息.
同样的, 假如有这么个类:
class Person { public: tempalte<typename T> explicit Person(T &&n) : _name(std::forward<T>(n)) {} explicit Person(int idx); Person(const Person&); Person(Person&&); ... };
看起来挺严谨, 实际上如果咱们这样:
Person p("Nancy"); //good name auto cloneOfP(p); //WTF
对, 又出事了, 这个叫做 Nancy 的女士(不会是男的吧...) p, 人家是个非常量左值(额, non-const lvalue) 所以那几个非模板版本的移动/拷贝 构造函数根本就不知道这是个啥. 所以 Nancy 就被抛给用模板实现的版本了:
//就是这样 class Person { public: tempalte<typename T> explicit Person(Person &n) : _name(std::forward<Person&>(n)) {} explicit Person(int idx); Person(const Person&); Person(Person&&); ... };
当然, 就像上面分析的, 当 Nancy 是个常量, 非模板版本的拷贝函数就来接家属了:
const Person p("Nancy"); auto cloneOfP(p); //copy ctor
可问题是, 总不能项目里都是常量对象吧, 所以具体怎么处理这个棘手的问题, 请看下篇博客.