Item 27: Familiarize yourself with alternatives to overloading on universal references.
Effective Modern C++ Item 27 的学习和解读。
在 Item26 中建议大家尽量不要对万能引用进行重载,但同时也确实存在需要对万能引用进行重载的场景。今天就和大家探索下如何满足这种场景的需求,这个 Item 将沿用上个 Item的例子,阅读本文前建议先看上一个 Item。
放弃重载
对于 Item26 中 logAndAdd 函数,为了避免万能引用实例化匹配产生的问题,一种方式就是不使用重载,取而代之的是给这些重载函数起不同的名字。
logAndAddName(...)
logAndAddNameByIdx(...)
这种方法虽然在一定程度上可以解决这个问题,但是对于构造函数,就无能为力了(构造函数函数名是固定的)。
const T& 传递
另一种选择是不采用万能引用传参(pass-by-universal-reference),使用 const T& 传参。这也是 Item26 开始就介绍的方法,但是它的缺点是效率不高。
值传递
直接选择传值,这种方式将在 Item41 中继续讨论,这里只介绍这种方法的使用:
class Person {
public:
explicit Person(std::string n)
: name(std::move(n)) {}
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
std::string 的构造函数没有传 int 的版本,所有 int 类型和类似 int 类型(int,short,size_t,long)参数的传递给构造函数都将匹配到 int 类型重载构造函数。类似的,所有 std::string 类型和类似 std::string 类型(比如字面的"Ruth")的参数传递给构造函数都将匹配到 std::string 类型重载构造函数。
使用 Tag 分发
const T& 传递和值传递都不支持完美转发。如果使用万能引用的动机是为了完美转发,那还必须只能使用万能引用,并且那你也不想放弃重载,这里介绍一种使用 Tag 分发的方法。
基于 Tag 分发其实就是使用 Tag 对参数进行区分,进而分发到不同的函数实现。对于上个 Item 中的例子:
std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
使用 Tag 分发的实现如下:
template<typename T>
void logAndAddImpl(T&& name, std::false_type)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
void logAndAddImpl(int idx, std::true_type)
{
logAndAdd(nameFromIdx(idx));
}
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
);
}
C++11 引入的 std::is_integral<T> 可以判断参数类型是否为整形。在这个例子中,如果 logAndAdd 传入的是左值类型的 int,T将被推导成 int&,但不是 int,为了解决这个问题,使用 std::remove_reference 去除引用。
从概念上讲,logAndAdd 传递了一个布尔值给 logAndAddImpl,表示传递的实参是否为整形。但是我们知道 true 和 false 都是运行时的值,而模板匹配是编译阶段的事情。C++ 标准库提供了 std::true_type 和 std::false_type 两种类型代表 true 和 false 的含义。如果 T 是整形,那么 logAndAdd 传递给 logAndAddImpl 的参数是一个继承了 std::true_type 的对象,否则是一个继承了std::false_type 的对象。
约束接受万能引用的模板
使用 Tag 分发的技术,是在通用引用参数函数内部根据参数类型进行分发,它解决不了 Item26 中介绍的 Person 完美转发构造函数的问题。如果你在一个构造函数内部实现 Tag 分发,但是编译器在一些情况下会自动生成构造函数,将会绕过使用 Tag 分发的构造函数。
问题不在于编译器生成的构造函数会绕过使用 Tag 分发的完美转发构造函数,而是从来没有绕过。例如你想用一个左值的对象去初始化一个新的对象,你想调用的是编译器生成的拷贝构造函数,但是正如 Item26 介绍的那样,实际上调用完美转发的构造函数。
万能引用的匹配重载函数总是贪婪的,我们需要另外一种技术控制万能引用调用的条件,那就是 std::enable_if。
默认条件下,所有模板都是 enable 的,当使用了 std::enable_if 后,只有满足条件的模板才是 enable 的。语法规则是这样的:
class Person {
public:
template<typename T,
typename = typename std::enable_if<condition>::type>
explicit Person(T&& n);
.....
};
只有满足了 condition 条件才使能。我们期望不是 Person 类型的参数,模板构造函数才使能,当然我们可以使用 is_same 来判断类型是否相同,因而我们的条件可能是 !std::is_same<Person, T>::value。但是这里会有点小问题,比如 Person 和 Person& 不是一个类型,而我们这里显然不希望 Person& 类型满足条件进而使能模板。
- 对于引用。我们期望 Person& 和 Person&& 都像 Person 一样处理,即不使能模板。
- 对于 const 和 volatile,即 CV 描述符。我们期望 const Person、volatile Person 和 volatile const Person 也能像 Person 一样处理,即不使能模板。
标准库为我们提供了 std::decay,std::decay<T>::type 的类型 和 T 的类型相同,它忽略了引用和 CV 描述符。因此我们想控制模板使能的条件是:
!std::is_same<Person, typename std::decay<T>::type>::value
这样就可以得到我们想要的实现:
class Person {
public:
template<
typename T,
typename = typename std::enable_if<
!std::is_same<Person,
typename std::decay<T>::type
>::value
>::type
>
explicit Person(T&& n);
…
};
再看 Item26 中万能引用重载在遇到类继承的问题:
class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs)
: Person(rhs)
{ … }
SpecialPerson(SpecialPerson&& rhs)
: Person(std::move(rhs))
{ … }
};
当我们拷贝或移动一个 SpecialPerson 对象,我们期望调用基类的拷贝或移动构造函数,但是我们传递给基类的是 SpecialPerson 类型的参数,会匹配到基类的完美转发构造函数。
标准库 type trait 提供了 std::is_base_of 帮我们解决这个问题。如果 T2 继承于 T1,那么 std::is_base_of<T1, T2>::value 为 true,并且 std::is_base_of<T, T>::value 也是 true。上面的代码使用 std::is_base_of 代替 is_same 得到的代码将更加合适:
class Person {
public:
template<
typename T,
typename = typename std::enable_if<
!std::is_base_of<Person,
typename std::decay<T>::type
>::value
>::type
>
explicit Person(T&& n);
…
};
如果使用 C++14 实现将更加简洁:
class Person {
public:
template<
typename T,
typename = typename std::enable_if_t<
!std::is_base_of<Person,
std::decay_t<T>::type
>::value
>
>
explicit Person(T&& n);
…
};
到目前为止,我们已经接近了完美解决了 Item26 中介绍的万能引用模板重载的问题。再加上处理整数参数类型的 Person 的重载,我们汇总代码如下:
class Person {
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n)
: name(std::forward<T>(n))
{ … }
explicit Person(int idx)
: name(nameFromIdx(idx))
{ … }
…
private:
std::string name;
};
权衡
本 Item 介绍的后两种技术:使用 Tag 分发和限制模板使能条件,都支持了完美转发。但使用完美转发也有缺点:
一个是有些类型不能完美转发,这个将在 Item30 中讨论。另外一个是当用户传递无效参数时,编译报错信息的可读性非常差。
例如,在创建 Person 对象的时候传递了个char16_t(C++11引进的一种以16位表示一个字符的类型)字符组成的字符串,而不是char:
Person p(u"Konrad Zuse");
当使用本 Item 的前三种技术时,编译器看到可执行的构造函数只接受 int 和 std::string,编译器会产生一些直观的错误信息表明:无法将 const char16_t[12] 转换到 int 或 std::string。
万能引用在接受 char16_t 类型时候没有问题,当构造函数把 char16_t 类型数组转发到 std::string 成员变量的构造中时,才发现 char16_t 数组不是 std::string 可接受的参数类型,我使用 g++ 的报错信息如下:
hello.cpp: In instantiation of ‘Person::Person(T&&) [with T = const char16_t (&)[12]; <template-parameter-1-2> = void]’:
hello.cpp:30:26: required from here
hello.cpp:18:28: error: no matching function for call to ‘std::__cxx11::basic_string<char>::basic_string(const char16_t [12])’
: name(std::forward<T>(n)) // args convertible to
^
In file included from /usr/include/c++/7/string:52:0,
from /usr/include/c++/7/bits/locale_classes.h:40,
from /usr/include/c++/7/bits/ios_base.h:41,
from /usr/include/c++/7/ios:42,
from /usr/include/c++/7/ostream:38,
from /usr/include/c++/7/iostream:39,
from hello.cpp:1:
/usr/include/c++/7/bits/basic_string.h:604:9: note: candidate: template<class _InputIterator, class> std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(_InputIterator, _InputIterator, const _Alloc&)
basic_string(_InputIterator __beg, _InputIterator __end,
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:604:9: note: template argument deduction/substitution failed:
hello.cpp:18:28: note: candidate expects 3 arguments, 1 provided
: name(std::forward<T>(n)) // args convertible to
^
In file included from /usr/include/c++/7/string:52:0,
from /usr/include/c++/7/bits/locale_classes.h:40,
from /usr/include/c++/7/bits/ios_base.h:41,
from /usr/include/c++/7/ios:42,
from /usr/include/c++/7/ostream:38,
from /usr/include/c++/7/iostream:39,
from hello.cpp:1:
/usr/include/c++/7/bits/basic_string.h:566:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&&, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(basic_string&& __str, const _Alloc& __a)
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:566:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:562:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(const basic_string& __str, const _Alloc& __a)
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:562:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:558:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::initializer_list<_Tp>, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:558:7: note: no known conversion for argument 1 from ‘const char16_t [12]’ to
std::initializer_list<char>’
/usr/include/c++/7/bits/basic_string.h:531:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(basic_string&& __str) noexcept
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:531:7: note: no known conversion for argument 1 from ‘const char16_t [12]’ to
std::__cxx11::basic_string<char>&&’
/usr/include/c++/7/bits/basic_string.h:519:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, _CharT, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type = long unsigned int]
basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:519:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:509:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:509:7: note: no known conversion for argument 1 from ‘const char16_t [12]’ to
const char*’
/usr/include/c++/7/bits/basic_string.h:499:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type = long unsigned int]
basic_string(const _CharT* __s, size_type __n,
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:499:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:481:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type = long unsigned int]
basic_string(const basic_string& __str, size_type __pos,
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:481:7: note: candidate expects 4 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:465:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type = long unsigned int]
basic_string(const basic_string& __str, size_type __pos,
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:465:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:450:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::size_type = long unsigned int]
basic_string(const basic_string& __str, size_type __pos,
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:450:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/7/bits/basic_string.h:437:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(const basic_string& __str)
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:437:7: note: no known conversion for argument 1 from ‘const char16_t [12]’ to
const std::__cxx11::basic_string<char>&’
/usr/include/c++/7/bits/basic_string.h:429:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string(const _Alloc& __a) _GLIBCXX_NOEXCEPT
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:429:7: note: no known conversion for argument 1 from ‘const char16_t [12]’ to
const std::allocator<char>&’
/usr/include/c++/7/bits/basic_string.h:420:7: note: candidate: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string() [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]
basic_string()
^~~~~~~~~~~~
/usr/include/c++/7/bits/basic_string.h:420:7: note: candidate expects 0 arguments, 1 provided
如果完美转发多次,错误信息将更加迷惑。std::is_constructible 可以在编译期间测试一个类型的对象是否能被另一个不同类型(或一些不同类型)的对象(或者另一些对象)构造,我们可以使用 static_assert 断言来实现:
class Person {
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n)
: name(std::forward<T>(n))
{
static_assert(
std::is_constructible<std::string, T>::value,
"Parameter n can't be used to construct a std::string"
);
…
}
explicit Person(int idx)
: name(nameFromIdx(idx))
{ … }
…
private:
std::string name;
};
g++ 编译报错如下:
/usr/include/c++/7/bits/basic_string.h:420:7: note: candidate expects 0 arguments, 1 provided
hello.cpp:20:6: error: static assertion failed: Parameter n can't be used to construct a std::string
static_assert(
^~~~~~~~~~~~~
至此,我们我们已经完美解决了 Item26 中介绍的万能引用模板重载的问题。
|