C++泛型库

可调用对象

        一些库包含这样一些接口,调用方可以向该类接口传递一些实体,并要求该实体必须被调用。这种编程方式称为回调,调用方传入的实体称为回调函数

        C++中,可以被用作回调参数的类型如下:

  • 函数指针类型
  • 仿函数,包括lambda表达式
  • 包含一个可以产生函数指针或函数引用的转化函数的class类型

        以上这些类型统称为函数对象类型,其对应的值称为函数对象

函数对象的支持

        关于上文提到的三种情况,前两种比较好理解,下面便是相关例子:

#include <iostream>
#include <vector>

template <typename Iter, typename Callable>
void foreach(Iter begin, Iter end, Callable callable) {
    for (auto iter = begin; iter != end; ++iter)
        callable(*iter);
}

void func(int i) {
    std::cout << i << std::endl;
}

struct func_obj {
    void operator()(int i) {
        std::cout << i << std::endl;
    }
};

int main(int argc, char **argv)
{
    std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    foreach(primes.begin(), primes.end(), func);
    foreach(primes.begin(), primes.end(), &func);
    foreach(primes.begin(), primes.end(), func_obj());
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    return 0;
}

        前两种情况能够说明平时遇到的一些问题,但有时,遇到的情况会复杂一些:

  • 调用方已有的函数参数与接口要求的回调参数不一致
  • 调用方已有的函数是一个非静态类成员函数,而接口仅允许传入一个函数,不允许传入对象

        上述则是一开始所提到的第三种情况。遇到这种情况,只能对已有的函数进行封装,然后再把封装后的函数或类传给接口,这便是函数转换。下面便是函数转换的一个例子:

#include <iostream>
#include <vector>

template <typename Iter, typename Callable>
void foreach(Iter begin, Iter end, Callable callable) {
    for (auto iter = begin; iter != end; ++iter)
        callable(*iter);
}

void add(int &x, int i) {
    x += i;
}

typedef void (*add_fp)(int &, int);
class add_wrapper
{
public:
    add_wrapper(add_fp fp, int i) : m_fp(fp), m_i(i) {}
    void operator()(int &x) {
        m_fp(x, m_i);
    }
    
private:
    add_fp m_fp;
    int m_i;
};

int main(int argc, char **argv)
{
    std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    foreach(primes.begin(), primes.end(), add_wrapper(add, 100));
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    
    return 0;
}

        看到上面这个例子,很容易联想到std::function,其实上面就是一个乞丐版的std::function实现。当然,直接使用std::function可以更简单地解决问题,如下:

#include <iostream>
#include <vector>

template <typename Iter, typename Callable>
void foreach(Iter begin, Iter end, Callable callable) {
    for (auto iter = begin; iter != end; ++iter)
        callable(*iter);
}

void add(int &x, int i) {
    x += i;
}

class add_class
{
public:
    void add(int &x, int i) {
        x += i;
    }
};

int main(int argc, char **argv)
{
    std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    
    std::function<void(int &)> add1 = std::bind(add, std::placeholders::_1, 100);
    foreach(primes.begin(), primes.end(), add1);
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    
    add_class add_obj;
    std::function<void(int &)> add2 = std::bind(&add_class::add, add_obj, std::placeholders::_1, 100);
    foreach(primes.begin(), primes.end(), add2);
    foreach(primes.begin(), primes.end(), [](int i)->void {std::cout << i << std::endl;});
    
    return 0;
}

        那么问题来了,为什么需要类似std::function的解决方案呢?大体原因如下:

  • 由于种种原因,不方便修改foreach和add源码,例如二者都是第三方库
  • std::function方案比直接用函数对add进行封装扩展性强,用函数对add封装方案如下,但如果要在原来的数值上加200呢?如果要换成乘法呢?为此需要一遍又一遍地写实现封装函数。
//...
int add_100(int &x) {
    x += 100;
}
//...
foreach(primes.begin(), primes.end(), add_100);
//...
  • 比直接使用函数指针更加安全 

 处理成员函数以及额外的参数

        除了前面提到std::function,C++17引入的std::invoke也能起到函数转换的作用,如下:

#include <iostream>
#include <vector>
#include <utility>
#include <functional>

template <typename Iter, typename Callable, typename ...Types>
void foreach(Iter begin, Iter end, Callable callable, Types &...args) {
    for (auto iter = begin; iter != end; ++iter)
        std::invoke(callable, args..., *iter);
}

class callable {
public:
    void print(int i) { std::cout << i << std::endl; }
};

void myprint(int i) { std::cout << i << std::endl; }

class myprint_cls {
public:
    void operator()(int i) { std::cout << i << std::endl; }
};

