polymorphic evolution

Marx once said: If you can use one interface to handle things, don't write two.

Polymorphism is set up to allow the client to use the same interface to handle different types of objects. Of course, the client here is in a broad sense. You write a set of interfaces and call it to another colleague. That colleague is your client. Even if you call it in another file, this "another file" can also called the client.

There are many reasons for doing this, but it can be summed up in one sentence: to make users feel comfortable using it.

There is no evidence to say, let's take a look at a small code first:

  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 }

Function overloading is used here to realize the so-called polymorphism. No matter what type of Test it is, users can pass it to common_interface without thinking.

Anyone who is familiar with C++ knows that function overloading is just a relatively low polymorphism, called static polymorphism. Correspondingly, there is another kind of dynamic polymorphism, that is, polymorphism realized through inheritance system, virtual function, virtual table pointer, etc., is polymorphism in the true sense.

This kind of polymorphism is relatively troublesome to implement. There must be an inheritance system, and then through virtual functions, through base class pointers or references to Barabara...

This kind of small demo, as long as you have learned C++, you must already have a picture in your head, so you don’t need to post the code, not to mention that Marx said that there is no need to post simple code to occupy space!

So let's look at a slightly more complicated scenario: if the data we want to process is sent from the upstream (this kind of sending is also generalized, not necessarily through the network). Then we need to use the same interface to process these data. If the sending end and the receiving end are synchronized, then using the function overloading in the above example can solve this problem.

But if the two are asynchronous, the problem will be much more complicated. First, there must be an asynchronous queue, and this queue must be able to put various types of data, which requires these data to have a common base class, and then the The base class pointer acts as a queue storage object. But the news from the upstream does not necessarily have such a common-base relationship. In the most extensive scenario, they may be irrelevant.

In order to put all the horses and cows into a queue, it is necessary to construct another inheritance embodiment as their "manager". These classes do not contain business functions and are managers in the true sense.

Asynchronous queue is a relatively independent module that can be found on the Internet. For convenience, here is one:

//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_;
};

Then there is the business code 

#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;
}

As the code shows, in order for the client (newly created thread) to use it well (no matter what type of object is in the queue, it is directly p->common_interface();), a lot of extra code is required to implement.

This behavior of writing a lot of useless codes in order to achieve a certain function can be considered as a shortcoming of the programming language, that is, the syntax for expressing certain semantics is too complicated. And this kind of semantics is polymorphism, one of the three major characteristics of object-oriented.

So later C++17 optimizes this part of the syntax, introduces the variant and visit functions, and uses the new syntax to achieve the same semantics, which is much simpler:

#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;
}
                                             

 There are still some details to pay attention to in the code that are worth discussing, but this article only focuses on language-level support for polymorphism.

Summary: The traditional polymorphic implementation needs to build an inheritance system, the type constraints are relatively strong, and it is more complicated to use; the new polymorphism in C++17 is very concise and has almost no type constraints, but the price of flexibility is that the risk requires programmers themselves bear.

Guess you like

Origin blog.csdn.net/cyfcsd/article/details/130689818