引子

  1. class Person {
  2. private:
  3. std::string name;
  4. public:
  5. // generic constructor for passed initial name:
  6. template <typename STR>
  7. explicit Person(STR &&n) : name(std::forward<STR>(n)) {
  8. std::cout << "TMPL-CONSTR for '" << name << "'\n";
  9. }
  10. // copy and move constructor:
  11. Person(Person const &p) : name(p.name) {
  12. std::cout << "COPY-CONSTR Person '" << name << "'\n";
  13. }
  14. Person(Person &&p) : name(std::move(p.name)) {
  15. std::cout << "MOVE-CONSTR Person '" << name << "'\n";
  16. }
  17. };

构造函数是一个perfect forwarding,所以:

  1. std::string s = "sname";
  2. Person p1(s); // init with string object => calls TMPL-CONSTR
  3. Person p2("tmp"); // init with string literal => calls TMPL-CONSTR

但是当尝试调用copy constructor时会报错:

  1. Person p3(p1); // ERROR

但是如果参数是const Person或者move constructor则正确:

  1. Person const p2c("ctmp"); // init constant object with string literal
  2. Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
  3. Person p4(std::move(p1)); // OK: move Person => calls MOVE-CONST

原因是:根据c++的重载规则,对于一个nonconstant lvalue Person p,member template

  1. template<typename STR>
  2. Person(STR&& n)

会优于copy constructor

  1. Person (Person const& p)

因为STR会直接被substituted为Person&,而copy constructor还需要一次const转换。

也许提供一个nonconstant copy constructor会解决这个问题,但是我们真正想做的是当参数是Person类型时,禁用掉member template。这可以通过std::enable_if<>来实现。

使用enable_if<>禁用模板

  1. template<typename T>
  2. typename std::enable_if<(sizeof(T) > 4)>::type
  3. foo() {
  4. }

sizeof(T) > 4为False时,该模板就会被忽略。如果sizeof(T) > 4为true时,那么该模板会被扩展为:

  1. void foo() {
  2. }

std::enable_if<>是一种类型萃取(type trait),会根据给定的一个编译时期的表达式(第一个参数)来确定其行为:

  • 如果这个表达式为true,std::enable_if<>::type会返回:

    • 如果没有第二个模板参数,返回类型是void。
    • 否则,返回类型是其第二个参数的类型。
  • 如果表达式结果false,std::enable_if<>::type不会被定义。根据下面会介绍的SFINAE(substitute failure is not an error),

    这会导致包含std::enable_if<>的模板被忽略掉。

给std::enable_if<>传递第二个参数的例子:

  1. template<typename T>
  2. std::enable_if_t<(sizeof(T) > 4), T>
  3. foo() {
  4. return T();
  5. }

如果表达式为真,那么模板会被扩展为:

  1. MyType foo();

如果你觉得将enable_if<>放在声明中有点丑陋的话,通常的做法是:

  1. template<typename T,
  2. typename = std::enable_if_t<(sizeof(T) > 4)>>
  3. void foo() {
  4. }

sizeof(T) > 4时,这会被扩展为:

  1. template<typename T,
  2. typename = void>
  3. void foo() {
  4. }

还有种比较常见的做法是配合using:

  1. template<typename T>
  2. using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
  3. template<typename T,
  4. typename = EnableIfSizeGreater4<T>>
  5. void foo() {
  6. }

enable_if<>实例

我们使用enable_if<>来解决引子中的问题:

  1. template <typename T>
  2. using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>;
  3. class Person {
  4. private:
  5. std::string name;
  6. public:
  7. // generic constructor for passed initial name:
  8. template <typename STR, typename = EnableIfString<STR>>
  9. explicit Person(STR &&n) : name(std::forward<STR>(n)) {
  10. std::cout << "TMPL-CONSTR for '" << name << "'\n";
  11. }
  12. // copy and move constructor:
  13. Person(Person const &p) : name(p.name) {
  14. std::cout << "COPY-CONSTR Person '" << name << "'\n";
  15. }
  16. Person(Person &&p) : name(std::move(p.name)) {
  17. std::cout << "MOVE-CONSTR Person '" << name << "'\n";
  18. }
  19. };

核心点:

  • 使用using来简化std::enable_if<>在成员模板函数中的写法。
  • 当构造函数的参数不能转换为string时,禁用该函数。

所以下面的调用会按照预期方式执行:

  1. int main() {
  2. std::string s = "sname";
  3. Person p1(s); // init with string object => calls TMPL-CONSTR
  4. Person p2("tmp"); // init with string literal => calls TMPL-CONSTR
  5. Person p3(p1); // OK => calls COPY-CONSTR
  6. Person p4(std::move(p1)); // OK => calls MOVE-CONST
  7. }

