在C++17之后,当使用类模板来创建对象时,可以不需要显式地指定模板参数类型了,比如,当使用std::tuple来创建一个元组对象时,可以使用下面的方式来定义一个tuple的对象实例:tuple<int, int> tp(1,2);。然而,如果是在C++17以下版本,这样定义会因为没有指定模板参数而编译失败:error: use of class template ‘tuple’ requires template arguments。
为了方便用户,可以同时为类模板提供一个工厂方法,通过调用工厂方法函数来创建对象,因为在调用函数时,编译器会根据函数实参类型自动推导函数的模板参数类型,这样就可以间接地为类模板推导出来模板参数类型。比如std::tuple就有一个名称为make_tuple()的工厂方法,可以使用下面的语句来创建tuple对象:auto tp= make_tuple(1,2);,此时编译器会自动推导tp的类型为tuple<int, int>。
这种方式是利用了编译器可以自动推导函数模板的参数类型的特点,那么,编译器可以自动推导函数模板的返回类型吗?比如,假设有一个工厂函数模板:template<typename R, typename T> R make_ptr(T t);
当使用 shared_ptr<int> sp = make_ptr(42);时,make_ptr()函数的返回值类型被推导为shared_ptr<int>类型,而当使用unique_ptr<string> sp = make_ptr(string(“42”));时,返回值类型被推导为unique_ptr<string>类型?
很遗憾做不到,编译器无法根据模板函数的返回类型来自动推导函数模板的模板类型。不过,既然可以通过函数的参数来自动推导,那么,可以变通一下,把返回值改成工厂函数的输出参数,即:
template<typename R, typename T>
void make_ptr(T t, R &r) {r = R(new T(t));
}
测试一下:
int main() {shared_ptr<int> sp;make_ptr(42, sp);printf("%d\n", *sp);unique_ptr<string> up;make_ptr(string("abcdefg"), up);printf("%s\n", (*up).c_str());
}
能工作,但并不是好的实现方案。首先,使用不方便,把返回值当作输出参数并不符合工厂方法函数的常规用法。其次,在创建对象时,需要先在调用方创建一个输出对象,然后再把这个对象作为输出参数去调用工厂方法,然后,在工厂方法内部使用赋值操作,把所创建的对象赋值给输出参数,有一些不必要的开销,无法copy elision优化。
有更好的方法吗?
下面介绍一种方法来实现这方式,假设有一个类,它可以统一表示为某种类型的指针对象,可以是unique_ptr、shared_ptr或者传统的裸指针类型。我们假设它的名称为universal_ptr,表示万能指针对象:
template<typename T>
class universal_ptr {T *ptr;public:universal_ptr(T t) {ptr = new T(move(t));}template<typename R>operator R() const {return R(ptr);}
};
它包含了一个数据成员:T *ptr,用来存放使用new创建的裸指针,并在构造函数中根据所传参数初始化这个指针。
同时重载了类型转换操作符operator R(),可以把这个万能指针对象转换成所需要的具体指针对象类型。看一下它的使用方式:
int main() {// 创建shared_ptr<int>对象shared_ptr<int> sp = universal_ptr(42);printf("%d\n", *sp);// 创建unique_ptr<strng>对象unique_ptr<string> up = universal_ptr(string("abcdefg"));printf("%s\n", (*up).c_str());//创建int *类型裸指针对象int *raw = universal_ptr(24);printf("%d\n", *raw);delete raw; // 需要手动释放资源
}
关键点是universal_ptr提供的类型转换操作符,使用shared_ptr<int> sp = universal_ptr(42);也就是创建了一个匿名对象universal_ptr tmporary(42),然后调用这个匿名对象的类型转换操作符:shared_ptr<int> sp = temporary.shared_ptr<int>();,相当于shared_ptr<int> sp = (shared_ptr<int>)temporary;即创建一个匿名对象之后,然后把这个匿名对象转换成shared_ptr<int>,此时编译器可以推导出转换的类型,因此也就间接得到了返回值类型,非常巧妙和新颖。
它的特点是,从形式上看是调用构造函数创建了一个universal_ptr<int>匿名对象,然后赋值给sp,但是实际上在赋值时调用了这个匿名对象的成员函数暗地里进行了类型转换。这个匿名对象的目的是仅仅把ptr成员转换成shared_ptr对象,然后调用它的转换函数,只是临时用一下,因此称为临时对象,当然在C++中临时对象的概念和作用远不止如此。它最鲜明的特点就是生存期很短,因为对用户是不可见的,用户也没法直接使用它,因此它的生存期可以很短,短到在赋值语句运行完之后就立即被销毁了,因此也被称为表达式生存期,即表达式一旦结束就会被销毁,也不用等到作用域结束。
为了支持C++17以前版本方便使用,也可以为universal_ptr<T>提供一个工厂方法,让创建对象的使用方式更像是调用一个工厂方法函数:
template<typename T>
universal_ptr<T> make_ptr(T t) {return universal_ptr<T>(move(t));
}
这样,就可以通过调用工厂方法来创建对象,如同调用make_shared()来创建shared_ptr对象一样。上面示例程序可修改如下:
int main() {shared_ptr<int> sp = make_ptr(42);printf("%d\n", *sp);unique_ptr<string> up = make_ptr(string("abcdefg"));printf("%s\n", (*up).c_str());int *raw = make_ptr(24);printf("%d\n", *raw);delete raw; // 需要手动释放资源
}
在这种应用场景中,临时对象还有一个功能,那就是储存创建目标对象时所用到的信息,也就是它的指针类型的数据成员,这个数据成员只是临时保存一下过渡阶段的数据,留到创建对象时再作为构造对象的参数。此外,临时对象使用指针形式的数据成员,也无需担心悬挂指针的问题,因为所创建的临时对象,它即时传参即时创建即时销毁,都是发生在同一个表达式中,它的生存期不会晚于创建时所传递的参数的生存期。
既然是在中间阶段创建了一个临时对象,那就有创建对象和销毁对象的开销,尽管用户并不知道这个临时对象,如果创建开销过大,也不并不是一个好的方案,在设计时应该注意这点。就以类universal_ptr来说,它只有一个数据成员,而且还是指针类型,指针占用内存非常小,在64位环境下仅占用8字节,而是这个类的析构、拷贝构造和移动构造函数都是缺省的,只是简单的指针赋值操作而已,因此,尽管在调用的中间阶段产生了临时对象,但开销非常小。
因此,如果使用这个方案,在设计临时对象的类时,可参考的原则或者套路是:
1、以指针或者引用类型定义数据成员
2、以函数模板重载类型转换操作符
基于上面的原则,我们再设计一个形似工厂方法的类,通过它可以统一使用传入的初始化列表数据来创建不同类型的容器对象,看看它的方便之处。
template<typename T>
class make_container {initializer_list<T> *list;public:make_container(initializer_list<T> t) {list = &t;}template<typename U>operator U() const {return U(std::move(*list));}
};
把类名定义为make_container,让它从形式上看就像是一个函数名称,以符合C++的工厂方法的命名;然后定义它的数据成员,为了支持{}形式的初始化形式,定义了一个initializer_list<T>类型的数据成员。显然如果把它定义成值类型,可能会造成不必要的内存空间开销和创建、销毁对象的时间开销。因此,可把它定义成了指针类型或者引用形式。
下面的测试代码可以使用它创建不同类型的容器,非常方便:
std::vector<int> vec = make_container(1,2,3,4,5,6,7,8,9});
std::list<int> lst = make_container(1,2,3,4,5,6,7,8,9});
std::deque<int> dwq = make_container(1,2,3,4,5,6,7,8,9});
std::forward_list<int> flist = make_container(1,2,3,4,5,6,7,8,9});