突破编程_C++_高级教程(模板编程实例)

1 使用模板实现一个泛型队列

当使用模板实现一个泛型队列时,需要定义一个模板类,该类将接受一个类型参数,用于指定队列中元素的类型。然后,可以在该类中实现队列的基本操作,如入队、出队、查看队首元素、判断队列是否为空等。
该实现需要考虑以下几个技术要点:
(1)模板声明:首先,需要使用模板声明队列类。这允许为多种数据类型创建队列实例。
(2)节点结构:队列通常使用链表或数组来实现。如果选择使用链表,则需要定义一个节点结构,该结构包含数据元素和一个指向下一个节点的指针。注意节点结构一般声明为内部类,其节点指针不需要被外面感知。
(3)队列结构:队列通常包含指向队首和队尾的指针,以及用于跟踪队列大小的变量。
(4)入队操作:enqueue 操作需要在队尾添加元素。如果是链表实现,你需要创建一个新节点,设置其数据,然后将其添加到队尾。
(5)出队操作:dequeue 操作需要从队首移除元素。对于链表实现,你需要移除队首节点,并更新队首指针。
(6)队首查看:front 操作应返回队首元素,但不移除它。
(7)大小与空检查:你需要实现方法来检查队列是否为空以及获取队列的大小。
(8)内存管理:在链表实现中,需要管理节点的内存分配和释放。在 dequeue 和析构函数中,需要释放不再需要的节点的内存。
(9)异常安全:确保在发生异常时,队列的状态保持一致。例如,如果在 enqueue 操作中分配内存失败,你需要确保队列不会进入不一致的状态。
(10)性能优化:考虑队列操作的性能,尤其是在大量数据的情况下。例如,可以通过预分配内存或重用已删除的节点来减少内存分配的次数。
如下为样例代码:

#include   
#include   

template <typename T>
class Queue 
{
private:
	struct Node 
	{
		T data;
		Node* nextNode;
		Node(T data) : data(data), nextNode(nullptr) {}
	};

public:
	Queue() : m_frontNode(nullptr), m_rearNode(nullptr), m_size(0) {}

	~Queue() { clear(); }

	void enqueue(const T& data) 
	{
		Node* newNode = new Node(data);
		if (nullptr == m_rearNode) 
		{
			m_frontNode = m_rearNode = newNode;
		}
		else 
		{
			m_rearNode->nextNode = newNode;
			m_rearNode = newNode;
		}
		m_size++;
	}

	bool dequeue(T& data) 
	{
		if (isEmpty()) 
		{
			return false;
		}
		data = m_frontNode->data;
		Node* temp = m_frontNode;
		m_frontNode = m_frontNode->nextNode;
		delete temp;
		m_size--;
		if (isEmpty())
		{
			m_rearNode = nullptr;
		}
		return true;
	}

	bool front(T& data) const 
	{
		if (isEmpty())
		{
			return false;
		}
		data = m_frontNode->data;
		return true;
	}

	bool isEmpty() const { return m_frontNode == nullptr; }

	size_t getSize() const { return m_size; }

	void clear() 
	{
		while (m_frontNode != nullptr)
		{
			T data;
			dequeue(data);
		}
	}

private:
	Node* m_frontNode;
	Node* m_rearNode;
	size_t m_size;

};

int main() {
	Queue<int> intQueue;
	Queue<std::string> strQueue;

	intQueue.enqueue(1);
	intQueue.enqueue(2);

	strQueue.enqueue("test1");
	strQueue.enqueue("test2");

	int value;
	std::string strValue;

	while (!intQueue.isEmpty()) 
	{
		intQueue.dequeue(value);
		std::cout << value << std::endl;
	}

	while (!strQueue.isEmpty()) 
	{
		strQueue.dequeue(strValue);
		std::cout << strValue << std::endl;
	}

	return 0;
}

上面代码的输出为:

1
2
test1
test2

在上面代码中,使用了链表来存储队列中的元素。Node 结构体表示链表中的一个节点,包含数据和指向下一个节点的指针。队列类 Queue 包含指向队首和队尾的指针,以及一个表示队列大小的变量。
enqueue 函数创建一个新节点并将其添加到队尾。如果队列是空的,它将同时设置队首和队尾指针。dequeue 函数移除队首节点并返回其数据。如果队列为空,它返回 false,否则返回 true 并更新队首指针。front 函数返回队首元素的数据。isEmpty 函数检查队列是否为空,而 getSize 函数返回队列的大小。clear 函数清空整个队列。