int main(int argc, char **argv)
{
    std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
    callable c;
    foreach(primes.begin(), primes.end(), &callable::print, c);
    callable *pc = &c;
    foreach(primes.begin(), primes.end(), &callable::print, pc);
    foreach(primes.begin(), primes.end(), [&](int i)->void { std::cout << i << std::endl; });
    foreach(primes.begin(), primes.end(), myprint);
    foreach(primes.begin(), primes.end(), myprint_cls());
    
    return 0;
}

        在处理回调函数方面,尽管std::invoke有函数转换的作用,统一了全局函数,成员函数,仿函数,以及lambda表达式的调用方式。但与std::function相比,参数位置的处理却没那么灵活。

其他一些实现泛型库的工具

std::addressof

        如果一个类重载了&操作符,就无法通过&获取其对象实例的地址,如下:

#include <memory>
#include <iostream>

class test {
public:
    test *operator&() { return nullptr; }
};

int main(int argc, char **argv) {

    test *ptr = new test();
    std::cout << ptr << std::endl;
    std::cout << &(*ptr) << std::endl;
    
    return 0;
}

         使用std::addressof取代&操作符,就可以解决该问题,如下:

std::cout << std::addressof(*ptr) << std::endl;

        std::declval

        

        函数模板 std::declval()可以被用作某一类型的对象的引用的占位符。该函数模板没有定义, 因此不能被调用(也不会创建对象)。因此它只能被用作不会被计算的操作数(比如 decltype 和 sizeof)。也因此,在不创建对象的情况下,依然可以假设有相应类型的可用对象。        

        比如在如下例子中,会基于模板参数 T1 和 T2 推断出返回类型 RT:

#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;
}

        为了避免在调用运算符?:的时候不得不去调用 T1 和 T2 的(默认)构造函数,这里使用了

std::declval,这样可以在不创建对象的情况下“使用”它们。不过该方式只能在不会做真正 的计算时(比如 decltype)使用。

 

完美转发临时变量

#include <string>
#include <iostream>

class tracer {
public:
    tracer(void) { std::cout << "tracer::tracer(void)" << std::endl; }
    tracer(const tracer &) { std::cout << "tracer::tracer(const tracer &)" << std::endl; }
    tracer(tracer &&) { std::cout << "tracer::tracer(tracer &&)" << std::endl; }
    tracer &operator=(const tracer &) {
        std::cout << "tracer::operator=(const tracer &)" << std::endl;
        return *this;
    }
    tracer &operator=(tracer &&) {
        std::cout << "tracer::operator=(tracer &&)" << std::endl;
        return *this;
    }
};

template <typename T>
T get(T x) {
    std::cout << "get" << std::endl;
    return x;
}

template <typename T>
void set(T x) {
    std::cout << "set" << std::endl;
}

template <typename T>
void foo(T x) {
    std::cout << "foo" << std::endl;
    //set(get(x));
    T &&tmp = get(x);
    //...
    set(tmp);
}

int main(int argc, char **argv) {
    tracer tr;
    foo(tr);
    return 0;
}

        对于上述代码,foo函数如果直接使用set(get(x));,get的返回值以右值引用传给set。但为了进行其他处理,使用tmp变量对get返回值进行保存,尽管tmp是一个右值引用,但调用set(tmp);时,tmp被已左值引用的方式传递。解决方案如下:

//...
template <typename T>
void foo(T x) {
    std::cout << "foo" << std::endl;
    T &&tmp = get(x);
    //...
    //set(std::move(tmp));
    set(std::forward<decltype(tmp)>(tmp));
}

//...

        当然此处也可以使用std::move(std::move与std::forward区别有待研究)。 

作为模板参数的引用

         如果函数模板的形参传递的不是引用,即使传递一个引用变量作为实参,其模板类型也不会被推断为引用,如下:

#include <iostream>
#include <string>

template <typename T>
void is_ref(T x) {
    std::cout << std::is_reference<T>::value << std::endl;
}

int main(int argc, char **argv) {
    std::string str0 = "hello";
    std::string &str1 = str0;
    is_ref(str0); //0
    is_ref(str1); //0
    is_ref<std::string &>(str0); //1
    is_ref<std::string &>(str1); //1
    
    return 0;
}

        尽管显示指定T的实例化类型为引用,但这不是模板设计的最初目的,因此,这种方案可能引发编译错误,如下:

template <typename T, T Z = T{}>
class ref_mem {
public:
    ref_mem(void) : m_zero(Z) {}
private:
    T m_zero;
};

