• c++11-17 模板核心知识(八)—— enable_if<>与SFINAE


    引子

    class Person {
    private:
      std::string name;
    
    public:
      // generic constructor for passed initial name:
      template <typename STR>
      explicit Person(STR &&n) : name(std::forward<STR>(n)) {
        std::cout << "TMPL-CONSTR for '" << name << "'
    ";
      }
    
      // copy and move constructor:
      Person(Person const &p) : name(p.name) {
        std::cout << "COPY-CONSTR Person '" << name << "'
    ";
      }
    
      Person(Person &&p) : name(std::move(p.name)) {
        std::cout << "MOVE-CONSTR Person '" << name << "'
    ";
      }
    };
    

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

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

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

    Person p3(p1);    // ERROR
    

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

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

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

    template<typename STR>
    Person(STR&& n)
    

    会优于copy constructor

    Person (Person const& p)
    

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

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

    使用enable_if<>禁用模板

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

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

    void foo() {
    }
    

    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<>传递第二个参数的例子:

    template<typename T>
    std::enable_if_t<(sizeof(T) > 4), T>
    foo() {
    return T();
    }
    

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

    MyType foo();
    

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

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

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

    template<typename T,
    typename = void>
    void foo() {
    }
    

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

    template<typename T>
    using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
    
    template<typename T,
    typename = EnableIfSizeGreater4<T>>
    void foo() {
    }
    

    enable_if<>实例

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

    template <typename T>
    using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>;
    
    class Person {
    private:
      std::string name;
    
    public:
      // generic constructor for passed initial name:
      template <typename STR, typename = EnableIfString<STR>>
      explicit Person(STR &&n) : name(std::forward<STR>(n)) {
        std::cout << "TMPL-CONSTR for '" << name << "'
    ";
      }
    
      // copy and move constructor:
      Person(Person const &p) : name(p.name) {
        std::cout << "COPY-CONSTR Person '" << name << "'
    ";
      }
      Person(Person &&p) : name(std::move(p.name)) {
        std::cout << "MOVE-CONSTR Person '" << name << "'
    ";
      }
    };
    

    核心点:

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

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

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

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

    • 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.

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

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

    template<typename T>
    concept ConvertibleToString = std::is_convertible_v<T,std::string>;
    
    ...
    template<typename STR>
    requires ConvertibleToString<STR>
    Person(STR&& n) : name(std::forward<STR>(n)) {
    ...
    }
    

    甚至可以改为:

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

    SFINAE (Substitution Failure Is Not An Error)

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

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

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

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

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

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

    考虑下面的例子:

    // number of elements in a raw array:
    template <typename T, unsigned N> 
    std::size_t len(T (&)[N]) { 
      return N; 
    }
    
    // number of elements for a type having size_type:
    template <typename T> 
    typename T::size_type len(T const &t) { 
      return t.size(); 
    }
    

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

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

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

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

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

    std::allocator<int> x;
    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成员函数,那么就忽略该模板。之前的定义为:

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

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

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

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

    比如:

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

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

    (完)

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

  • 相关阅读:
    反转链表 --剑指offer
    链表的倒数第K个节点
    打印1到最大的n位数----java实现
    Permutations java实现
    Generate Parentheses java实现
    Binary Tree Level Order Traversal java实现
    hadoop中日志聚集问题
    PIG的配置
    hadoop2.20.0集群安装教程
    Map/Reduce之间的Partitioner接口
  • 原文地址:https://www.cnblogs.com/zhangyachen/p/14025038.html
Copyright © 2020-2023  润新知