目录
- 使⽤distance和advance将容器的const_iterator转换为iterator
- 正确使用 `swap` 函数来清理容器
- 什么是纯函数?
- 为什么通过函数指针调用通常不会被内联?
- NVI 设计模式
- 派⽣类需要访问基类保护的成员,或需要重新定义继承来的虚函数,采⽤private继承
- 为什么需要声明正常的拷贝构造函数和赋值操作符
- 类模板相关函数支持所有参数的隐式类型转换时,最好将其定义为类模板内部的 friend 函数
- 非类型模板参数
使⽤distance和advance将容器的const_iterator转换为iterator
在 C++ 中,std::distance
和 std::advance
是两个标准库算法,可以用来帮助在迭代器之间移动,并在某些情况下将 const_iterator
转换为 iterator
。
背景知识
std::distance
:计算两个迭代器之间的距离(即元素个数)。std::advance
:将迭代器前进或后退指定的距离。
问题描述
有时候你可能需要将一个 const_iterator
转换为非 const
的 iterator
。标准库没有直接的机制来完成这种转换,因为它会破坏 const
的语义。但是你可以通过获取相对位置(使用 std::distance
)并应用到非 const
的迭代器(使用 std::advance
)来间接实现这个转换。
示例代码
假设我们有一个 std::vector
,我们将展示如何从 const_iterator
获取位置,然后将该位置应用到 iterator
中。
#include <iostream>
#include <vector>
#include <iterator>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
// 获取 const_iterator
std::vector<int>::const_iterator const_it = vec.cbegin() + 2; // 指向元素 30
// 计算 const_iterator 与 vec.cbegin() 之间的距离
std::vector<int>::difference_type pos = std::distance(vec.cbegin(), const_it);
// 创建非 const 的 iterator,并将其前进到相同的位置
std::vector<int>::iterator it = vec.begin();
std::advance(it, pos);
// 现在 it 指向与 const_it 相同的位置
std::cout << "Element pointed to by iterator: " << *it << std::endl; // 输出:30
// 修改元素的值
*it = 35;
std::cout << "Modified vector: ";
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
解释
-
获取
const_iterator
:std::vector<int>::const_iterator const_it = vec.cbegin() + 2;
获取指向vec
中第三个元素(值为 30)的const_iterator
。
-
计算距离:
std::distance
用于计算两个迭代器之间的距离(即元素的数量)。std::vector<int>::difference_type pos = std::distance(vec.cbegin(), const_it);
计算const_it
与vec.cbegin()
之间的距离,结果是 2。
-
前进
iterator
:std::vector<int>::iterator it = vec.begin();
创建一个非const
的iterator
,指向vec
的起始位置。std::advance(it, pos);
将it
前进 2 个位置,使其指向与const_it
相同的位置。
-
修改元素的值:
- 现在
it
指向与const_it
相同的位置,可以通过*it = 35;
修改该位置的元素值。
- 现在
-
输出结果:
- 打印出修改后的
vector
,验证修改是否成功。
- 打印出修改后的
总结
通过使用 std::distance
和 std::advance
,可以间接将 const_iterator
的位置转换为 iterator
。这种方法维护了 const
的语义,并确保你不违反 C++ 标准中的规则。这种技术适用于所有标准容器,包括 std::vector
、std::list
、std::deque
等。
使用 swap
技巧删除 std::vector
中的元素是一种高效的内存管理方法,它可以减少内存碎片并优化性能。然而,你提到的示例代码 vector<C> cs.swap(cs);
和 string s; string (s).swap(s);
语法上不正确并且存在误解。下面我们详细讨论正确的用法。
正确使用 swap
函数来清理容器
std::vector::swap
的用法
std::vector::swap
是一种可以高效清理 std::vector
的方法。swap
函数将两个容器的内容进行交换。在你想要清空一个 std::vector
时,可以利用 swap
函数将其与一个空的 vector
进行交换。
示例代码
#include <iostream>
#include <vector>
#include <string>
int main() {
// 示例 1: 清理 std::vector
std::vector<int> cs = {1, 2, 3, 4, 5};
std::cout << "Original vector size: " << cs.size() << std::endl;
std::vector<int>().swap(cs); // 与空 vector 进行交换
std::cout << "Vector size after swap: " << cs.size() << std::endl;
// 示例 2: 清理 std::string
std::string s = "Hello, World!";
std::cout << "Original string: " << s << std::endl;
std::string().swap(s); // 与空 string 进行交换
std::cout << "String after swap: " << s << std::endl;
return 0;
}
解释
-
清理
std::vector
:std::vector<int> cs = {1, 2, 3, 4, 5};
创建了一个包含5个整数的vector
。std::vector<int>().swap(cs);
使用一个临时的空vector
与cs
进行交换。这样,cs
变成了一个空的vector
,而原来的数据则被临时vector
持有并在其作用域结束时被销毁。- 这种方法可以有效地清理
vector
,并且避免了内存碎片问题。
-
清理
std::string
:std::string s = "Hello, World!";
创建了一个包含字符串 “Hello, World!” 的string
。std::string().swap(s);
使用一个临时的空string
与s
进行交换。这样,s
变成了一个空的string
,而原来的数据则被临时string
持有并在其作用域结束时被销毁。- 这种方法可以有效地清理
string
,并且避免了内存碎片问题。
总结
使用 swap
函数与一个临时的空容器进行交换,是清理容器的高效方法。这种方法适用于所有支持 swap
的标准容器,如 std::vector
和 std::string
。它通过交换内容来避免直接清空容器,进而减少内存碎片,提高性能。
- 正确用法:
std::vector<int>().swap(cs); std::string().swap(s);
删除某个元素
使用 “swap 技巧” 删除容器中的元素是一种常见的方法,特别是对于 std::vector
这样的顺序容器。这种方法通过将要删除的元素与容器末尾的元素交换,然后删除最后一个元素来实现高效的删除操作。这样可以避免移动大量元素,提升性能。
示例代码
下面是一个使用 “swap 技巧” 从 std::vector
中删除元素的示例:
#include <iostream>
#include <vector>
// 一个用于检查是否需要删除元素的示例函数
bool badvalue(int value) {
return value % 2 == 0; // 示例条件:删除所有偶数
}
void remove_bad_values(std::vector<int>& vec) {
for (auto it = vec.begin(); it != vec.end(); ) {
if (badvalue(*it)) {
// 将当前元素与最后一个元素交换
std::swap(*it, vec.back());
// 删除最后一个元素
vec.pop_back();
// 不递增迭代器,因为需要检查交换后的元素
} else {
++it; // 递增迭代器
}
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
std::cout << "Original vector: ";
for (int value : vec) {
std::cout << value << " ";
}
std::cout << std::endl;
remove_bad_values(vec);
std::cout << "Modified vector: ";
for (int value : vec) {
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
解释
-
删除元素的逻辑:
- 我们定义了一个函数
badvalue
,用于判断元素是否需要删除。此处示例条件是删除所有偶数。
- 我们定义了一个函数
-
使用
swap
技巧删除元素:- 遍历
std::vector
中的元素。 - 如果当前元素满足删除条件,则将当前元素与容器末尾的元素交换,然后删除最后一个元素。
- 由于交换后当前迭代器位置的新元素需要重新检查,所以不递增迭代器。
- 如果当前元素不需要删除,则递增迭代器。
- 遍历
-
示例输出:
- 输出原始向量的内容。
- 使用
remove_bad_values
函数删除不需要的元素。 - 输出修改后的向量内容。
优点
- 效率高:交换操作的时间复杂度为 O(1),而删除末尾元素的时间复杂度也是 O(1)。这比逐个删除并移动剩余元素的方式效率更高。
- 简单易懂:逻辑清晰,代码简洁。
总结
使用 “swap 技巧” 删除 std::vector
中的元素是一种高效的方法。通过将要删除的元素与末尾元素交换并删除末尾元素,可以避免移动大量元素,从而提升性能。这种方法适用于顺序容器(如 std::vector
),但不适用于关联容器(如 std::map
和 std::set
)。
什么是纯函数?
在C++编程中,一个谓词函数用于根据某些条件检查或筛选元素。确保谓词是“纯函数”意味着这个函数在调用过程中不产生任何副作用,并且在相同输入下总是返回相同的输出。下面我将详细解释如何实现这一点。
纯函数有以下特性:
- 确定性:对于相同的输入,总是返回相同的输出。
- 无副作用:不修改全局状态、不改变输入参数、不进行I/O操作(例如打印到控制台或读写文件)等。
如何编写纯函数判别式
例子:检查一个整数是否为偶数
这是一个简单的例子,用于检查整数是否为偶数的判别式。我们将确保它是一个纯函数。
#include <iostream>
#include <vector>
#include <algorithm>
// 纯函数:检查一个整数是否为偶数
bool isEven(int value) {
return value % 2 == 0;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 std::copy_if 复制所有偶数到另一个容器
std::vector<int> evenNumbers;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(evenNumbers), isEven);
// 打印结果
std::cout << "Even numbers: ";
for (int num : evenNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
解释
-
定义纯函数:
bool isEven(int value)
是一个纯函数,因为它满足所有纯函数的条件。- 它的输入参数
value
是一个整数,函数体内没有副作用(如修改全局变量、进行 I/O 操作等),返回值仅依赖于输入参数。
-
使用纯函数:
- 在
main
函数中,我们使用标准算法std::copy_if
过滤std::vector
中的偶数并将其复制到另一个容器evenNumbers
中。 std::copy_if
的最后一个参数是谓词isEven
,用作筛选条件。
- 在
更复杂的示例:带有捕获列表的 Lambda 表达式
我们还可以使用 Lambda 表达式作为纯函数判别式,只要它们没有副作用。
#include <iostream>
#include <vector>
#include <algorithm>
// 使用 Lambda 表达式作为判别式
void filterEvenNumbers(const std::vector<int>& vec) {
std::vector<int> evenNumbers;
// 定义一个纯函数的 Lambda 表达式
auto isEven = [](int value) {
return value % 2 == 0;
};
std::copy_if(vec.begin(), vec.end(), std::back_inserter(evenNumbers), isEven);
// 打印结果
std::cout << "Even numbers: ";
for (int num : evenNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
filterEvenNumbers(vec);
return 0;
}
解释
-
定义 Lambda 表达式:
auto isEven = [](int value) { return value % 2 == 0; };
是一个纯函数的 Lambda 表达式。- 它没有捕获任何外部变量,函数体内没有副作用,返回值仅依赖于输入参数。
-
使用 Lambda 表达式:
- 在
filterEvenNumbers
函数中,我们使用 Lambda 表达式isEven
作为谓词,过滤并输出偶数。
- 在
确保纯函数的要点
- 参数传递:确保函数参数是值传递或
const
引用传递,这样可以避免函数修改输入参数。 - 避免全局状态依赖:函数不应依赖或修改全局变量或静态变量。
- 避免副作用:函数内部不进行任何 I/O 操作、不修改外部状态。
为什么通过函数指针调用通常不会被内联?
内联(inline) 是一种编译器优化技术,其中函数调用被替换为函数体本身,从而减少函数调用的开销。但是,编译器是否选择内联一个函数调用,取决于多种因素,包括函数定义、调用方式以及编译器的优化策略。
1. 函数指针的调用方式
通过函数指针进行调用时,实际调用的函数在编译时可能是不确定的。这种不确定性使得编译器难以在编译时展开函数调用。函数指针可能在运行时被赋予不同的值,指向不同的函数。
#include <iostream>
void foo() {
std::cout << "foo() called" << std::endl;
}
void bar() {
std::cout << "bar() called" << std::endl;
}
int main() {
void (*funcPtr)() = foo; // 指向 foo 的函数指针
// 通过函数指针调用
funcPtr(); // 在这里编译器不知道 funcPtr 指向哪个函数
funcPtr = bar; // 现在指向 bar
funcPtr(); // 可能是不同的函数
return 0;
}
在上述例子中,funcPtr
是一个函数指针,它可以指向 foo
或 bar
。编译器在编译时无法确定 funcPtr
在每个调用时到底指向哪个函数,因此无法进行内联优化。
2. 编译器的内联决策
编译器会根据函数的定义和调用方式来决定是否进行内联优化。对于普通的函数调用,如果函数体较小且被频繁调用,编译器更倾向于内联该函数,以减少函数调用的开销。但是,对于通过函数指针的调用,由于目标函数在编译时不确定,编译器通常不会进行内联。
如何内联函数调用?
如果希望函数能够被内联,通常应避免通过函数指针进行调用,而是直接调用函数或使用编译时能够确定的函数调用方式。
直接函数调用的内联示例
#include <iostream>
inline void foo() {
std::cout << "foo() called" << std::endl;
}
void bar() {
foo(); // 直接调用,编译器可以内联
}
int main() {
foo(); // 直接调用,编译器可以内联
bar();
return 0;
}
在这个示例中,foo
函数被声明为 inline
,编译器可以选择在 bar
函数和 main
函数中直接内联 foo
函数的调用。
使用现代编译器的优化选项
现代编译器提供了更多的优化选项和指示符,可以帮助编译器更好地进行内联优化。例如,GCC 和 Clang 编译器提供了 __attribute__((always_inline))
选项来强制内联:
#include <iostream>
__attribute__((always_inline)) inline void foo() {
std::cout << "foo() called" << std::endl;
}
int main() {
foo(); // 强制内联
return 0;
}
总结
- 通过函数指针进行调用:编译器通常不会对通过函数指针进行的调用进行内联优化,因为函数指针的目标在编译时不确定。
- 直接调用:如果希望函数能够被内联,应直接调用函数或使用编译时能够确定的函数调用方式。
- 编译器优化选项:使用现代编译器的优化选项(如
__attribute__((always_inline))
)可以强制编译器内联某些函数调用。
NVI 设计模式
在面向对象编程中,Non-Virtual Interface(NVI)设计模式是一种常见的模式。它通过使用公共的非虚成员函数来包裹较低访问性(private/protected)的虚函数,从而控制派生类对虚函数的重载行为。这样做可以确保某些操作在子类重载的函数之前或之后执行,增强类的健壮性和可维护性。
在 NVI 设计模式中,基类的公共接口由非虚函数组成,这些非虚函数调用受保护或私有的虚函数。派生类可以重载这些受保护或私有的虚函数,但不能直接重载公共的非虚函数。
示例
假设我们有一个基类 Base
,它有一个公共的非虚函数 publicMethod
,这个函数调用一个受保护的虚函数 protectedMethod
。派生类 Derived
可以重载 protectedMethod
来提供具体实现。
#include <iostream>
class Base {
public:
// 公共的非虚成员函数
void publicMethod() {
// 在调用受保护的虚函数之前执行一些操作
std::cout << "Base::publicMethod: Pre-processing" << std::endl;
// 调用受保护的虚函数
protectedMethod();
// 在调用受保护的虚函数之后执行一些操作
std::cout << "Base::publicMethod: Post-processing" << std::endl;
}
protected:
// 受保护的虚函数
virtual void protectedMethod() {
std::cout << "Base::protectedMethod: Default implementation" << std::endl;
}
};
class Derived : public Base {
protected:
// 重载受保护的虚函数
void protectedMethod() override {
std::cout << "Derived::protectedMethod: Custom implementation" << std::endl;
}
};
int main() {
Derived d;
d.publicMethod();
return 0;
}
解释
-
Base 类:
publicMethod
是公共的非虚函数,它调用受保护的虚函数protectedMethod
。protectedMethod
是受保护的虚函数,可以在派生类中重载。publicMethod
包裹了对protectedMethod
的调用,并在调用前后执行一些额外的操作。
-
Derived 类:
Derived
类重载了基类的protectedMethod
,提供了自定义实现。publicMethod
不能被重载,因为它是非虚函数,这样可以确保publicMethod
的行为一致。
-
main 函数:
- 在
main
函数中,创建一个Derived
类的实例,并调用publicMethod
。 - 输出显示
publicMethod
调用了protectedMethod
的自定义实现,同时保留了在调用前后执行的额外操作。
- 在
运行结果
Base::publicMethod: Pre-processing
Derived::protectedMethod: Custom implementation
Base::publicMethod: Post-processing
优点
- 控制流程:通过 NVI 设计模式,基类可以控制函数调用的整体流程,在调用子类的重载方法之前和之后执行一些操作。
- 增强健壮性:基类可以确保某些操作始终执行,从而提高类的健壮性和一致性。
- 隐藏实现细节:子类只能重载受保护或私有的虚函数,无法直接修改公共接口,保护了类的内部实现细节。
总结
NVI 设计模式是一种强有力的设计模式,通过使用公共的非虚成员函数包裹较低访问性(private/protected)的虚函数,可以有效地控制派生类对虚函数的重载行为,增强类的健壮性和可维护性。这种模式在大型软件系统中尤其有用,可以提高代码的可读性和一致性。
派⽣类需要访问基类保护的成员,或需要重新定义继承来的虚函数,采⽤private继承
在 C++ 中,继承方式有三种:public
继承、protected
继承和 private
继承。不同的继承方式决定了基类成员在派生类中的访问权限。
private 继承
当使用 private
继承时,基类的 public
和 protected
成员在派生类中都变成 private
的。private
继承主要用于实现“组合”(composition)关系而不是“是一个”(is-a)关系。
例子:private 继承访问基类保护成员并重新定义虚函数
假设我们有一个基类 Base
,它有一个保护成员变量和一个虚函数。派生类 Derived
通过 private
继承访问基类的保护成员并重新定义继承来的虚函数。
#include <iostream>
class Base {
public:
virtual ~Base() = default;
protected:
int protectedMember = 42;
virtual void protectedMethod() const {
std::cout << "Base::protectedMethod, protectedMember: " << protectedMember << std::endl;
}
};
class Derived : private Base {
public:
void callBaseMethod() const {
// 访问基类的保护成员和方法
protectedMethod();
std::cout << "Derived::callBaseMethod, protectedMember: " << protectedMember << std::endl;
}
// 重新定义继承来的虚函数
void protectedMethod() const override {
std::cout << "Derived::protectedMethod, protectedMember: " << protectedMember << std::endl;
}
};
int main() {
Derived d;
d.callBaseMethod();
return 0;
}
解释
-
Base 类:
protectedMember
是一个保护成员变量。protectedMethod
是一个保护虚函数,输出protectedMember
的值。
-
Derived 类:
Derived
类通过private
继承Base
类。callBaseMethod
是一个公共成员函数,用于访问基类的保护成员和方法。Derived
类重载了基类的protectedMethod
。
-
main 函数:
- 在
main
函数中,创建一个Derived
类的实例,并调用callBaseMethod
。
- 在
运行结果
Derived::protectedMethod, protectedMember: 42
Derived::callBaseMethod, protectedMember: 42
关键点
-
private 继承:
- 通过
private
继承,基类的public
和protected
成员在派生类中都变成private
的。 - 派生类可以访问基类的保护成员和方法,但它们在派生类的外部不可见。
- 通过
-
访问基类的保护成员:
- 派生类通过
private
继承,可以直接访问基类的保护成员和方法。
- 派生类通过
-
重定义虚函数:
- 派生类可以重新定义基类的虚函数,实现多态。
什么时候使用 private
继承
private
继承通常用于实现“实现继承”而不是“接口继承”。换句话说,当派生类想要重用基类的实现,但不希望将基类的接口暴露给派生类的用户时,可以使用 private
继承。这种方式有效地隐藏了继承关系,使得派生类用户无法直接访问基类的成员。
总结
private
继承将基类的public
和protected
成员变成private
的。- 派生类可以通过
private
继承访问基类的保护成员,并重定义继承来的虚函数。 private
继承适用于需要重用基类实现但不希望暴露继承接口的场景。
为什么需要声明正常的拷贝构造函数和赋值操作符
在 C++ 中,声明成员模板用于泛化 copy 构造或 assignment 操作时,仍然需要声明正常的拷贝构造函数和拷贝赋值操作符。这是因为成员模板不会被编译器视为常规的拷贝构造函数或赋值操作符,编译器在某些情况下仍然会需要使用默认的拷贝构造函数和赋值操作符。
-
编译器的行为:
- 编译器会自动生成默认的拷贝构造函数和赋值操作符,但当你声明了自定义的构造函数或赋值操作符后,编译器将不会生成这些默认的函数。
- 当你只声明了成员模板形式的构造函数或赋值操作符,编译器不会将其视为常规的拷贝构造函数或赋值操作符。这会导致在需要使用默认拷贝操作时,编译器无法找到合适的函数。
-
类型转换:
- 成员模板可以用于处理不同类型的拷贝构造和赋值操作,但这些模板不会被用于处理相同类型的拷贝构造和赋值。
- 因此,为了确保类可以正常使用拷贝构造和赋值操作,需要声明相应的常规函数。
示例代码
以下是一个示例,展示了如何声明成员模板用于泛化拷贝构造和赋值操作,并同时声明正常的拷贝构造函数和赋值操作符:
#include <iostream>
class MyClass {
public:
int value;
// 默认构造函数
MyClass() : value(0) {}
// 常规拷贝构造函数
MyClass(const MyClass& other) : value(other.value) {
std::cout << "Copy constructor called" << std::endl;
}
// 常规拷贝赋值操作符
MyClass& operator=(const MyClass& other) {
if (this != &other) {
value = other.value;
std::cout << "Copy assignment operator called" << std::endl;
}
return *this;
}
// 泛化拷贝构造函数模板
template <typename T>
MyClass(const T& other) : value(other.value) {
std::cout << "Template copy constructor called" << std::endl;
}
// 泛化拷贝赋值操作符模板
template <typename T>
MyClass& operator=(const T& other) {
value = other.value;
std::cout << "Template assignment operator called" << std::endl;
return *this;
}
};
class OtherClass {
public:
int value;
OtherClass(int v) : value(v) {}
};
int main() {
MyClass a;
MyClass b(a); // 调用常规拷贝构造函数
MyClass c;
c = a; // 调用常规拷贝赋值操作符
OtherClass other(42);
MyClass d(other); // 调用模板拷贝构造函数
MyClass e;
e = other; // 调用模板赋值操作符
return 0;
}
解释
-
常规拷贝构造函数和赋值操作符:
MyClass(const MyClass& other)
是常规拷贝构造函数,用于拷贝相同类型的对象。MyClass& operator=(const MyClass& other)
是常规拷贝赋值操作符,用于赋值相同类型的对象。
-
泛化拷贝构造函数和赋值操作符模板:
template <typename T> MyClass(const T& other)
是泛化拷贝构造函数模板,可以用于拷贝不同类型的对象,只要这些对象具有value
成员。template <typename T> MyClass& operator=(const T& other)
是泛化拷贝赋值操作符模板,可以用于赋值不同类型的对象。
-
main 函数:
- 创建
MyClass
的实例a
和b
,并使用常规的拷贝构造函数。 - 使用常规的拷贝赋值操作符将
a
的值赋给c
。 - 创建
OtherClass
的实例other
,并使用模板拷贝构造函数和模板赋值操作符将其值赋给d
和e
。
- 创建
运行结果
Copy constructor called
Copy assignment operator called
Template copy constructor called
Template assignment operator called
总结
- 声明常规的拷贝构造函数和赋值操作符:即使你有成员模板用于泛化拷贝构造和赋值操作,仍然需要声明常规的拷贝构造函数和赋值操作符,以确保编译器在需要时能够找到合适的函数。
- 成员模板:用于处理不同类型的对象,但不会覆盖相同类型的拷贝构造和赋值操作。
类模板相关函数支持所有参数的隐式类型转换时,最好将其定义为类模板内部的 friend 函数
在 C++ 中,当你编写一个类模板并希望其相关函数支持所有参数的隐式类型转换时,最好将这些函数定义为类模板内部的 friend 函数。这种方式可以确保友元函数对模板参数的所有可能实例都可见,并且支持隐式类型转换。
为什么使用 friend 函数
- 隐式类型转换:将函数定义为模板类的 friend 函数可以确保它们能够访问类的私有和保护成员,并且支持参数的隐式类型转换。
- 模板参数的特化:友元函数模板可以与类模板一起实例化,确保它们在模板参数的所有可能实例中都有效。
示例:类模板中的友元函数
假设我们有一个简单的类模板 Box
,它存储一个值,并且我们希望支持该值的隐式类型转换。
定义类模板和友元函数
#include <iostream>
template <typename T>
class Box {
public:
// 构造函数
Box(const T& value) : value_(value) {}
// 声明友元函数模板
template <typename U>
friend std::ostream& operator<<(std::ostream& os, const Box<U>& box);
template <typename U>
friend Box<U> operator+(const Box<U>& lhs, const Box<U>& rhs);
private:
T value_;
};
// 定义友元函数模板
template <typename U>
std::ostream& operator<<(std::ostream& os, const Box<U>& box) {
os << box.value_;
return os;
}
template <typename U>
Box<U> operator+(const Box<U>& lhs, const Box<U>& rhs) {
return Box<U>(lhs.value_ + rhs.value_);
}
int main() {
Box<int> intBox1(10);
Box<int> intBox2(20);
Box<double> doubleBox(15.5);
// 支持隐式类型转换
std::cout << "intBox1: " << intBox1 << std::endl;
std::cout << "intBox2: " << intBox2 << std::endl;
std::cout << "doubleBox: " << doubleBox << std::endl;
Box<int> intBox3 = intBox1 + intBox2;
std::cout << "intBox3 (intBox1 + intBox2): " << intBox3 << std::endl;
// 支持隐式类型转换(int 和 double)???
Box<double> doubleBox2 = intBox1 + doubleBox;
std::cout << "doubleBox2 (intBox1 + doubleBox): " << doubleBox2 << std::endl;
return 0;
}
解释
-
类模板
Box
:Box
是一个类模板,存储一个值value_
。- 构造函数接受一个类型为
T
的值并初始化value_
。
-
友元函数模板声明:
operator<<
和operator+
被声明为Box
的友元函数模板。- 这些友元函数模板允许隐式类型转换,并能够访问
Box
的私有成员value_
。
-
友元函数模板定义:
operator<<
输出Box
的value_
。operator+
返回一个新的Box
,其值是两个Box
的value_
之和。
-
main 函数:
- 创建
Box<int>
和Box<double>
的实例。 - 输出
Box
的值,演示了operator<<
的使用。 - 将两个
Box<int>
相加,演示了operator+
的使用。 - 将一个
Box<int>
和一个Box<double>
相加,演示了隐式类型转换和operator+
的使用。
- 创建
运行结果
intBox1: 10
intBox2: 20
doubleBox: 15.5
intBox3 (intBox1 + intBox2): 30
doubleBox2 (intBox1 + doubleBox): 25.5
总结
- 友元函数模板:通过将相关函数声明为类模板的友元函数模板,可以确保这些函数支持所有参数的隐式类型转换。
- 访问私有成员:友元函数模板可以访问类的私有成员,提供更灵活和强大的功能。
- 类型转换支持:友元函数模板能够处理不同类型的操作数,实现类型转换和操作符重载。
非类型模板参数
非类型模板参数(Non-Type Template Parameters, NTTPs)是模板参数的一种,它们不是类型,而是具体的值。这些值可以是整型常量、指针、引用、枚举、或者其他支持常量表达式的值。非类型模板参数允许在编译时指定模板的某些属性,从而在一定程度上减少运行时开销,并能在编译时捕获更多的错误。
使用非类型模板参数的优缺点
优点:
- 编译时常量:可以在编译时指定某些属性,减少运行时开销。
- 优化性能:允许编译器针对不同的参数实例化模板,进行特定优化。
- 类型安全:模板实例化时,编译器会检查非类型模板参数的类型和范围,提供类型安全。
缺点:
- 代码膨胀:对于不同的参数值,模板会生成不同的实例,可能导致代码膨胀。
- 灵活性较低:非类型模板参数必须在编译时确定,因此在一些需要运行时决定的场景中不适用。
例子:非类型模板参数
示例 1:使用非类型模板参数定义数组类
#include <iostream>
// 非类型模板参数 N 表示数组的大小
template <typename T, int N>
class Array {
public:
T data[N]; // 大小为 N 的数组
// 默认构造函数
Array() {
for (int i = 0; i < N; ++i) {
data[i] = T();
}
}
// 获取数组大小
constexpr int size() const {
return N;
}
// 重载索引运算符
T& operator[](int index) {
return data[index];
}
const T& operator[](int index) const {
return data[index];
}
};
int main() {
Array<int, 5> intArray;
for (int i = 0; i < intArray.size(); ++i) {
intArray[i] = i * 10;
}
for (int i = 0; i < intArray.size(); ++i) {
std::cout << intArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,Array
类模板使用了非类型模板参数 N
来指定数组的大小。这使得数组的大小在编译时就确定下来,从而避免了运行时的动态分配。
示例 2:实现简单的固定尺寸矩阵
#include <iostream>
template <typename T, int Rows, int Cols>
class Matrix {
public:
T data[Rows][Cols]; // 固定大小的矩阵
Matrix() {
for (int i = 0; i < Rows; ++i) {
for (int j = 0; j < Cols; ++j) {
data[i][j] = T();
}
}
}
constexpr int numRows() const { return Rows; }
constexpr int numCols() const { return Cols; }
T& operator()(int row, int col) {
return data[row][col];
}
const T& operator()(int row, int col) const {
return data[row][col];
}
};
int main() {
Matrix<int, 3, 3> intMatrix;
intMatrix(0, 0) = 1;
intMatrix(1, 1) = 2;
intMatrix(2, 2) = 3;
for (int i = 0; i < intMatrix.numRows(); ++i) {
for (int j = 0; j < intMatrix.numCols(); ++j) {
std::cout << intMatrix(i, j) << " ";
}
std::cout << std::endl;
}
return 0;
}
在这个例子中,Matrix
类模板使用了非类型模板参数 Rows
和 Cols
来指定矩阵的行数和列数,从而在编译时确定矩阵的大小。
使用非类型模板参数的替代方案
尽管非类型模板参数有其优势,但在某些情况下,可以使用其他方法来替代它们,以避免代码膨胀和增加灵活性。例如,可以使用函数参数或类成员变量来替代非类型模板参数。
替代方案 1:使用函数参数
#include <iostream>
#include <vector>
template <typename T>
class Array {
public:
Array(int size) : data(size) {}
int size() const {
return data.size();
}
T& operator[](int index) {
return data[index];
}
const T& operator[](int index) const {
return data[index];
}
private:
std::vector<T> data;
};
int main() {
Array<int> intArray(5);
for (int i = 0; i < intArray.size(); ++i) {
intArray[i] = i * 10;
}
for (int i = 0; i < intArray.size(); ++i) {
std::cout << intArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,Array
类的大小由构造函数的参数指定,而不是由模板参数指定,从而避免了代码膨胀。
替代方案 2:使用类成员变量
#include <iostream>
template <typename T>
class Array {
public:
Array(int size) : size_(size), data(new T[size]) {}
~Array() {
delete[] data;
}
int size() const {
return size_;
}
T& operator[](int index) {
return data[index];
}
const T& operator[](int index) const {
return data[index];
}
private:
int size_;
T* data;
};
int main() {
Array<int> intArray(5);
for (int i = 0; i < intArray.size(); ++i) {
intArray[i] = i * 10;
}
for (int i = 0; i < intArray.size(); ++i) {
std::cout << intArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,Array
类的大小由成员变量 size_
指定,而不是由模板参数指定,从而避免了代码膨胀。
总结
- 非类型模板参数 在编译时确定模板的某些属性,有助于优化性能和提高类型安全性,但可能导致代码膨胀。
- 替代方案:在某些情况下,可以使用函数参数或类成员变量来替代非类型模板参数,从而增加灵活性并避免代码膨胀。