起因 上个月我做了一个实验,对比了C++中几种Callable实现方式的性能 。今天我想继续这个实验,给Worker
类加一个接受右值的构造函数。
下文中的编译环境都是Apple LLVM version 8.1.0 (clang-802.0.42)。
第一次尝试 我的第一版代码长这样(精简后):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; template <typename HandlerT>void Test (HandlerT&& h) { Worker<HandlerT> worker{std::forward<HandlerT>(h)}; } void Func (int x) {}int main (int argc, char ** argv) { Test (Func); }
编译居然出错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ > g++ -std=c++11 func_pointer_ctor.cpp func_pointer_ctor.cpp:12:14: error: multiple overloads of 'Worker' instantiate to the same signature 'void (void (&&)(int))' explicit Worker(HandlerT&& handler) {} ^ func_pointer_ctor.cpp:17:22: note: in instantiation of template class 'Worker<void (&)(int)>' requested here Worker<HandlerT> worker{std::forward<HandlerT>(h)}; ^ func_pointer_ctor.cpp:23:5: note: in instantiation of function template specialization 'Test<void (&)(int)>' requested here Test(Func); ^ func_pointer_ctor.cpp:11:14: note: previous declaration is here explicit Worker(const HandlerT& handler) {} ^ 1 error generated.
但是把代码中的Test(Func)
换成Test([](int) {})
就没有问题。看出错信息是说当我传入一个函数时,它实例化出来的Worker
的两个构造函数是一样的。
第二次尝试 那么我专门为Func
特化一个版本好了:
1 2 3 4 5 6 7 8 9 10 11 12 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; using FuncT = void (int );template <>struct Worker <FuncT> { explicit Worker (FuncT func) {} }; ...
这里我专门为void (int)
特化了一个Worker
版本,只有一个构造函数,这回应该没问题了吧。
编译还是出错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ > g++ -std=c++11 func_pointer_ctor.cpp func_pointer_ctor.cpp:12:14: error: multiple overloads of 'Worker' instantiate to the same signature 'void (void (&&)(int))' explicit Worker(HandlerT&& handler) {} ^ func_pointer_ctor.cpp:23:22: note: in instantiation of template class 'Worker<void (&)(int)>' requested here Worker<HandlerT> worker{std::forward<HandlerT>(h)}; ^ func_pointer_ctor.cpp:29:5: note: in instantiation of function template specialization 'Test<void (&)(int)>' requested here Test(Func); ^ func_pointer_ctor.cpp:11:14: note: previous declaration is here explicit Worker(const HandlerT& handler) {} ^ 1 error generated.
看出错信息,编译器并没有实例化我们的特化版本,这说明我们特化的类型有问题。仔细看错误信息,其中有Worker<void (&)(int)>
,与我们上面用的void (int)
不同。
第三次尝试 这回我们把FuncT
的类型改为void (&)(int)
再试一下:
1 2 3 4 5 6 7 8 9 10 11 12 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; using FuncT = void (&)(int );template <>struct Worker <FuncT> { explicit Worker (FuncT func) {} }; ...
这回编译终于没有问题了。现在我想改一下调用Test
时传入的参数:
1 2 3 4 5 ... int main (int argc, char ** argv) { auto f = Func; Test (f); }
编译居然又出错了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ > g++ -std=c++11 func_pointer_ctor.cpp func_pointer_ctor.cpp:12:14: error: multiple overloads of 'Worker' instantiate to the same signature 'void (void (*&&)(int))' explicit Worker(HandlerT&& handler) {} ^ func_pointer_ctor.cpp:23:22: note: in instantiation of template class 'Worker<void (*&)(int)>' requested here Worker<HandlerT> worker{std::forward<HandlerT>(h)}; ^ func_pointer_ctor.cpp:30:5: note: in instantiation of function template specialization 'Test<void (*&)(int)>' requested here Test(f); ^ func_pointer_ctor.cpp:11:14: note: previous declaration is here explicit Worker(const HandlerT& handler) {} ^ 1 error generated.
仔细看出错信息,里面的类型和之前的不一样了!现在是Worker<void (*&)(int)>
了。
第四次尝试 这回我们再增加一种针对void (*&)(int)
的特化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; using RefFuncT = void (&)(int );template <>struct Worker <RefFuncT> { explicit Worker (RefFuncT func) {} }; using PRefFuncT = void (*&)(int );template <>struct Worker <PRefFuncT> { explicit Worker (PRefFuncT func) {} }; template <typename HandlerT>void Test (HandlerT&& h) { Worker<HandlerT> worker{std::forward<HandlerT>(h)}; } void Func (int x) {}int main (int argc, char ** argv) { auto f = Func; Test (f); Test (Func); }
编译成功,两种形式都支持。
函数的类型 Func
的类型是void (int)
,但在模板的类型推断中,传入的Func
会退化为它对应的函数指针或函数引用。
在Effective Modern C++ Item1 中讲到:
另一个会退化为指针的类型是函数。函数会退化为函数指针,且规则与数组相同。例如:
1 2 3 4 5 6 7 8 9 10 void someFunc (int , double ) ; template <typename T>void f1 (T param) ;template <typename T>void f2 (T& param) ;f1 (someFunc); f2 (someFunc);
Test
的形参类型是带引用的,因此我们在直接传入Func
时得到的HandlerT
就是void (&)(int)
。
而auto f = Func
中,根据auto
的类型推断规则,Func
会退化为函数指针,即f
的类型是void (*)(int)
,传入Test
后得到的类型是void (*&)(int)
。为什么这里多了个引用?见下节。
普适引用、引用折叠与完美转发 为了避免下面的代码太蛋疼,本小节假设有using FuncT = void (int)
。
什么是引用折叠?参见Effective Modern C++ Item28 :
普适引用是个例外,C++有单独的规则来把类型推断中出现的引用的引用转换为单个引用,称为“引用折叠”。折叠规则为:
1 2 3 4 T& & => T& T& && => T& T&& & => T& T&& && => T&&
说来你可能不信,上面的例子里出现了引用折叠。还记得我们一直在处理的编译错误是什么吗?Worker
的两个构造函数的原型相同。我们再看一下这两个构造函数:
1 2 3 4 5 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} };
你可能会觉得奇怪,它们明明是不同的引用类型,为什么最终会变成相同的样子?
当我们调用Test(Func)
时,编译器首先要实例化Test
。Func
的类型在模板类型推断时退化为函数指针,即FuncT*
,而在普适引用的类型推断中,有以下规则:
在普适引用的类型推断中,如果实参是左值,那么T
就是左值引用;如果实参是右值,那么T
没有引用,就是这个类型本身。
因此HandlerT
就是FuncT*&
。因此我们实例化了Worker<FuncT*&>
。代入到上面的两个构造函数,我们得到:
1 2 3 4 struct Worker <FuncT*&> { explicit Worker (const FuncT*& & handler) ; explicit Worker (FuncT*& && handler) ; };
再应用引用折叠,我们得到:
1 2 3 4 struct Worker <FuncT*&> { explicit Worker (const FuncT*& handler) ; explicit Worker (FuncT*& handler) ; };
一个是const引用,一个是非const引用。我们用于构造Worker
的参数h
的类型是FuncT*&
,不带const,因此编译器不知道该选择哪个函数。
如果实参是const引用 那么h
的类型就是const FuncT*&
,那么两个构造函数会变成:
1 2 3 4 struct Worker <const FuncT*&> { explicit Worker (const FuncT*& handler) ; explicit Worker (const FuncT*& handler) ; };
完全一样了!
如果实参是右值引用 根据上面的普适引用类型推断规则,你可能会认为HandlerT
是FuncT
,两个构造函数的参数类型一个是const FuncT&
,一个是FuncT&&
,就不会出错了。可是又错了(错误就不列出来了)。原因在于,一个右值引用变量本身是左值 ,因此HandlerT
的类型是FuncT&& &
,即FuncT&
。
注意看上面的规则,“如果实参是右值”而不是“如果实参是右值引用”。
如果实参是右值 这回我们终于得到了一个正确的程序,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; template <typename HandlerT>void Test (HandlerT&& h) { Worker<HandlerT> worker{std::forward<HandlerT>(h)}; } using FuncT = void (int );void Func (int x) {}FuncT* Make () { return Func; } int main (int argc, char ** argv) { Test (Make ()); }
如果不使用完美转发 如果我们不使用完美转发,即在代码中去掉std::forward
,直接使用Worker<HandlerT> worker{h}
,会发生什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} }; template <typename HandlerT>void Test (HandlerT&& h) { Worker<HandlerT> worker{h}; } using FuncT = void (int );void Func (int x) {}FuncT* Make () { return Func; } int main (int argc, char ** argv) { Test (Make ()); }
1 2 ➜ > g++ -std=c++11 func_pointer_ctor.cpp ➜ >
编译成功了。但是不是与完美转发版本有相同的效果呢?我们运行两个版本,看一下结果:
1 2 3 4 ➜ > ./perfect_forward rvalue ➜ > ./right_ref lvalue
有没有很惊讶?去掉了完美转发,我们调用的居然是const引用版本的构造函数。原因很简单,还是“右值引用本身是左值”。
所以结论是什么? 总结一下我们遇到的问题。
我们有一个模板类型Worker
,它有两个构造函数,一个接受左值引用,一个接受右值引用:
1 2 3 4 5 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) {} explicit Worker (HandlerT&& handler) {} };
然而,当模板参数为一个左值引用类型时,这两个构造函数的函数原型会产生冲突:
1 2 3 Handlert => T& const HandlerT& => const T& & => const T& HandlerT&& => T& && => T&
通常我们不会这么实例化模板类,但如果这次实例化是发生在一个完美转发函数中,众所周知,完美转发是要应用在普适引用上的,而普适引用的特性就是,如果实参是左值,形参就会被推断成左值引用。这样我们就很不幸的用左值引用类型来实例化Worker
,导致出现上面的问题。
看起来问题出在我们并不希望用一个带引用的类型来实例化Worker
,那就把引用去掉。我们可以用std::remove_reference
来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <typename HandlerT>struct Worker { explicit Worker (const HandlerT& handler) { cout << "lvalue" << endl; } explicit Worker (HandlerT&& handler) { cout << "rvalue" << endl; } }; template <typename HandlerT>void Test (HandlerT&& h) { using T = typename std::remove_reference<HandlerT>::type; Worker<T> worker{std::forward<T>(h)}; } using FuncT = void (int );void Func (int x) {}FuncT* Make () { return Func; } int main (int argc, char ** argv) { Test (Func); Test (Make ()); }
编译成功,运行一下:
1 2 3 4 ➜ > g++ -std=c++11 func_pointer_ctor.cpp ➜ > ./a.out lvalue rvalue
非常完美!所以看起来我们得到以下几个结论:
在用普适引用参数的类型构造一个模板类时,用std::remove_reference
去掉它的引用。
普适引用不是右值引用(参见区分普适引用与右值引用 ),如果要实现完美转发,记得用std::forward
。
谨慎重载普适引用,如果要重载,参考这里 ,以及,确认你真的调用了预期的重载版本。