2 使用模板实现一个通用对象池

对象池是一种用于优化频繁创建和销毁小对象的内存管理策略。对象池预先创建一组对象并存储起来,当需要对象时,从池中获取而不是创建新对象;使用完毕后,将对象归还到池中而不是销毁它。这样可以减少内存分配和垃圾回收的开销,提高性能。
以下是 C++ 对象池的一些应用场景:
高并发环境
在高并发场景下,如果每个线程频繁地创建和销毁对象,将会导致大量的内存分配和回收操作,从而增加系统的开销。使用对象池可以预先为每个线程分配一定数量的对象,并在对象使用完毕后将其回收至对象池中,从而避免频繁的内存分配和回收操作,提高系统的并发性能。
小型对象创建
对于频繁创建和销毁的小型对象,如连接池中的连接对象、线程池中的线程对象等,使用对象池可以显著提高性能。由于这些对象的创建和销毁成本相对较低,因此将它们预先分配在对象池中,并在需要时重复利用,可以减少内存分配和回收的开销。
资源受限环境
在某些资源受限的环境下,如嵌入式系统或移动设备,内存资源可能非常有限。在这种情况下,使用对象池可以避免不必要的内存分配和回收操作,从而节省内存资源。
性能敏感应用
对于性能要求较高的应用,如实时系统、游戏等,使用对象池可以减少内存分配和回收的开销,从而提高系统的整体性能。
由于池子中的对象是不确定的类型,所以需要使用到模板编程,如下是样例代码:

#include   
#include   
#include   
#include   
#include 

template<typename T>
class MyPool;

template<typename T>
class MyPoolItem
{
public:
	MyPoolItem(std::shared_ptr<MyPool<T>> pool, std::shared_ptr<T> obj)
	{ 
		m_pool = pool;
		m_obj = obj; 
		printf("create pool item\n");
	};

	~MyPoolItem()
	{
		m_pool->releaseObj(m_obj);
		printf("destory pool item and auto release object\n");
	};

public:
	std::shared_ptr<T> getObj() { return m_obj; }

private:
	std::shared_ptr<MyPool<T>> m_pool;
	std::shared_ptr<T> m_obj;
};


template<typename T>
class MyPool : public std::enable_shared_from_this<MyPool<T>>
{
public:
	MyPool(size_t initSize, size_t maxSize) 
	{
		m_initSize = initSize;
		m_maxSize = maxSize > initSize ? maxSize : initSize;
		init();
	};
	~MyPool() { m_objs.clear(); };

public:
	std::shared_ptr<MyPool<T>> getSharedPtr()
	{
		return this->shared_from_this();
	}

	MyPoolItem<T> fetchPoolItem()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		if (m_objs.size() > 0)
		{
			std::shared_ptr<T> obj = m_objs.back();
			m_objs.pop_back();
			return MyPoolItem<T>(getSharedPtr(), obj);
		}
		if (m_currentSize >= m_maxSize)
		{
			return MyPoolItem<T>(getSharedPtr(), nullptr);
		}

		m_currentSize++;
		return MyPoolItem<T>(getSharedPtr(), std::make_shared<T>());
	}

	void releaseObj(std::shared_ptr<T> obj)
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		m_objs.emplace_back(std::make_shared<T>());
	}

private:
	void init()
	{
		m_currentSize = 0;
		for (size_t i = 0; i < m_initSize; i++)
		{
			m_objs.emplace_back(std::make_shared<T>());
			m_currentSize++;
		}
	}

private:
	std::vector<std::shared_ptr<T>> m_objs;
	size_t m_maxSize;
	size_t m_initSize;
	size_t m_currentSize;
	std::mutex m_mutex;
};

class MyClass
{
public:
	void print()
	{
		printf("obj say : hello\n");
	}
};

int main() 
{
	std::shared_ptr<MyPool<MyClass>> pool = std::make_shared<MyPool<MyClass>>(2,10);
	{
		MyPoolItem<MyClass> poolItem = pool->fetchPoolItem();
		if (nullptr != poolItem.getObj())
		{
			poolItem.getObj()->print();
		}
	}

	return 0;
}

上面代码的输出为:

create pool item
obj say : hello
destory pool item and auto release object