int null = 0;
int main(int argc, char **argv) {
    ref_mem<int> rm1, rm2;
    rm1 = rm2;
    
    ref_mem<int &> rm3; //non-const lvalue reference to type 'int' cannot bind to an initializer list temporary
                        //in instantiation of default argument for 'ref_mem<int &>' required here
    ref_mem<int &, 0> rm4; //value of type 'int' is not implicitly convertible to 'int &'
    rm3 = rm4;

    ref_mem<int &, null> rm5, rm6;
    rm5 = rm6; //object of type 'ref_mem<int &, null>' cannot be assigned because its copy assignment operator is implicitly deleted
                // copy assignment operator of 'ref_mem<int &, null>' is implicitly deleted because field 'm_zero' is of reference type 'int &'
    
    return 0;
}

        引用类型用于非模板类型参数同样会变的复杂和危险,如下:

#include <vector>
#include <iostream>

template <typename T, int &SZ>
class arr {
public:
    arr(void) : m_elem(SZ) {}
    
    void print(void) {
        for (int i = 0; i < SZ; ++i)
            std::cout << m_elem[i] << ' ';
    }
private:
    std::vector<T> m_elem;
};

int size = 10;
int main(int argc, char **argv) {
    arry<int &, size> y; //编译错误太多,眼花缭乱
    arr<int, size> x; 
    x.print();
    size += 100;
    x.print(); //内存越界,行为未定义
    
    return 0;
}

延迟计算

        在实现模板的时候,有时可能需要考虑不完全类型的情形。什么是不完全类型?下面是一段相关的解释:

不完全类型‌是指那些在函数之外、类型的大小不能被确定的类型。这种类型可能无法实例化,也不能访问其成员,但可以使用派生的指针类型。不完全类型包括未指定长度的数组、未完全定义的‌结构体或‌联合体,以及void类型。‌

        上述几种非完全类型,数结构体的情况最为复杂,下面是一个结构体相关的不完全类型例子:

#include <stdio.h>

struct Node;

struct List {
    Node head;
};

struct Node {
	int id;
	Node next;
};

int main(int argc, char **argv)
{
	Node n;
	return 0;
}

        关于上面的例子:List定义使用Node时,Node仅仅进行了声明,未进行定义;Node定义中使用Node时,Node未完成定义。这两种情况中Node都属于属于不完全类型,因此,无法实例化,编译报错:

incomplete.cpp:6:10: error: field has incomplete type 'Node'
    Node head;
         ^
incomplete.cpp:3:8: note: forward declaration of 'Node'
struct Node;
       ^
incomplete.cpp:11:7: error: field has incomplete type 'Node'
        Node next;
             ^
incomplete.cpp:9:8: note: definition of 'Node' is not complete until the closing '}'
struct Node {

        可以使用Node指针取代Node来解决问题。接下来,看下非完全类型在模板中的影响。 

#include <string>
#include <iostream>
#include <type_traits>

template <typename T>
class Ptr final {
public:
    Ptr() : m_ptr(new T()) {}
    
    T && foo(void) {
        return *m_ptr;
    }
private:
    T *m_ptr;
};

struct Node {
    std::string name;
    Ptr<Node> next;
};

int main(int argc, char **argv)
{
    Node node;
    return 0;
}

        在上面的例子中,尽管在Node定义next,Node属于不完全类型,上面的源码仍然可以通过编译,因为Ptr中使用的是指针(直接使用Node无法通过编译)。现在,对Ptr::foo做下修改,要求如果类型支持移动语义,返回移动对象,否则返回引用对象,新的源码如下:

...    
    typename std::conditional<std::is_move_constructible<T>::value, T &&, T &> foo(void) {
        return *m_ptr;
    }
...

       然而源码无法通过编译,报错如下:

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__type_traits/is_move_constructible.h:24:38: error: incomplete type 'Node' used in type trait expression
    : public integral_constant<bool, __is_constructible(_Tp, __add_rvalue_reference_t<_Tp>)>
                                     ^
DeferEvaluation.cpp:12:36: note: in instantiation of template class 'std::is_move_constructible<Node>' requested here
    typename std::conditional<std::is_move_constructible<T>::value, T &&, T &> foo(void) {
                                   ^
DeferEvaluation.cpp:22:15: note: in instantiation of template class 'Ptr<Node>' requested here
    Ptr<Node> next;
              ^
DeferEvaluation.cpp:20:8: note: definition of 'Node' is not complete until the closing '}'
struct Node {
       ^

        之所以报错,是因为std::is_move_constructible 要求其参数必须是完全类型。而在使用 Ptr<Node>定义next时,Node属于非完全类型。解决方法是引入一个新的模板参数D,把T作为D的默认参数,新的源码如下:

...
template <typename D = T>
typename std::conditional<std::is_move_constructible<D>::value, D &&, D &> foo(void) {
    return *m_ptr;
}
...

        这样,成员函数foo在实例化后仍然是一个函数模板,不再依赖模板实参Node。再次调用成员函数foo时,foo再次被实例化。但此时即便使用Node作为模板实参也可以正常编译,因为这个实例化过程在Node定义的外部,Node属于完全类型

        

         

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值