说明:C++11 引入了模板函数的默认模板参数这一特性。在定义模板函数时,可以为其模板参数指定默认值。这样在调用该模板函数时,如果调用者没有显式提供某个模板参数,编译器就会使用该参数的默认值。
接下来直接上例子感受下,myFunction 是一个模板函数,它有两个模板参数 T 和 U。T 的默认类型是 int,U 的默认类型是 double。如下所示:
//模板函数定义
template <typename T = int, typename U = double>
void myFunction(T a, U b) {
// 函数实现
}
int main(int argc ,char **argv){
//提供两个模板参数:
myFunction<float, char>(3.14, 'a');
//只提供一个模板参数
myFunction<float>(3.14, 'a'); // T 为 float, U 使用默认的 double
//不提供任何模板参数:
myFunction(3, 3.14); // T 使用默认的 int, U 使用默认的 double
return 0;
}
通过使用默认模板参数,开发者可以更加灵活地使用模板函数,而无需在每次调用时都显式提供所有模板参数。这提高了代码的可读性和可维护性。
1 为什么C++11之前未引入,但C++11后开始引入?
在C++11之前已经引入函数模板与类模板,在模板类声明时可以允许其有默认模板参数,但不支持函数模板的默认模板参数;C++11开始支持函数模板的默认模板参数。那么 为什么函数模板的默认模板参数C++11之前未引入,但C++11后开始引入?这里将问题拆分成2个并分别解读。具体如下:
1.1 为什么之前未引入函数模板的默认模板参数
之前 C++ 标准库中的模板类和函数并没有设计成默认模板参数的形式,主要有以下几个历史原因:
- 早期 C++ 标准的设计:C++ 最初的设计目标是向后兼容 C 语言,因此在早期标准的制定过程中,设计者们更倾向于保持简洁和兼容性,而不是引入过多新特性。默认模板参数在早期 C++ 标准中并没有被纳入考虑范畴。
- 实现复杂度:为模板引入默认参数会增加编译器的实现复杂度,需要处理更多的边界情况。这在早期 C++ 编译器的实现上可能会带来一定的困难。
- 潜在问题:过度使用默认模板参数可能会增加代码的复杂性和混淆性。如果默认参数设计不当,可能会导致意料之外的行为。
- 语言演化:C++ 标准的制定是一个渐进的过程。随着语言的不断发展和用户需求的变化,新特性才逐步被纳入标准。默认模板参数直到 C++11 才被正式引入,这也说明了语言设计的谨慎性。
所以,之前标准库中没有广泛使用默认模板参数更多是基于当时 C++ 语言设计的历史背景和考虑。随着语言的不断进化,这种设计方式也在不断优化和改进。
1.2 为什么C++11 开始引入函数模板的默认模板参数
C++11 引入函数模板的默认模板参数主要有以下几个原因:
- 提高代码可读性和可维护性:默认模板参数可以减少模板函数调用时需要显式指定的参数数量,从而提高代码的可读性。当模板函数的参数有合理的默认值时,调用方可以更专注于其他参数,而不需要关心所有参数的具体取值。
- 增强灵活性:默认模板参数使得模板函数的使用更加灵活。开发者可以根据需要选择使用默认值或自定义参数。这种灵活性在某些特殊场景下非常有用,比如在模板元编程中。
- 减少重复代码:在一些情况下,如果没有默认模板参数,开发者可能需要编写多个具有相似签名的模板函数,这会增加代码冗余。默认模板参数可以帮助减少这种重复代码,提高代码的整体质量。
- 编译器实现的成熟度:相比于早期 C++ 标准,现代编译器在处理默认模板参数方面已经有了较为成熟的实现,这降低了引入该特性的技术门槛。
总之,引入默认模板参数是为了更好地支持模板元编程,提高代码的可读性和可维护性,同时也降低了开发者的工作量。这种语言特性的引入体现了 C++ 设计的不断优化和完善。
2 函数模板的默认模板参数 使用详解
2.1 简化模板函数调用
template <typename T = int, typename U = double>
void printElements(T a, U b) {
std::cout << "a = " << a << ", b = " << b << std::endl;
}
int main() {
printElements(10, 3.14); // a = 10, b = 3.14
printElements(5.2); // a = 5.2, b = 0
printElements('x', 'y'); // a = 'x', b = 'y'
return 0;
}
在这个例子中,printElements
函数有两个模板参数 T
和 U
。当调用该函数时,如果不提供任何模板参数,编译器会自动使用默认的 int
和 double
类型。这大大简化了函数的调用方式。
2.2 支持可变数量的模板参数
template <typename T = int, typename... Args>
void variadicFunction(T a, Args... args) {
std::cout << "a = " << a << std::endl;
((std::cout << "arg = " << args << " "), ...);
std::cout << std::endl;
}
int main() {
variadicFunction(1, 2.3, 'x', "hello");
// a = 1
// arg = 2.3 arg = 'x' arg = hello
variadicFunction(1.2);
// a = 1.2
return 0;
}
在这个例子中,variadicFunction 是一个可变参数模板函数。第一个参数 T 有默认值 int,其余参数使用可变参数模式 Args...。这样可以灵活地处理不同数量和类型的参数。
2.3 配合 SFINAE 技术实现特定类型的偏特化
SFINAE技术是 "Substitution Failure Is Not An Error" 的缩写,它是一种 C++ 编译器特性,在处理模板函数调用时非常有用。
SFINAE 的基本原理是:当编译器尝试对模板函数进行实例化时,如果该实例化过程中发生了类型替换失败,编译器不会立即报错,而是会尝试其他可行的模板。这种"类型替换失败不是错误"的行为可以让我们在编写模板函数时,有更大的灵活性来进行条件判断和特殊化。
SFINAE 常见的应用包括:
- 重载函数选择: 根据参数类型的不同,SFINAE 可以让编译器选择合适的重载函数。
- 编译期 if 语句: 利用 SFINAE 可以实现类似 if constexpr 的编译期条件判断。
- 特性检测: 可以通过 SFINAE 来检查某个类型是否具备特定的成员函数或类型。
总之,SFINAE 是 C++ 模板元编程中非常重要的一个特性,可以让我们编写出更加灵活、可扩展的模板代码。它为 C++ 模板元编程提供了强大的基础。配合 SFINAE 技术实现特定类型的偏特化的参考代码如下所示:
template <typename T, typename U = typename std::enable_if<std::is_integral<T>::value, T>::type>
void printIntegral(T value) {
std::cout << "Integral value: " << value << std::endl;
}
template <typename T, typename U = typename std::enable_if<!std::is_integral<T>::value, T>::type>
void printNonIntegral(T value) {
std::cout << "Non-integral value: " << value << std::endl;
}
int main() {
printIntegral(42); // Integral value: 42
printIntegral(3.14); // Error: no matching function for call to 'printIntegral'
printNonIntegral(3.14); // Non-integral value: 3.14
printNonIntegral(42); // Error: no matching function for call to 'printNonIntegral'
return 0;
}
在这个例子中,我们使用 SFINAE (Substitution Failure Is Not An Error) 技术来实现对整型和非整型值的不同处理。第二个模板参数 U 的默认值利用 std::enable_if 来确定函数是否可以被实例化。
这种结合默认模板参数和 SFINAE 的方式,可以帮助我们编写更加灵活和安全的模板函数。
2.4 实现默认比较函数
template <typename T, typename Compare = std::less<T>>
bool myCompare(const T& a, const T& b) {
Compare comp;
return comp(a, b);
}
int main() {
std::cout << myCompare(3, 5) << std::endl; // true
std::cout << myCompare(5, 3) << std::endl; // false
std::cout << myCompare(3.14, 2.71, std::greater<double>()) << std::endl; // true
return 0;
}
在这个例子中,myCompare 函数有两个模板参数 T 和 Compare。Compare 的默认类型是 std::less<T>,表示使用小于比较。但是在调用时,我们也可以提供自定义的比较函数,如 std::greater<double>。这为函数的使用提供了更大的灵活性。
2.5 实现通用的打印函数
template <typename T, typename Printer = typename std::ostream_iterator<T>>
void printElements(std::ostream& os, const std::initializer_list<T>& list, const std::string& sep = ", ") {
std::copy(list.begin(), list.end(), Printer(os, sep.c_str()));
}
int main() {
printElements(std::cout, {1, 2, 3}); // 1, 2, 3
printElements(std::cout, {'a', 'b', 'c'}, " | "); // a | b | c
printElements(std::cerr, {"hello", "world", "c++"}); // hello, world, c++
return 0;
}
在这个例子中,printElements
函数有三个模板参数:
T
: 待打印元素的类型Printer
: 打印器类型,默认为std::ostream_iterator<T>
sep
: 元素之间的分隔符,默认为,
通过使用默认模板参数,我们可以非常灵活地调用 printElements
函数,打印各种类型的元素,并自定义分隔符。
2.6 在继承关系中使用默认模板参数
template <typename T = int>
class Base {
public:
void doSomething(T value) {
std::cout << "Base::doSomething() with value: " << value << std::endl;
}
};
template <typename U = double, typename T = U>
class Derived : public Base<T> {
public:
void doSomething(U value) {
std::cout << "Derived::doSomething() with value: " << value << std::endl;
Base<T>::doSomething(static_cast<T>(value));
}
};
int main() {
Derived<> d1; // T = double, U = double
d1.doSomething(3.14);// Derived::doSomething() with value: 3.14
// Base::doSomething() with value: 3.14
Derived<int> d2; // T = int, U = int
d2.doSomething(42); // Derived::doSomething() with value: 42
// Base::doSomething() with value: 42
return 0;
}
在这个例子中,Derived 类继承自 Base 类,并且 Derived 也有两个模板参数 U 和 T。T 的默认值是 U,这样 Derived 就可以灵活地选择与 Base 类相同或不同的类型。
2.7 编写通用的反转容器函数
template <typename Container, typename Iterator = typename Container::iterator>
void reverseContainer(Container& c) {
std::reverse(std::begin(c), std::end(c));
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
reverseContainer(v);
// v: {5, 4, 3, 2, 1}
std::string str = "hello";
reverseContainer(str);
// str: "olleh"
return 0;
}
在这个例子中,reverseContainer 函数有两个模板参数:
- Container: 待反转的容器类型
- Iterator: 容器的迭代器类型,默认为 Container 的默认迭代器类型
通过使用默认模板参数 Iterator,我们可以轻松地反转各种类型的容器,如 std::vector、std::string 等,而无需显式指定迭代器类型。
2.8 实现简单的容器查找函数
template <typename Container, typename T, typename Finder = std::find<typename Container::const_iterator, T>>
typename Container::size_type findInContainer(const Container& c, const T& value) {
auto it = Finder(std::begin(c), std::end(c), value);
return it == std::end(c) ? Container::npos : std::distance(std::begin(c), it);
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::size_t index = findInContainer(v, 3); // index = 2
index = findInContainer(v, 10); // index = Container::npos
std::string str = "hello";
index = findInContainer(str, 'l'); // index = 2
index = findInContainer(str, 'x'); // index = std::string::npos
return 0;
}
在这个例子中,findInContainer 函数有三个模板参数:
- Container: 待查找的容器类型
- T: 待查找的元素类型
- Finder: 查找算法,默认为 std::find
通过使用默认模板参数 Finder,我们可以轻松地在各种类型的容器中查找元素,并返回元素的下标或 npos 表示未找到。
2.9 在类模板中使用默认模板参数
template <typename T, typename Alloc = std::allocator<T>>
class MyVector {
public:
using value_type = T;
using allocator_type = Alloc;
using size_type = std::size_t;
using iterator = T*;
using const_iterator = const T*;
MyVector() : MyVector(allocator_type{}) {}
explicit MyVector(const allocator_type& alloc) : _alloc(alloc) {}
void push_back(const T& value) {
// 使用 _alloc 分配内存并构造元素
}
// 其他成员函数...
private:
allocator_type _alloc;
// 其他成员变量...
};
int main() {
MyVector<int> v1; // 使用默认分配器 std::allocator<int>
MyVector<std::string, std::allocator<std::string>> v2; // 自定义分配器
return 0;
}
在这个例子中,MyVector 类模板有两个模板参数:
- T: 元素类型
- Alloc: 内存分配器类型,默认为 std::allocator<T>
通过使用默认模板参数 Alloc,我们可以在创建 MyVector 对象时,选择使用默认的分配器或自定义的分配器。这增加了类模板的灵活性和可复用性。
总的来说,默认模板参数为 C++ 的模板编程带来了更大的灵活性和可读性,在简化调用、支持可变参数以及实现特定类型的偏特化等场景中都能发挥重要作用。希望这些示例能够帮助你更好地理解和应用这一特性。