前面一篇文章我们讨论了字符串作为参数的模板函数推导问题,下面我们看一下使用不同字符串参数类型对模板函数实例化的影响。代码如下,在语句后面的注释为该句的输出。该输出是 g++ 编译后产生的输出,主要是因为输出简洁,而且我们这里只关心模板函数的不同实例,并不关心 const 类型。
#include <iostream>
#include <typeinfo>
#include <vector>
#include <string>
using namespace std;template<typename T>
void foo(const T& t)
{
cout << "foo: generic(" << t << ") " << typeid(t).name() << endl;
}template<typename T>
void bar(const T t)
{
cout << "bar: generic(" << t << ") " << typeid(t).name() << endl;
}/*
$ c++filt [-t] A1_c A2_c A3_c Ss PKc
char [1]
char [2]
char [3]
std::basic_string<char, std::char_traits<char>, std::allocator<char> >
char const*
*/
int main()
{
foo(""); // foo: generic() A1_c
foo("0"); // foo: generic(0) A2_c
foo("01"); // foo: generic(01) A3_c
foo(static_cast<string>("")); // foo: generic() Ss
foo(static_cast<string>("0")); // foo: generic(0) Ss
foo(static_cast<string>("01")); // foo: generic(01) Ss
foo(static_cast<const char *>("")); // foo: generic() PKc
foo(static_cast<const char *>("0")); // foo: generic(0) PKc
foo(static_cast<const char *>("01")); // foo: generic(01) PKc
foo(*(new string(""))); // foo: generic() Ss
foo(*(new string("0"))); // foo: generic(0) Ss
foo(*(new string("01"))); // foo: generic(01) Ss
bar(""); // foo: generic() PKc
bar("0"); // foo: generic(0) PKc
bar("01"); // foo: generic(01) PKc
bar(static_cast<string>("")); // foo: generic() Ss
bar(static_cast<string>("0")); // foo: generic(0) Ss
bar(static_cast<string>("01")); // foo: generic(01) Ss
bar(static_cast<const char *>("")); // foo: generic() PKc
bar(static_cast<const char *>("0")); // foo: generic(0) PKc
bar(static_cast<const char *>("01")); // foo: generic(01) PKc
bar(*(new string(""))); // foo: generic() Ss
bar(*(new string("0"))); // foo: generic(0) Ss
bar(*(new string("01"))); // foo: generic(01) Ss
return 0;
}
基于前一篇博客的分析,我们知道形如 "hello" 的常量字符串在编译时的类型是 char 数组。不同长度的 char 数组,其类型是不一样的,我们可以使用下面语句:
cout << (typeid(char [1]) == typeid(char [2])) << endl;
来验证这一想法。因此,如果我们使用不同长度的字符串作为参数调用 foo,编译器就会为模板函数 foo 实例化不同的实例函数,这一点已经由 foo 的前三个输出验证。我们还可以通过 readelf 来读取目标文件符号表,或者 objdump 查看目标文件反汇编代码中 foo 的实例函数的数量。
$ readelf -s test.o | c++filt -t | less
$ objdump -S test.o | c++filt -t | less
这也就是说,我们使用原始字符串调用了三次 foo,其实是三个不同的实例函数,这样显然会导致目标代码臃肿。那么怎么避免这种情况出现呢?下面我们使用了三种不同的方法,将字符串 static_cast 成 string 或者 const char * 类型,或者使用字符串构造一个 string 对象作为参数,这三种情况都能保证不同(内容)字符串参数的调用使用的是同一个实例化的模板函数。
有没有方法避免类型转换呢?我们可以使用非引用参数类型作为模板函数的模板参数,如 bar 模板函数所示。如前一篇中的分析,此时 char 数组类型会被隐式转换成 char 指针类型,然后进行模板函数推导。所以我们看到即使传的是原始字符串参数,其调用的实例化函数仍然是 char const * 类型的。由于这里类型 T 被推导为 char const * 类型,所以传递的仍然是指针。
但是下面的 string 类型的实例化模板函数实现的就是值传递了,这在函数运行效率上可能会有一些影响。不过现代的函数库对 string 都实现为 copy-on-write(例如 MFC 的 CString 和 Qt 的 QString),我想 STL 的 string 应该也不例外,而 const T 参数并不允许对参数修改,所以效率上的影响应该还是比较小的。只是在语义上与传一个指针就有不同了,假如不限定 T 是 const,那么值传递 string 时,对 string 的修改就无法反映到原来 string 上了。
最后,到底哪个方法好呢?我不知道,我没有足够的实践经验来评论哪种方法更好。我这两篇文章的目的仅仅是探讨一下使用不同形式字符串作为模板函数参数时可能发生的奇怪现象,以及要注意的方面,至于哪种方法更好,可能要留待实际需求来决定。
附:第一段代码的 VS 2008 编译器编译结果执行的输出:
foo: generic() char const [1]
foo: generic(0) char const [2]
foo: generic(01) char const [3]
foo: generic() class std::basic_string,class std::allocator >
foo: generic(0) class std::basic_string,class std::allocator >
foo: generic(01) class std::basic_string,class std::allocator >
foo: generic() char const *
foo: generic(0) char const *
foo: generic(01) char const *
foo: generic() class std::basic_string,class std::allocator >
foo: generic(0) class std::basic_string,class std::allocator >
foo: generic(01) class std::basic_string,class std::allocator >
bar: generic () char const *
bar: generic (0) char const *
bar: generic (01) char const *
bar: generic () class std::basic_string,class std::allocator >
bar: generic (0) class std::basic_string,class std::allocator >
bar: generic (01) class std::basic_string,class std::allocator >
bar: generic () char const *
bar: generic (0) char const *
bar: generic (01) char const *
bar: generic () class std::basic_string,class std::allocator >
bar: generic (0) class std::basic_string,class std::allocator >
bar: generic (01) class std::basic_string,class std::allocator >