注意在不同版本中的写法:

  • C++17 : using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>
  • C++14 : using EnableIfString = std::enable_if_t<std::is_convertible<T, std::string>::value>
  • C++11 : using EnableIfString = typename std::enable_if<std::is_convertible<T, std::string>::value>::type

使用Concepts简化enable_if<>

如果你还是觉得enable_if<>不够直观,那么可以使用之前文章提到过的C++20引入的Concept.

  1. template<typename STR>
  2. requires std::is_convertible_v<STR,std::string>
  3. Person(STR&& n) : name(std::forward<STR>(n)) {
  4. ...
  5. }

我们也可以将条件定义为通用的Concept:

  1. template<typename T>
  2. concept ConvertibleToString = std::is_convertible_v<T,std::string>;
  3. ...
  4. template<typename STR>
  5. requires ConvertibleToString<STR>
  6. Person(STR&& n) : name(std::forward<STR>(n)) {
  7. ...
  8. }

甚至可以改为:

  1. template<ConvertibleToString STR>
  2. Person(STR&& n) : name(std::forward<STR>(n)) {
  3. ...
  4. }

SFINAE (Substitution Failure Is Not An Error)

在C++中针对不同参数类型做函数重载时很常见的。编译器需要为一个调用选择一个最适合的函数。

当这些重载函数包含模板函数时,编译器一般会执行如下步骤:

  • 确定模板参数类型。
  • 将函数参数列表和返回值的模板参数替换掉(substitute)
  • 根据规则决定哪一个函数最匹配。

但是替换的结果可能是毫无意义的。这时,编译器不会报错,反而会忽略这个函数模板。

我们将这个原则叫做:SFINAE(“substitution failure is not an error)

但是替换(substitute)和实例化(instantiation)不一样:即使最终不需要被实例化的模板也要进行替换(不然就无法执行上面的第3步)。不过它只会替换直接出现在函数声明中的相关内容(不包含函数体)。

考虑下面的例子:

  1. // number of elements in a raw array:
  2. template <typename T, unsigned N>
  3. std::size_t len(T (&)[N]) {
  4. return N;
  5. }
  6. // number of elements for a type having size_type:
  7. template <typename T>
  8. typename T::size_type len(T const &t) {
  9. return t.size();
  10. }

当传递一个数组或者字符串时,只有第一个函数模板匹配,因为T::size_type导致第二个模板函数会被忽略:

  1. int a[10];
  2. std::cout << len(a); // OK: only len() for array matches
  3. std::cout << len("tmp"); // OK: only len() for array matches

同理,传递一个vector会只有第二个函数模板匹配:

  1. std::vector<int> v;
  2. std::cout << len(v); // OK: only len() for a type with size_type matches

注意,这与传递一个对象,有size_type成员,但是没有size()成员函数不同。例如:

  1. std::allocator<int> x;
  2. std::cout << len(x); // ERROR: len() function found, but can’t size()

编译器会根据SFINAE原则匹配到第二个函数,但是编译器会报找不到std::allocator<int>的size()成员函数。在匹配过程中不会忽略第二个函数,而是在实例化的过程中报错。

而使用enable_if<>就是实现SFINAE最直接的方式。

SFINAE with decltype

有的时候想要为模板定义一个合适的表达式是比较难得。

比如上面的例子,假如参数有size_type成员但是没有size成员函数,那么就忽略该模板。之前的定义为:

  1. template<typename T>
  2. typename T::size_type len (T const& t) {
  3. return t.size();
  4. }
  5. std::allocator<int> x;
  6. std::cout << len(x) << '\n'; // ERROR: len() selected, but x has no size()

这么定义会导致编译器选择该函数但是会在instantiation阶段报错。

处理这种情况一般会这么做:

  • 通过trailing return type来指定返回类型 (auto -> decltype)
  • 将所有需要成立的表达式放在逗号运算符的前面。
  • 在逗号运算符的最后定义一个类型为返回类型的对象。

比如:

  1. template<typename T>
  2. auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() ) {
  3. return t.size();
  4. }

这里,decltype的参数是一个逗号表达式,所以最后的T::size_type()为函数的返回值类型。逗号前面的(void)(t.size())必须成立才可以。

(完)

朋友们可以关注下我的公众号,获得最及时的更新:

c++11-17 模板核心知识(八)—— enable_if<>与SFINAE的更多相关文章

  1. c++11-17 模板核心知识(十一)—— 编写泛型库需要的基本技术

    Callables 函数对象 Function Objects 处理成员函数及额外的参数 std::invoke<>() 统一包装 泛型库的其他基本技术 Type Traits std:: ...

  2. c++11-17 模板核心知识(十二)—— 模板的模板参数 Template Template Parameters

    概念 举例 模板的模板参数的参数匹配 Template Template Argument Matching 解决办法一 解决办法二 概念 一个模板的参数是模板类型. 举例 在c++11-17 模板核 ...

  3. c++11-17 模板核心知识(十五)—— 解析模板之依赖型类型名称与typename Dependent Names of Types

    模板名称的问题及解决 typename规则 C++20 typename 上篇文章c++11-17 模板核心知识(十四)-- 解析模板之依赖型模板名称 Dependent Names of Templ ...

  4. c++11-17 模板核心知识(十四)—— 解析模板之依赖型模板名称(.template/->template/::template)

    tokenization与parsing 解析模板之类型的依赖名称 Dependent Names of Templates Example One Example Two Example Three ...

  5. c++11-17 模板核心知识(二)—— 类模板

    类模板声明.实现与使用 Class Instantiation 使用类模板的部分成员函数 Concept 友元 方式一 方式二 类模板的全特化 类模板的偏特化 多模板参数的偏特化 默认模板参数 Typ ...

  6. c++11-17 模板核心知识(三)—— 非类型模板参数 Nontype Template Parameters

    类模板的非类型模板参数 函数模板的非类型模板参数 限制 使用auto推断非类型模板参数 模板参数不一定非得是类型,它们还可以是普通的数值.我们仍然使用前面文章的Stack的例子. 类模板的非类型模板参 ...

  7. c++11-17 模板核心知识(一)—— 函数模板

    1.1 定义函数模板 1.2 使用函数模板 1.3 两阶段翻译 Two-Phase Translation 1.3.1 模板的编译和链接问题 1.4 多模板参数 1.4.1 引入额外模板参数作为返回值 ...

  8. c++11-17 模板核心知识(五)—— 理解模板参数推导规则

    Case 1 : ParamType是一个指针或者引用,但不是universal reference T& const T& T* Case 2 : ParamType是Univers ...

  9. c++11-17 模板核心知识(九)—— 理解decltype与decltype(auto)

    decltype介绍 为什么需要decltype decltype(auto) 注意(entity) 与模板参数推导和auto推导一样,decltype的结果大多数情况下是正常的,但是也有少部分情况是 ...

随机推荐

  1. php生成签名

    // 生成签名private function makeSignature($params){ foreach ($params as $key=>$value){ $arr[$key] = $ ...

  2. Python基础数据类型及其转换

    数据类型: 浮点型float: 3.1415 也就是小数 整型 int: 123,主要用来运算,+-*/ 字符串 str: 'abc?', 记录少量信息 布尔值 bool: True or False ...

  3. Cypress系列(68)- request() 命令详解

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 作用 发起一个 HTTP 请求 语法格式 ...

  4. 分布式文件存储数据库 MongoDB

    MongoDB 简介 Mongo 并非芒果(Mango)的意思,而是源于 Humongous(巨大的:庞大的)一词. MongoDB 是一个基于分布式文件存储的 NoSQL 数据库.由 C++ 语言编 ...

  5. byte + byte = int

    byte+byte=int,低级向高级是隐式类型转换,高级向低级必须强制类型转换,byte<char<short<int<long<float<double

  6. webpack4的安装使用

    1.全局安装Webpack 安装到全局后你可以在任何地方共用一个 Webpack 可执行文件( 也就是说可以直接在终端使用webpack的命名 ,例如:webpack --config webpack ...

  7. B站:我是程序汪:电话面试(待更新)

    电话面试: 商城: 1.spring bean的生命周期你可以简单描述一下吗? 能记得几个接口名吗? 2.springMVC的处理流程 3.项目出现生产问题,排查日志有什么方法吗,思路,大概说一下 4 ...

  8. 实用!8个 chrome插件玩转GitHub,单个文件下载小意思

    作为程序员对 GitHub 应该都不会陌生,我经常沉迷其中,找一些惊艳的项目或者工具.不过用的时间久了,发现它的用户体验实在是不敢恭维,有时候会让你做很多重复操作,浪费不少时间. 比如我想单独下载一个 ...

  9. 解决Linux-Centos7启动Mysql服务失败丢失mysql.sock问题

    在新安装mysql后进行启动发现报错 mysql启动服务命令 systemctl start mysqld@3306 Starting mysqld (via systemctl):  Job for ...

  10. 一个Task.Factory.StartNew的错误用法

    同事写了这样一段代码: FactoryStartNew类: using System; using System.Collections.Generic; using System.Linq; usi ...