美文网首页C++ Templates
【C++ Templates(10)】泛型库

【C++ Templates(10)】泛型库

作者: downdemo | 来源:发表于2018-04-28 13:49 被阅读38次

    Callable

    • 标准库定义了许多可调用实例的组件,这里有一个术语叫回调(callback),回调的含义是:对一个库,客户端希望库能够调用客户端自定义的某些函数,这种调用称为回调,比如一个排序函数可能包含一个回调参数作为排序规则。回调的概念原本是为仿函数保留的,仿函数通常以函数调用实参的形式传递给库
    • C++中有几种用于回调的类型,它们能直接使用语法f(...)作为函数调用实参传递,这些类型统称为函数对象类型,这种类型的一个值就是一个函数对象
      • 函数指针
      • 带有重载operator()的类类型(有时叫functor)
      • 带有产生一个函数指针或函数引用的转换函数的类类型

    支持函数对象

    • 先看看标准库中for_each()算法实现
    // basics/foreach.hpp
    
    template<typename Iter, typename Callable>
    void foreach (Iter current, Iter end, Callable op)
    {
        while (current != end) { //as long as not reached the end
            op(*current); // call passed operator for current element
            ++current; // and move iterator to next element
        }
    }
    
    • 下面展示了对各种函数对象使用这个模板的方法
    // basics/foreach.cpp
    
    #include <iostream>
    #include <vector>
    #include "foreach.hpp"
    
    // a function to call:
    void func(int i)
    {
        std::cout << "func() called for: " << i << '\n';
    }
    
    // a function object type (for objects that can be used as functions):
    class FuncObj {
    public:
        void operator() (int i) const {  // Note: const member function
            std::cout << "FuncObj::op() called for: " << i << '\n';
        }
    };
    
    int main()
    {
        std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
        foreach(primes.begin(), primes.end(),  // range
              func);                         // function as callable (decays to pointer)
    
        foreach(primes.begin(), primes.end(),  // range
              &func);                        // function pointer as callable
    
        foreach(primes.begin(), primes.end(),  // range
              FuncObj());                    // function object as callable
    
        foreach(primes.begin(), primes.end(),  // range
              [] (int i) {                   // lambda as callable
                std::cout << "lambda called for: " << i << '\n';
              });
    }
    
    • 将函数名称作为函数实参传递时,传递的其实是它的指针或引用,就像数组一样,函数可以被不衰退地传递为引用,然而函数类型不能被const修饰,如果用Callable const&类型,const会被忽略
    • 第二个显式指定指针的调用与第一个等价(函数名称隐式衰退为指针值)但可能更清晰
    • 传递仿函数时,传递的是一个类类型对象作为回调,调用一个类类型通常是调用它的operator(),注意定义operator()时应该将其定义为const成员函数
    op(*current);
    // 通常会转换为
    op.operator()(*current); // call operator() with parameter *current for op
    
    • 一个类类型对象也可能饮食转换为一个代理调用函数的指针或引用,下面的F是类类型对象能转换为的函数指针或函数引用类型,这是很不寻常的
    op(*current);
    // 将转换为
    (op.operator F())(*current);
    
    • C++11开始,可以方便地用lambda表达式产生仿函数(被称为闭包)。有趣的是,[]开头(没有捕获)的lambda产生一个函数指针的转换运算符,但总不会被选为代理调用函数,因为它在匹配时总是被闭包的operator()更差

    处理成员函数和附加实参

    • 之前的例子中没有用到成员函数,因为通常调用一个non-static成员函数只要直接X.f(...),这不符合函数对象的模式
    • C++17开始,标准库提供了std::invoke(),方便地将这种情况与普通函数调用语法结合
    // basics/foreachinvoke.hpp
    
    #include <utility>
    #include <functional>
    template<typename Iter, typename Callable, typename... Args>
    void foreach (Iter current, Iter end, Callable op, Args const&...
    args)
    {
        while (current != end) {
            std::invoke(op, //call passed callable with
            args..., // any additional args
            *current); // and the current element
            ++current;
        }
    }
    
    • 这里除了callable参数,也能接收任意数量的附加参数。如果callable是一个指向成员的指针,使用第一个附加实参作为this对象,其余所有的附加参数都只作为实参传递给callable
    • 否则,所有的附加参数只被作为实参传递给callable
    • 注意,这里不能为callable或附加参数使用完美转发,第一个调用可能窃取它们的值,导致随后迭代调用op的非预期行为
    • 使用这个实现
    #include <iostream>
    #include <vector>
    #include <string>
    #include "foreachinvoke.hpp"
    
    // a class with a member function that shall be called
    class MyClass {
    public:
        void memfunc(int i) const {
            std::cout << "MyClass::memfunc() called for: " << i << '\n';
        }
    };
    
    int main()
    {
        std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    
        // pass lambda as callable and an additional argument:
        foreach(primes.begin(), primes.end(),          // elements for 2nd arg of lambda
            [](std::string const& prefix, int i) { // lambda to call
                std::cout << prefix << i << '\n';
            },
            "- value: ");                          // 1st arg of lambda
        
        // call obj.memfunc() for/with each elements in primes passed as argument
        MyClass obj;
        foreach(primes.begin(), primes.end(),  // elements used as args
            &MyClass::memfunc,             // member function to call
            obj);                          // object to call memfunc() for
    }
    

    包裹函数调用

    • std::invoke()的一个常见应用是包裹单个函数调用,现在可以通过完美转发callable和所有传递的实参支持移动语义
    #include <utility>     // for std::invoke()
    #include <functional>  // for std::forward()
    
    template<typename Callable, typename... Args>
    decltype(auto) call(Callable&& op, Args&&... args)
    {
        return std::invoke(std::forward<Callable>(op),    // passed callable with
            std::forward<Args>(args)...);  // any additional args
    }
    
    • 另一个有趣的方面是如何处理被调用函数的返回值,完美转发给调用者,为了支持返回引用(如std::ostream&),使用decltype(auto)代替auto
    template<typename Callable, typename... Args>
    decltype(auto) call(Callable&& op, Args&&... args)
    
    • 如果想临时存储std::invoke()返回的值,也必须用decltype(auto)声明临时变量
    decltype(auto) ret{std::invoke(std::forward<Callable>(op),
    std::forward<Args>(args)...)};
    ...
    return ret;
    
    • 注意,把ret声明为auto&&是不正确的,auto&&作为一个引用,生命周期不会超出return语句
    • 但使用decltype(auto)也有一个问题,如果callable有void的返回类型,把ret初始化为decltype(auto)是不允许的,因为void是一个不完整的类型
    • 一个解决方法是在那条语句之前声明一个对象,该对象的析构函数执行你希望实现的可观察的行为
    struct cleanup {
        ~cleanup() {
            ... // code to perform on return
        }
    } dummy;
    return std::invoke(std::forward<Callable>(op),
        std::forward<Args>(args)...);
    
    • 另一个方法是使用if constexpr不同地实现void和non-void
    #include <utility>     // for std::invoke()
    #include <functional>  // for std::forward()
    #include <type_traits> // for std::is_same<> and invoke_result<>
    
    template<typename Callable, typename... Args>
    decltype(auto) call(Callable&& op, Args&&... args)
    {
        if constexpr(std::is_same_v<std::invoke_result_t<Callable, Args...>, void>) {
            // return type is void:
            std::invoke(std::forward<Callable>(op),
                std::forward<Args>(args)...);
            //...
            return;
        }
        else {
            // return type is not void:
            decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                std::forward<Args>(args)...)};
            // ...
            return ret;
        }
    }
    

    其他实现泛型库的实用工具

    Type Traits

    • 标准库提供了称为type traits的各种实用工具,允许评估和修饰类型
    #include <type_traits>
    
    template<typename T>
    class C
    {
        // ensure that T is not void (ignoring const or volatile):
        static_assert(!std::is_same_v<std::remove_cv_t<T>,void>,
            "invalid instantiation of class C for void type");
    public:
        template<typename V>
        void f(V&& v) {
            if constexpr(std::is_reference_v<T>) {
                ... // special code if T is a reference type
            }
            if constexpr(std::is_convertible_v<std::decay_t<V>,T>) {
                ... // special code if V is convertible to T
            }
            if constexpr(std::has_virtual_destructor_v<V>) {
                ... // special code if V has virtual destructor
            }
        }
    };
    
    • 然而,注意type traits使用时可能与预期表现不符
    std::remove_const_t<int const&> // yields int const&
    
    • 这里是引用不是const,所以调用没有效果,因此移除引用和const的顺序是
    std::remove_const_t<std::remove_reference_t<int
    const&>> // int
    std::remove_reference_t<std::remove_const_t<int
    const&>> // int const
    
    • 也可以直接调用
    std::decay_t<int const&> // yields int
    
    • 但会转化原始数组和函数为对应的指针类型
    • 也会有type traits有要求的情况,不满足要求会导致未定义行为
    make_unsigned_t<int> // unsigned int
    make_unsigned_t<int const&> // undefined behavior (hopefully error)
    
    • 有时这些结果可能出乎意料
    add_rvalue_reference_t<int> // int&&
    add_rvalue_reference_t<int const> // int const&&
    add_rvalue_reference_t<int const&> // int const& (lvalueref
    remains lvalue-ref)
    
    • 上例中因为引用折叠规则,产生了左值引用。另一个例子如下
    is_copy_assignable_v<int> // yields true (generally, you can assign an int to an int)
    is_assignable_v<int, int> // yields false (can't call 42 = 42)
    
    • is_copy_assignable只检查能否把int赋给另一个(检查左值操作),is_assignable则考虑到值类型(这里检查能否把右值赋给右值),因此第一个表达式等价于
    is_assignable_v<int&,int&> // yields true
    
    • 同样的原因
    is_swappable_v<int> // yields true (assuming lvalues)
    is_swappable_v<int&,int&> // yields true (equivalent to the previous check)
    is_swappable_with_v<int,int> // yields false (taking value category into account)
    

    std::addressof()

    • std::addressof<>()函数模板产生一个函数或对象的地址,即使对象类型有一个重载运算符&,因此如果需要任何类型的对象的地址都推荐使用std::addressof()
    template<typename T>
    void f (T&& x)
    {
        auto p = &x; // might fail with overloaded operator &
        auto q = std::addressof(x); // works even with overloaded operator &
        ...
    }
    

    std::declval()

    • std::declval<>()函数模板能用于一个具体类型的对象的引用,这个函数没有定义因此不能被调用(并且不创建一个对象),因此只能被未评估的操作数(如decltype和sizeof构造)
    • 比如下面的声明从T1和T2推断默认返回类型RT,为了避免调用T1和T2的构造函数,使用std::declval获取对应对象但不创建。不要忘记使用std::decay<>确保默认返回类型不能为引用,因为std::declval()本身产生右值引用
    #include <utility>
    template<typename T1, typename T2,
        typename RT = std::decay_t<decltype(true ?
            std::declval<T1>() : std::declval<T2>())>>
    RT max (T1 a, T2 b)
    {
        return b < a ? a : b;
    }
    

    完美转发临时对象

    • 可以使用转发引用和std::forward>?完美转发泛型参数
    template<typename T>
    void f (T&& t) // t是转发引用
    {
        g(std::forward<T>(t)); // 完美转发实参t给g()
    }
    
    • 然而有时必须完美转发不来自参数的数据,这种情况下可以使用auto&&创建变量用于转发。比如假设把get()的返回值完美转发给set()
    template<typename T>
    void foo(T x)
    {
        set(get(x));
    }
    
    • 如果以后想改动代码来对get()产生的值执行操作,可以把值保存在一个auto&&声明的变量中
    template<typename T>
    void foo(T x)
    {
        auto&& val = get(x);
        ...
        // perfectly forward the return value of get() to set():
        set(std::forward<decltype(val)>(val));
    }
    

    引用作为模板参数

    • 尽管不常见,模板类型参数可以变成引用类型
    #include <iostream>
    template<typename T>
    void tmplParamIsReference(T) {
        std::cout << "T is reference: " << std::is_reference_v<T> <<
    '\n';
    }
    int main()
    {
        std::cout << std::boolalpha;
        int i;
        int& r = i;
        tmplParamIsReference(i); // false
        tmplParamIsReference(r); // false
        tmplParamIsReference<int&>(i); // true
        tmplParamIsReference<int&>(r); // true
    }
    
    • 即使引用变量传给模板,T还是被推断为引用类型的原有类型(因为对于引用变量v,表达式v是引用类型,而表达式的类型则不会是引用)。而显式指定则可以强制T为引用,一些模板可能设计时没有考虑这个可能性,于是引发错误和未定义行为
    template<typename T, T Z = T{}>
    class RefMem {
    private:
        T zero;
    public:
        RefMem() : zero{Z} { 
        }
    };
    
    int null = 0;
    
    int main()
    {
        RefMem<int> rm1, rm2;
        rm1 = rm2;             // OK
    
        RefMem<int&> rm3;      // ERROR: invalid default value for N
        RefMem<int&, 0> rm4;   // ERROR: invalid default value for N
    
        extern int null;
        RefMem<int&,null> rm5, rm6;
        rm5 = rm6;             // ERROR: operator= is deleted due to reference member
    }
    
    • 对非类型模板参数使用引用类型也很tricky和危险
    #include <vector>
    #include <iostream>
    
    template<typename T, int& SZ>     // Note: size is reference
    class Arr {
    private:
        std::vector<T> elems;
    public:
        Arr() : elems(SZ) {           // use current SZ as initial vector size
        }
        void print() const {
            for (int i=0; i<SZ; ++i) {  // loop over SZ elements
                std::cout << elems[i] << ' ';  
            }
        }
    };
    
    int size = 10;
    
    int main()
    {
        Arr<int&,size> y; // compile-time ERROR deep in the code of class std::vector<>
    
        Arr<int,size> x;  // initializes internal vector with 10 elements
        x.print();        // OK
        size += 100;      // OOPS: modifies SZ in Arr<>
        x.print();        // run-time ERROR: invalid memory access: loops over 120 elements
    }
    
    • 上面这个例子有些牵强,但在更复杂的情况下确实可能发生,在C++17中非类型参数可以被推断,比如
    template<typename T, decltype(auto) SZ>
    class Arr;
    
    • 使用decltype(auto)很容易产生引用类型。因此通常在这里会默认使用auto,标准库因此也有一些令人惊讶的规约限制,比如即使模板参数初始化为引用,为了仍然有赋值运算符,std::pair<>和std::tuple<>实现了赋值运算符,而不是使用默认行为
    namespace std {
        template<typename T1, typename T2>
        struct pair {
            T1 first;
            T2 second;
            ...
            // default copy/move constructors are OK even with references:
            pair(pair const&) = default;
            pair(pair&&) = default;
            ...
            // but assignment operator have to be defined to be
    available with references:
            pair& operator=(pair const& p);
            pair& operator=(pair&& p) noexcept(...);
            ...
        };
    }
    
    • 又比如因为可能造成的副作用的复杂性,C++17的类模板std::optional<>和std::variant<>对引用是非法的
    • 为了禁用引用,简单的static断言就足够了
    template<typename T>
    class optional
    {
        static_assert(!std::is_reference<T>::value,
            "Invalid instantiation of optional<T> for references");
        ...
    };
    

    延迟评估(Defer Evaluation)

    • 实现模板时,有时代码是否能处理不完整类型也会引发问题
    template<typename T>
    class Cont {
    private:
        T* elems;
    public:
        ...
    };
    
    • 目前这个类能用于不完整类型
    struct Node
    {
        std::string value;
        Cont<Node> next; // only possible if Cont accepts
    incomplete types
    };
    
    • 然而如果使用一些traits,可能就会失去处理不完整类型的能力
    template<typename T>
    class Cont {
    private:
        T* elems;
    public:
        ...
        typename
        std::conditional<std::is_move_constructible<T>::value,
            T&&,
            T&
            >::type
        foo();
    };
    
    • 这里用trait std::conditional决定返回类型为T&&还是T&,这依赖于T是否支持移动语义。问题在于is_move_constructible要求实参是完整类型(且不是void或一个数组的未知绑定),因此这个声明失败,带有这个声明的struct node声明也会失败
    • 可以用一个成员模板替代foo()解决问题,这样std::is_move_constructible的评估会延迟到foo()的实例化点
    template<typename T>
    class Cont {
    private:
        T* elems;
    public:
        template<typename D = T>
        typename
        std::conditional<std::is_move_constructible<D>::value,
            T&&,
            T&
            >::type
        foo();
    };
    

    编写泛型库时需要考虑的东西

    • 使用转发引用转发模板中的值,如果值不依赖于模板参数,使用auto&&
    • 当参数被声明为转发引用,传递左值时,准备好模板参数会是引用类型
    • 需要一个依赖于模板参数的地址时,使用std::addressof()以防参数被绑定到一个重载了operator&的类型
    • 对成员函数模板,确保它们不是比预定义拷贝/移动构造函数或赋值运算符更好的匹配
    • 当模板参数可能是字符串字面值和不是传值传递时,考虑使用std::decay
    • 如果有依赖于模板参数的out或inout参数,准备好处理const模板实参
    • 准备好处理模板参数为引用的副作用,尤其是想确保返回类型不能变成一个引用时
    • 准备好处理对不完整类型的支持,比如递归数据结构
    • 对所有数组类型重载,而不只是T[SZ]

    相关文章

      网友评论

        本文标题:【C++ Templates(10)】泛型库

        本文链接:https://www.haomeiwen.com/subject/gqdglftx.html