马克思曾经说过:能用一个接口处理的事情,就别写俩。
多态是为了让用户端能用同一个接口,处理不同类型的对象而设置的。当然,这里的用户端是广义的,你写的一套接口,给另一个同事调用,那个同事就是你的用户端,哪怕是你自己在另一个文件里调用,这“另一个文件”也可以称为是用户端。
之所以要这么做,原因很多,但总结起来就一句话:让用户用起来爽。
口说无凭,先上个小代码看看:
1 #include <iostream>
2
3 using namespace std;
4
5 class Test1{};
6 class Test2{};
7 class Test3{};
8 class Test4{};
9
10 void common_interface(const Test1 &t) {
11 cout << "deal with Test1..." << endl;
12 }
13 void common_interface(const Test2 &t) {
14 cout << "deal with Test2..." << endl;
15 }
16 void common_interface(const Test3 &t) {
17 cout << "deal with Test3..." << endl;
18 }
19
20
21 void foo() {
22 Test1 t1;
23 Test2 t2;
24 Test3 t3;
27 common_interface(t1);
28 common_interface(t2);
29 common_interface(t3);
31 }
32 int main() {
33 foo();
34 return 0;
35 }
这里使用函数重载,来实现了所谓的多态,无论是哪种Test类型,用户可以无脑的传递给common_interface。
熟悉C++的都知道,函数重载只是一种比较low的多态,叫静态多态。相对应的,还有一种动态多态,也就是通过继承体系,虚函数,虚表指针等实现的多态,才是真正意义上的多态。
这种多态实现起来就相对麻烦了,必须要有一个继承体系,然后通过虚函数,通过基类的指针或引用巴拉巴拉……
这种小demo,只要学过C++的,脑袋里肯定已经有画面了,就不贴代码了,何况马克思说过,简单的代码没必要贴出来占地方!
所以我们来看一个稍微复杂的场景:假如我们要处理的数据是从上游发送过来的(这种发送也是广义的,并不一定是通过了网络)。然后我们要用同一个接口处理这些数据,如果发送端和接收端是同步的,那用上面例子的函数重载就可以解决这种问题。
但如果二者是异步的,问题就会复杂很多,首先必须要有一个异步队列,而这个队列里要能放入各种类型的数据,这就要求这些数据有一个共同的基类,然后将基类指针作为队列存储对象。但上游传过来的消息并不一定存在这种共基类的关系,最广泛的场景,他们可能是风马牛不相及的东西。
为了将风马牛都放入一个队列中,就必须再构造一个继承体现,作为他们的“管理者”,这些类不含业务功能,是真正意义上的管理者。
异步队列是一个相对独立的模块,可以在网上找到很多,为了方便,这里提供一个:
//safe_queue.h
#include <condition_variable>
#include <deque>
#include <mutex>
template <typename T>
class SafeQueue {
public:
using lock_type = std::unique_lock<std::mutex>;
public:
SafeQueue() = default;
~SafeQueue() = default;
template<typename IT>
void push(IT &&item) {
static_assert(std::is_same_v<T, std::decay_t<IT>>, "Item type is not convertible!!!");
{
lock_type lock{mutex_};
queue_.emplace_back(std::forward<IT>(item));
}
cv_.notify_one();
}
auto pop() -> T {
lock_type lock{mutex_};
cv_.wait(lock, [&]() { return !queue_.empty(); });
auto front = std::move(queue_.front());
queue_.pop_front();
return front;
}
private:
std::deque<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
};
然后就是业务代码了
#include <iostream>
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
#include "safe_queue.h"
using namespace std;
class Test1 {
public:
Test1() = default;
Test1(const Test1 &other) {
cout << "Test1 Copy Constructor..." << endl;
}
Test1(const Test1 &&other) {
cout << "Test1 Move Constructor..." << endl;
}
};
class Test2 {
public:
Test2() = default;
Test2(const Test2 &other) {
cout << "Test2 Copy Constructor..." << endl;
}
Test2(const Test2 &&other) {
cout << "Test2 Move Constructor..." << endl;
}
};
class Test3 {
public:
Test3() = default;
Test3(const Test3 &other) {
cout << "Test3 Copy Constructor..." << endl;
}
Test3(const Test3 &&other) {
cout << "Test3 Move Constructor..." << endl;
}
};
class Test4 {
public:
Test4() = default;
Test4(const Test4 &other) {
cout << "Test4 Copy Constructor..." << endl;
}
Test4(const Test4 &&other) {
cout << "Test4 Move Constructor..." << endl;
}
};
lass DealBase{
public:
virtual void common_interface() {}
};
class DealTest1 : public DealBase {
public:
template<typename T>
DealTest1(T &&t): t_{std::forward<T>(t)} { }
virtual void common_interface() {
cout << "deal with Test1..." << endl;
}
private:
Test1 t_;
};
class DealTest2 : public DealBase {
public:
template<typename T>
DealTest2(T &&t): t_{std::forward<T>(t)} { }
virtual void common_interface() {
cout << "deal with Test2..." << endl;
}
private:
Test2 t_;
};
class DealTest3 : public DealBase {
public:
template<typename T>
DealTest3(T &&t): t_{std::forward<T>(t)} { }
virtual void common_interface() {
cout << "deal with Test3..." << endl;
}
private:
Test3 t_;
};
class DealTest4 : public DealBase {
public:
template<typename T>
DealTest4(T &&t): t_{std::forward<T>(t)} { }
private:
Test4 t_;
};
class Widget {
public:
Widget() :
worker_{std::thread([this](){ this->worker_func();})},
to_quit_{false} { }
~Widget() {
to_quit_ = true;
if (worker_.joinable()) {
worker_.join();
cout << "thread is destroy" << endl;
}
}
void add_task(DealBase* pdb) {
queue.push(pdb);
}
private:
void worker_func() {
while(!to_quit_.load()) {
//do something with class member
std::this_thread::sleep_for(std::chrono::seconds(1));
cout << "thread is working" << endl;
while(true) {
DealBase *p = queue.pop();
p->common_interface();
sum_mutable_data += 1;
}
}
}
private://线程对象
mutable std::atomic_bool to_quit_;
SafeQueue<DealBase*> queue;
int sum_mutable_data;
};
void foo() {
Test1 t1;
Test2 t2;
Test3 t3;
Test4 t4;
DealBase *p1 = new DealTest1{t1};
DealBase *p2 = new DealTest2(std::move(t2));
DealBase *p3 = new DealTest3(std::move(t3));
DealBase *p4 = new DealTest4(std::move(t4));
Widget w;
w.add_task(p1);
w.add_task(p2);
w.add_task(p3);
w.add_task(p4);
w.add_task(123);
std::this_thread::sleep_for(std::chrono::seconds(10));
delete p1;
delete p2;
delete p3;
delete p4;
}
int main() {
foo();
return 0;
}
正如代码所示,为了用户端(新建的线程)用起来爽(不管队列里是什么类型对象,直接就是p->common_interface();),需要很多额外的代码来实现。
这种为了实现某种功能,需要写很多无用代码代码的行为,可以认为是编程语言的缺点,即表达某种语义的语法过于复杂。而这种语义,就是面向对象三大特性之一的多态。
所以后面C++ 17对这部分语法进行了优化,引入了variant 和 visit函数,使用新的语法完成同样的语义,就简单多了:
#include <iostream>
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
#include <variant>
#include "safe_queue.h"
using namespace std;
class Test1 {
public:
Test1() = default;
Test1(const Test1 &other) {
cout << "Test1 Copy Constructor..." << endl;
}
Test1(const Test1 &&other) {
cout << "Test1 Move Constructor..." << endl;
}
};
class Test2 {
public:
Test2() = default;
Test2(const Test2 &other) {
cout << "Test2 Copy Constructor..." << endl;
}
Test2(const Test2 &&other) {
cout << "Test2 Move Constructor..." << endl;
}
};
lass Test3 {
public:
Test3() = default;
Test3(const Test3 &other) {
cout << "Test3 Copy Constructor..." << endl;
}
Test3(const Test3 &&other) {
cout << "Test3 Move Constructor..." << endl;
}
};
class Test4 {
public:
Test4() = default;
Test4(const Test4 &other) {
cout << "Test4 Copy Constructor..." << endl;
}
Test4(const Test4 &&other) {
cout << "Test4 Move Constructor..." << endl;
}
};
template<class... Ts> struct overloaded : Ts... {using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
template <typename... Ts>
class Widget {
public:
using queue_t = SafeQueue<std::variant<Ts...>>;
public:
Widget() :
worker_{std::thread([this](){ this->worker_func();})}, //可以在构造函数里起线程,也可以在其他成员函数里
to_quit_{false} { }
~Widget() {
to_quit_ = true;
if (worker_.joinable()) {
worker_.join();
cout << "thread is destroy" << endl;
}
}
template<typename IT>
void add_task(IT &&item) {
queue.push(std::forward<IT>(item));
}
private:
void worker_func() {
//do something with class member
while(!to_quit_.load()) {
cout << "thread is working" << endl;
const auto &item = queue.pop();
std::visit(overloaded{
[](auto &msg){ cout << "deal unknown type..." << endl;},
[](const Test1 &t){ cout << "deal Test1 type..." << endl;},
[](const Test2 &t){ cout << "deal Test2 type..." << endl;},
[](const Test3 &t){ cout << "deal Test3 type..." << endl;},
[](const Test4 &t){ cout << "deal Test4 type..." << endl;}
},item);
sum_mutable_data += 1;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
private:
std::thread worker_; //线程对象
mutable std::atomic_bool to_quit_; //线程的回收标志
queue_t queue;
int sum_mutable_data;
};
void foo() {
Test1 t1;
Test2 t2;
Test3 t3;
Test4 t4;
Widget<Test1, Test2, Test3, Test4> w;
w.add_task(std::move(t1));
w.add_task(std::move(t2));
w.add_task(std::move(t3));
w.add_task(std::move(t4));
std::this_thread::sleep_for(std::chrono::seconds(10));
}
int main() {
foo();
return 0;
}
代码中还有一些要注意的细节值得探讨,但本文只关注为了实现多态,语言层面的支持。
总结:传统的多态实现需要构建继承体系,类型约束比较强,使用起来比较复杂;C++17中新的多态非常简练,且几乎没有类型约束,但灵活的代价是风险需要程序员自己承担。