在上面代码中, MyPool 类模板维护了一个 m_objs 向量来存储预先创建的对象。 fetchPoolItem 方法从 m_objs 中取出一个对象,并将其封装到一个 MyPoolItem 的对象中,返回给调用者。如果 m_objs 为空,则会分配一个新的对象。 MyPoolItem 对象在被使用完后会通过析构函数自动将 obj 返还给对象池,这是一种 RAII( Resource Acquisition Is Initialization )的编程方式。
如果对象池中的对象是一个抽象类,则可以使用使用模板嵌套(类模板中包含函数模板)的方式来处理,对 fetchPoolItem 函数做如下修改:

template<typename U>
MyPoolItem<T> fetchPoolItem()
{
	std::unique_lock<std::mutex> lock(m_mutex);
	if (m_objs.size() > 0)
	{
		std::shared_ptr<T> obj = m_objs.back();
		m_objs.pop_back();
		return MyPoolItem<T>(getSharedPtr(), obj);
	}
	if (m_currentSize >= m_maxSize)
	{
		return MyPoolItem<T>(getSharedPtr(), nullptr);
	}

	m_currentSize++;
	return MyPoolItem<T>(getSharedPtr(), std::make_shared<U>());
}

3 使用模板实现一个万能函数包装器

万能函数包装器通常是一个模板类或模板函数,它使用模板参数来推断所处理参数的类型,并根据该类型执行相应的操作。这样的包装器允许程序员以一种统一的方式处理不同的数据类型,而无需编写大量的重载函数或特化模板。
以下是 C++ 万能函数包装器的一些应用场景:
泛型算法
在算法库中,经常需要编写可以处理不同数据类型的算法。使用万能函数包装器,可以编写一个通用的算法,该算法可以处理任何类型的数据,而无需为每种数据类型编写单独的算法。
回调函数和函数对象
在事件驱动编程或异步编程中,经常需要将函数作为参数传递给其他函数。使用万能函数包装器,可以编写一个可以接受任何可调用对象的函数,从而提高代码的灵活性和可重用性。
函数指针和函数对象之间的桥接
在某些情况下,可能需要将函数指针转换为函数对象,或者反之。万能函数包装器可以用于实现这种桥接,使得不同类型的函数可以被统一地处理。
函数重载的简化
当需要为同一个函数编写多个重载版本,而这些版本之间只有参数类型不同时,可以使用万能函数包装器来简化代码。通过包装器,可以将多个重载函数合并为一个通用函数。
适配器和装饰器模式
在软件设计中,适配器模式和装饰器模式经常用于将不兼容的接口转换为兼容的接口。万能函数包装器可以作为这些模式的一种实现方式,用于将不同类型的函数或可调用对象适配为统一的接口。
如下为万能函数包装器的代码实现样例:

#include   
#include   

template <typename Func>
class UniversalFunctionWrapper 
{
public:
	explicit UniversalFunctionWrapper(Func func) : func_(func) {}

	template <typename... Args>
	auto call(Args&&... args) -> decltype(auto) {
		return m_func(std::forward<Args>(args)...);
	}

private:
	Func m_func;
};

int main() 
{
	// 创建一个接受int类型参数的函数包装器  
	UniversalFunctionWrapper<std::function<void(int)>> intWrapper([](int x) {
		std::cout << "int function called with " << x << std::endl;
	});

	// 创建一个接受double类型参数的函数包装器  
	UniversalFunctionWrapper<std::function<void(double)>> doubleWrapper([](double x) {
		std::cout << "double function called with " << x << std::endl;
	});

	// 使用包装器调用函数  
	intWrapper.call(1);
	doubleWrapper.call(1.2);

	return 0;
}

上面代码的输出为:

int function called with 1
double function called with 1.2

在上面代码中, UniversalFunctionWrapper 是一个模板类,它接受一个可调用对象(如 Lambda 表达式)作为参数,并通过 call 成员函数以统一的方式调用这个可调用对象。通过使用模板和类型推导,包装器可以处理多种不同类型的函数和可调用对象。

4 编译时计算斐波那契数列

编译时计算斐波那契数列是一种使用模板元编程在编译时计算斐波那契数列的技术。斐波那契数列是一个常见的数列,其中每个数字是前两个数字的和,序列从 0 和 1 开始。
在编译时计算斐波那契数列意味着在代码编译阶段而不是运行时阶段计算出斐波那契数列的值。这可以通过递归模板来实现,每个模板实例化对应斐波那契数列中的一个数字。
如下为样例代码:

#include   

// 递归基础情况  
template <int N>
struct Fibonacci;

// 终止条件  
template <>
struct Fibonacci<0> 
{
	static const int value = 0;
};

template <>
struct Fibonacci<1> 
{
	static const int value = 1;
};

// 递归情况  
template <int N>
struct Fibonacci 
{
	static const int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};

int main() 
{
	// 编译时计算斐波那契数列的第 6 项  
	std::cout << "Fibonacci<6>::value = " << Fibonacci<6>::value << std::endl;
	return 0;
}

上面代码的输出为:

Fibonacci<6>::value = 8

在上面代码中,Fibonacci 是一个模板结构体,它对于每个整数 N 都有一个特化。特化包括基本情况( Fibonacci<0> 和 Fibonacci<1> ),它们直接给出斐波那契数列的前两项,以及递归情况( Fibonacci ),它依赖于前两个项的值。
当编译器遇到 Fibonacci<10>::value 时,它会递归地展开这个表达式,直到它到达基本情况并计算出最终的值。这个值在编译时就已经确定了,因此在运行时不会发生任何计算。
需要注意的是,由于这种递归展开是在编译时进行的,编译器可能会对模板展开的深度设置限制,以防止无限递归或过度的编译时间。在实际应用中,需要确保所计算的斐波那契数列的项数在编译器允许的范围内。
此外,由于编译时计算是在编译阶段完成的,因此这种技术非常适合用于常量表达式、静态数组大小、枚举值等需要在编译时确定的情况。

5 使用类型萃取实现策略模式

类型萃取(Type Traits)和策略模式(Strategy Pattern)是两种不同的编程概念,但它们可以结合使用来实现更加灵活和可扩展的代码。策略模式是一种行为设计模式,它定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。而类型萃取则允许我们在编译时检查类型的属性,并根据这些属性提供不同的行为。
如下的样例展示了如何使用类型萃取来实现策略模式。
首先定义一个策略接口,该接口描述了策略所必须实现的方法:

// 策略接口  
class Strategy 
{  
public:  
    virtual ~Strategy() = default;  
    virtual void execute() const = 0; // 策略执行的方法  
};

然后创建几个实现了该接口的具体策略类:

// 具体策略类A  
class StrategyA : public Strategy 
{  
public:  
    void execute() const override { std::cout << "exec strategy A" << std::endl; }  
};  
  
// 具体策略类B  
class StrategyB : public Strategy 
{  
public:  
    void execute() const override { std::cout << "exec strategy B" << std::endl; }  
};

接下来创建一个上下文类:
这个上下文类接受一个策略对象作为参数,并在执行时调用该策略的方法。这里使用 std::enable_if 和类型萃取来根据传入的策略类型选择不同的行为

#include   

// 上下文类,使用类型萃取来选择策略  
template<typename StrategyType>
class Context 
{
public:
	Context(StrategyType strategy) : m_strategy(strategy) {}

	void setStrategy(StrategyType strategy) { m_strategy = strategy; }

	void executeStrategy() { m_strategy->execute(); }

private:
	StrategyType m_strategy;

	// 启用条件: 必须是 Strategy* 类型或者 const Strategy* 类型 
	static_assert(std::is_same<Strategy*, StrategyType>::value || std::is_same<const Strategy*, StrategyType>::value, "must be Strategy* type or const Strategy* type");

};

在上面代码中,启用条件用于检查给定的类型是否是 Strategy 类型或其指针。 Context 类模板接受一个策略类型作为参数,并使用 std::is_same 来确保只有当传入的类型满足指定条件时,模板实例化才是有效的。
最后,可以这样使用 Context 类:

int main()
{
	// 创建策略对象  
	StrategyA a;
	StrategyB b;

	// 创建上下文对象,并设置策略  
	Context<Strategy*> contextA(&a);
	contextA.executeStrategy(); // 输出: Executing Strategy A  

	Context<const Strategy*> contextB(&b);
	contextB.executeStrategy(); // 输出: Executing Strategy B  

	return 0;
}

在上面代码中,通过将不同的策略对象传递给 Context 类来动态地改变其行为。 Context 类在编译时通过类型萃取来确保它只接受 Strategy 类型的对象,并在运行时执行相应的策略。

你可能感兴趣的:(突破编程_C++_高级教程,c++,开发语言)